SpringSecurity 安全框架详解

news2024/11/15 16:25:41

SpringSecurity 安全框架详解

1.简介

先赘述一下身份认证和用户授权:

  • 用户认证(Authentication):系统通过校验用户提供的用户名和密码来验证该用户是否为系统中的合法主体,即是否可以访问该系统;
  • 用户授权(Authorization):系统为用户分配不同的角色,以获取对应的权限,即验证该用户是否有权限执行该操作;

Web应用的安全性包括用户认证和用户授权两个部分,而Spring Security(以下简称Security)基于Spring框架,正好可以完整解决该问题。

它的真正强大之处在于它可以轻松扩展以满足自定义要求。

2.原理

Security可以看做是由一组filter过滤器链组成的权限认证。它的整个工作流程如下所示:
在这里插入图片描述
图中绿色认证方式是可以配置的,橘黄色和蓝色的位置不可更改:

  • FilterSecurityInterceptor:最后的过滤器,它会决定当前的请求可不可以访问Controller
  • ExceptionTranslationFilter:异常过滤器,接收到异常消息时会引导用户进行认证;

2.1 项目准备

我们使用Spring Boot框架来集成。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.0</version>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

<!-- 阿里JSON解析器 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.74</version>
</dependency>

<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
    <version>2.10.6</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>

application.yml配置

spring:
 application:
  name: securityjwt
 datasource:
  driver-class-name: com.mysql.cj.jdbc.Driver
  url: jdbc:mysql://127.0.0.1:3306/cheetah?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
  username: root
  password: 123456

server:
 port: 8080

mybatis:
 mapper-locations: classpath:mapper/*.xml
 type-aliases-package: com.itcheetah.securityjwt.entity
 configuration:
  map-underscore-to-camel-case: true

rsa:
 key:
  pubKeyFile: C:\Users\Desktop\jwt\id_key_rsa.pub
  priKeyFile: C:\Users\Desktop\jwt\id_key_rsa

3.SQL文件

/**
\* sys_user_info
**/*

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

*-- ----------------------------*
*-- Table structure for sys_user_info*
*-- ----------------------------*
DROP TABLE IF EXISTS `sys_user_info`;
CREATE TABLE `sys_user_info` (
 `id` bigint(20) NOT NULL AUTO_INCREMENT,
 `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
 `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
 PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;


*/**
\* product_info
**/*

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

*-- ----------------------------*
*-- Table structure for product_info*
*-- ----------------------------*
DROP TABLE IF EXISTS `product_info`;
CREATE TABLE `product_info` (
 `id` bigint(20) NOT NULL AUTO_INCREMENT,
 `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
 `price` decimal(10, 4) NULL DEFAULT NULL,
 `create_date` datetime(0) NULL DEFAULT NULL,
 `update_date` datetime(0) NULL DEFAULT NULL,
 PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

2.2 jwt 引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!--Token生成与解析-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

引入之后启动项目,会有如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zW0bIJsk-1675318879342)(SpringSecurity%20%E5%AE%89%E5%85%A8%E6%A1%86%E6%9E%B6%E8%AF%A6%E8%A7%A3.assets/image-20230202110917813.png)]

其中用户名为user,密码为上图中的字符串。

2.3 有关Security类详解

2.3.1 SecurityConfig详解

//开启全局方法安全性*
@EnableGlobalMethodSecurity(prePostEnabled=true, securedEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  *//认证失败处理类*
  @Autowired
  private AuthenticationEntryPointImpl unauthorizedHandler;

  *//提供公钥私钥的配置类*
  @Autowired
  private RsaKeyProperties prop;

  @Autowired
  private UserInfoService userInfoService;
  
  @Override
  protected void configure(HttpSecurity httpSecurity) throws Exception {
    httpSecurity
        *// CSRF禁用,因为不使用session*
        .csrf().disable()
        *// 认证失败处理类*
        .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
        *// 基于token,所以不需要session*
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
        *// 过滤请求*
        .authorizeRequests()
        .antMatchers(
            HttpMethod.GET,
            "/*.html",
            "/**/*.html",
            "/**/*.css",
            "/**/*.js"
        ).permitAll()
        *// 除上面外的所有请求全部需要鉴权认证*
        .anyRequest().authenticated()
        .and()
        .headers().frameOptions().disable();
    *// 添加JWT filter*
    httpSecurity.addFilter(new TokenLoginFilter(super.authenticationManager(), prop))
        .addFilter(new TokenVerifyFilter(super.authenticationManager(), prop));
  }

  *//指定认证对象的来源*
  public void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    auth.userDetailsService(userInfoService)
    *//从前端传递过来的密码就会被加密,所以从数据库*
    *//查询到的密码必须是经过加密的,而这个过程都是*
    *//在用户注册的时候进行加密的。*
    .passwordEncoder(passwordEncoder());
  }

  *//密码加密*
  @Bean
  public BCryptPasswordEncoder passwordEncoder(){
    return new BCryptPasswordEncoder();
  }
}

「拦截规则」

  • anyRequest:匹配所有请求路径
  • accessSpringEl表达式结果为true时可以访问
  • anonymous:匿名可以访问
  • denyAll:用户不能访问
  • fullyAuthenticated:用户完全认证可以访问(非remember-me下自动登录)
  • hasAnyAuthority:如果有参数,参数表示权限,则其中任何一个权限可以访问
  • hasAnyRole:如果有参数,参数表示角色,则其中任何一个角色可以访问
  • hasAuthority:如果有参数,参数表示权限,则其权限可以访问
  • hasIpAddress:如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
  • hasRole:如果有参数,参数表示角色,则其角色可以访问
  • permitAll:用户可以任意访问
  • rememberMe:允许通过remember-me登录的用户访问
  • authenticated:用户登录后可访问

2.3.2 认证失败处理类

/**
 \* 返回未授权
 \*/*
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable {

  private static final long serialVersionUID = -8970718410437077606L;

  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
      throws IOException {
    int code = HttpStatus.UNAUTHORIZED;
    String msg = "认证失败,无法访问系统资源,请先登陆";
    ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
  }
}

2.4 认证流程

2.4.1 自定义认证过滤器

public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

  private AuthenticationManager authenticationManager;

  private RsaKeyProperties prop;

  public TokenLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
    this.authenticationManager = authenticationManager;
    this.prop = prop;
  }

  */**
   \* @author cheetah
   \* @description 登陆验证
   \* @date 2021/6/28 16:17
   \* @Param [request, response]
   \* @return org.springframework.security.core.Authentication
   **/*
  public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    try {
      UserPojo sysUser = new ObjectMapper().readValue(request.getInputStream(), UserPojo.class);
      UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword());
      return authenticationManager.authenticate(authRequest);
    }catch (Exception e){
      try {
        response.setContentType("application/json;charset=utf-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        PrintWriter out = response.getWriter();
        Map resultMap = new HashMap();
        resultMap.put("code", HttpServletResponse.SC_UNAUTHORIZED);
        resultMap.put("msg", "用户名或密码错误!");
        out.write(new ObjectMapper().writeValueAsString(resultMap));
        out.flush();
        out.close();
      }catch (Exception outEx){
        outEx.printStackTrace();
      }
      throw new RuntimeException(e);
    }
  }



  */**
   \* @author cheetah
   \* @description 登陆成功回调
   \* @date 2021/6/28 16:17
   \* @Param [request, response, chain, authResult]
   \* @return void
   **/*
  public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
    UserPojo user = new UserPojo();
    user.setUsername(authResult.getName());
    user.setRoles((List<RolePojo>)authResult.getAuthorities());
    *//通过私钥进行加密:token有效期一天*
    String token = JwtUtils.generateTokenExpireInMinutes(user, prop.getPrivateKey(), 24 * 60);
    response.addHeader("Authorization", "Bearer "+token);
    try {
      response.setContentType("application/json;charset=utf-8");
      response.setStatus(HttpServletResponse.SC_OK);
      PrintWriter out = response.getWriter();
      Map resultMap = new HashMap();
      resultMap.put("code", HttpServletResponse.SC_OK);
      resultMap.put("msg", "认证通过!");
      resultMap.put("token", token);
      out.write(new ObjectMapper().writeValueAsString(resultMap));
      out.flush();
      out.close();
    }catch (Exception outEx){
      outEx.printStackTrace();
    }
  }
}

2.4.2 登录流程

Security默认登录路径为/login,当我们调用该接口时,会在登录的时候调方法loadUserByUsername(username)

在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-332jqN6w-1675318879343)(SpringSecurity%20%E5%AE%89%E5%85%A8%E6%A1%86%E6%9E%B6%E8%AF%A6%E8%A7%A3.assets/image-20230202135750222.png)]

该方法的具体实现类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RiZgZTQH-1675318879343)(SpringSecurity%20%E5%AE%89%E5%85%A8%E6%A1%86%E6%9E%B6%E8%AF%A6%E8%A7%A3.assets/image-20230202135859270.png)]

如果用户名称不存在就直接抛出用户名或密码错误,如果用户名字存在,在把该用户对应的信息和权限用AdminUserDetails这个类接收

需要注意的是,这个接收用户信息的接受类需要实现SpringSecurity中的接口UserDetails

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FEORyhKT-1675318879344)(SpringSecurity%20%E5%AE%89%E5%85%A8%E6%A1%86%E6%9E%B6%E8%AF%A6%E8%A7%A3.assets/image-20230202140053587.png)]

如果密码正确的话,就把对应的用户用户信息和用户权限用UsernamePasswordAuthenticationToken初始化,并且增加Security框架中的过滤器增加该权限setAuthentication。

ps:这个类UsernamePasswordAuthenticationToken在下文验证token时会出现。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YPHPcIPI-1675318879344)(SpringSecurity%20%E5%AE%89%E5%85%A8%E6%A1%86%E6%9E%B6%E8%AF%A6%E8%A7%A3.assets/image-20230202140333577.png)]

如果Secruity框架都没啥问题了,这时候就掉用生成token的接口了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cfpfyeTp-1675318879344)(SpringSecurity%20%E5%AE%89%E5%85%A8%E6%A1%86%E6%9E%B6%E8%AF%A6%E8%A7%A3.assets/image-20230202140613346.png)]

token = jwtTokenUtil.generateToken(userDetails);

接口大概的实现方法,就是拿用户名称是做Jwt的载体

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fo1f6Kp0-1675318879344)(SpringSecurity%20%E5%AE%89%E5%85%A8%E6%A1%86%E6%9E%B6%E8%AF%A6%E8%A7%A3.assets/image-20230202140639629.png)]

2.4.3 这里顺便提一下Jwt

jwt简称json web token,他是由`.分割的三部分组成,这三部分依次是:

  • 头部(Header)
  • 负载(Payload)
  • 签名(Signature)

Header

JWT的Header中存储了所使用的加密算法和Token类型

{
   "alg" : "HS256",
   "typ" : "JWT"
}

Payload

payload表示负载,也是一个JSON对象,JWT规定了7个官方字段供选用。

iss (issuer) : 签发人
exp (expiration time) : 过期时间
sub (subject) : 主题
aud (audience) : 受众
nbf (Not Before) : 生效时间
iat (Issued At) : 签发时间
jti (JWT ID) : 编号

Signature

Signature部分是对前两部分的签名,防止数据篡改。
首先,需要指定一一个密钥(secret) 。 这个密钥只有服务器才知道,不能泄露给用户。然后,使用Header里面指定的签名算法(默认是HMAC SHA256),按照下面的公式产生签名。

JWT优缺点:

JWT拥有基于Token的会话管理方式所拥有的一切优势,不依赖Cookie,使得其可以防止CSRF攻击,也能在禁用Cookie的浏览器环境中正常运行。
而JWT的最大优势是服务端不再需要存储Session,使得服务端认证鉴权业务可以方便扩展,避免存储Session所需要引入的Redis等组件,降低了系统架构复杂度。但这也是JWT最大的劣势,由于有效期存储在Token中,JWT Token-旦签发,就会在有效期内-直可用,无法在服务端废止,当用户进行登出操作,只能依赖客户端删除掉本地存储的JWT Token,如果需要禁用用户,单纯使用JWT就无法做到了。

2.5 整体流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yDY6q4dD-1675318879345)(SpringSecurity%20%E5%AE%89%E5%85%A8%E6%A1%86%E6%9E%B6%E8%AF%A6%E8%A7%A3.assets/image-20230202141958118.png)]
具体代码地址
https://github.com/hongjiatao/spring-boot-anyDemo
可以的话点赞三连,也是对我最大的支持。谢谢

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/193142.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【JavaEE】文件操作IO之File 、InputStream、OutputStream 用法详解

目录 一、文件概念 &#xff08;1&#xff09;文件定义与组成 &#xff08;2&#xff09;文件的树形结构组织和目录 &#xff08;3&#xff09;文件路径 &#xff08;4&#xff09;文件分类 &#xff08;5&#xff09;文件操作 二、文件操作File类 &#xff08;1&…

20230202在AIO-3568J开发板在原厂Android12下增加ll命令

20230202在AIO-3568J开发板在原厂Android12下增加ll命令 2023/2/2 11:50 1、使用EVB2的DTS&#xff1a;rk3568-evb2-lp4x-v10.dts&#xff0c;ENG模式编译。没有ll命令&#xff01; console:/ $ console:/ $ ll /system/bin/sh: ll: inaccessible or not found 127|console:/ …

ESP-IDF:堆排序测试

堆排序测试 /堆排序测试/ void printheap (int arr[],int length) { for(int i0;i<length;i) { cout<<arr[i]<<" "; } cout<<endl; } void swapheap (int arr[],int a, int b) { int temp arr[a]; arr[a] arr[b]; arr[b] temp; } void he…

【数据库原理与SQL Server应用】Part04——数据库操作

【数据库原理与SQL Server应用】Part04——数据库操作一、数据库基本概念1.1 物理数据库1.1.1 页和区1.1.2 数据库文件1.1.3 文件组1.2 逻辑数据库1.3 SQL Server 的系统数据库和用户数据库1.4 报表服务器和报表数据库二、创建数据库2.1 管理工具界面方式创建数据库2.2 命令行方…

CSS列表与表格

目录 ​编辑 HTML 列表和 CSS 列表属性 不同的列表项目标记 实例 图像作为列表项标记 实例 定位列表项标记 实例 删除默认设置 实例 列表 - 简写属性 实例 设置列表的颜色样式 实例 更多实例 所有 CSS 列表属性 表格边框 实例 全宽表格 实例 双边框 合并…

【数据结构与算法】最小生成树 | 最短路径

&#x1f320;作者&#xff1a;阿亮joy. &#x1f386;专栏&#xff1a;《数据结构与算法要啸着学》 &#x1f387;座右铭&#xff1a;每个优秀的人都有一段沉默的时光&#xff0c;那段时光是付出了很多努力却得不到结果的日子&#xff0c;我们把它叫做扎根 目录&#x1f449;…

【图】邻接表存储图

目录 一、概念 图是什么 各种图的定义 二、图的存储结构 邻接矩阵 邻接表 代码实现邻接表存储图&#xff08;不含权重&#xff09; 一、概念 图是什么 图&#xff08;Graph)是由顶点的有穷非空集合和顶点之间边的集合组成&#xff0c;通常表示为:G(V,E)&#xff0c;其中…

Hystrix断路器执行原理

状态机 Hystrix断路器有三种状态,分别是关闭(Closed)、打开(Open)与半开(Half-Open),三种状态转化关系如下: Closed 断路器关闭:调用下游的请求正常通过Open 断路器打开:阻断对下游服务的调用,直接走 Fallback 逻辑Half-Open 断路器处于半开状态:SleepWindowInMi…

第 14 章python学习知识记录(一)

文章目录前言14.1 numpy的使用14.1.1 数字运算14.1.2 N维数组14.1.3 矩阵运算与广播14.1.4 元素访问14.2 Matplotlib的使用14.2.1 绘制简单图形14.2.2 绘制复杂图形14.2.3 显示图片14.3 os函数14.3.1 获取文件路径14.3.2 路径的基本操作14.4 tqdm的使用14.4.1 tqdm的导入和使用…

docker搭建hadoop和hive集群

一、安装docker并生成相关的镜像&#xff08;1&#xff09;安装docker安装docker教程https://www.runoob.com/docker/centos-docker-install.html只要在终端输入&#xff1a;sudo docker run hello-world后出现如下图的内容就证明安装docker成功了&#xff08;2&#xff09;拉取…

让HTTPS、SSH 共享端口的——工具SSLH

目录 安装 SSLH 配置 Apache 或 Nginx Web 服务器 配置 SSLH 测试 安装 SSLH sudo apt-get install sslh 配置 Apache 或 Nginx Web 服务器 编辑 Web 服务器&#xff08;nginx 或 apache&#xff09;配置文件并找到以下行&#xff1a; listen 443 ssl; 将其修改为&…

Uni-App 如何实现消息推送功能?

原文链接&#xff1a;Uni-App 如何实现消息推送功能&#xff1f; 前言 这里用的是uni-app自带的UniPush1.0&#xff08;个推服务&#xff09;&#xff0c;所以只针对UniPush1.0介绍实现步骤。 建议查阅的文章&#xff1a; UniPush 1.0 使用指南Unipush 常见问题 当然现在已…

Flink day01

Flink简介 Flink起源于Stratosphere项目&#xff0c;Stratosphere是在2010~2014年由3所地处柏林的大学和欧洲的一些其他的大学共同进行的研究项目&#xff0c;2014年4月Stratosphere的代码被复制并捐赠给了Apache软件基金会&#xff0c;参加这个孵化项目的初始成员是Stratosphe…

Flutter项目实战

1.Androidstudio 获取dart支持才会出现 下图&#xff0c;才可以单独运行 2.需要 flutter pub get 3.Android Studio 中使用 FlutterJsonBeanFactory 插件 注意点&#xff1a;需要保证该 Android Studio 窗口下是一个完整的Flutter项目&#xff08;窗口下有且仅有一个Flutter项目…

录音软件哪个好用?推荐3款亲测好用的录音软件

很多小伙伴不知道电脑如何录制声音&#xff1f;其实电脑录制声音很简单&#xff0c;借助一款好用录音软件&#xff0c;就可以轻松录制。那你知道录音软件哪个好用吗&#xff1f;小编平常的工作经常需要使用到录音软件。今天就给大家推荐3款亲测好用的录音软件&#xff0c;感兴趣…

k8s核心资源Service

1、介绍为一组pod&#xff08;一次部署&#xff09;统一暴露一个ip和端口&#xff0c;供外界访问。2、集群内对外统一暴露kubectl expose deploy <部署名> --port<对外暴露端口> --target-port<容器内部端口>kubectl expose deploy mynginx --port8000 --tar…

torch_geometric--Convolutional Layers

torch_geometric–Convolutional Layers 卷积层 MessagePassing 式中口口口表示可微的排列不变函数&#xff0c;例如: sum, mean, min, max 或者 mul 和 γΘ\gamma_{\Theta}γΘ​和ϕΘ\phi_{\Theta}ϕΘ​表示可微函数&#xff0c;例如 MLPs. 请参见这里的附带教程。 参数…

JDBC-BasicDAO

引入 之前的sql语句是固定的只能通过参数传入&#xff0c;要写那么多方法肯定不好弄 这就引出了DAO 这里是整个流程示意图 最下面开始说 就是mysql库的每一张表对应一个JavaBean 右边是我们获取和关闭连接的工具类 上面XXXDAO的意思是 每一个表&#xff0c;都有一个与之对应的D…

高压放大器在镓基液态金属微型马达驱动实验研究中的应用

实验名称&#xff1a;高压放大器在镓基液态金属微型马达驱动实验研究中的应用 研究方向&#xff1a;新型材料 测试目的&#xff1a; 微/纳马达虽然是一种以实际应用为基础的动力装置&#xff0c;但其在科学研究方面的价值也尤为重要。在微/纳米尺度下&#xff0c;它可以接受能量…

【测试如何学代码?】学习代码的最佳实践经验分享

为什么要写这篇&#xff1f; 经常在群里看到大家问&#xff1a;该选择哪门语言&#xff1f;哪门语言有钱途&#xff1f; 其实&#xff0c;不管哪门语言&#xff0c;只要深入学好了都不会差&#xff0c;当然&#xff0c;我们选择语言最好还是要和自己的技术方向及职业发展相匹配…