SpringSecurity(五)JWT令牌

基于Token的分离(无状态)

基于Token的前后端分离主打无状态,无状态服务是指在处理每个请求时,服务本身不会维持任何与请求相关的状态信息。每个请求被视为独立的、自包含的操作,服务只关注处理请求本身,而不关心前后请求之间的状态变化。也就是说,用户在发起请求时,服务器不会记录其信息,而是通过用户携带的Token信息来判断是哪一个用户:

  • 有状态:用户请求接口 -> 从Session中读取用户信息 -> 根据当前的用户来处理业务 -> 返回

  • 无状态:用户携带Token请求接口 -> 从请求中获取用户信息 -> 根据当前的用户来处理业务 -> 返回

无状态服务的优点包括:

  1. 服务端无需存储会话信息:传统的会话管理方式需要服务端存储用户的会话信息,包括用户的身份认证信息和会话状态。而使用Token,服务端无需存储任何会话信息,所有的认证信息都包含在Token中,使得服务端变得无状态,减轻了服务器的负担,同时也方便了服务的水平扩展。

  2. 减少网络延迟:传统的会话管理方式需要在每次请求中都携带会话标识,即使是无状态的RESTful API也需要携带身份认证信息。而使用Token,身份认证信息已经包含在Token中,只需要在请求的Authorization头部携带Token即可,减少了每次请求的数据量,减少了网络延迟。

  3. 客户端无需存储会话信息:传统的会话管理方式中,客户端需要存储会话标识,以便在每次请求中携带。而使用Token,客户端只需要保存Token即可,方便了客户端的存储和管理。

  4. 跨域支持:Token可以在各个不同的域名之间进行传递和使用,因为Token是通过签名来验证和保护数据完整性的,可以防止未经授权的修改。

一个JWT令牌由3部分组成:标头(Header)、有效载荷(Payload)和签名(Signature)。在传输的时候,会将JWT的钱2部分分别进行Base64编码后用.进行连接形成最终需要传输的字符串。

  • 标头:包含一些元数据信息,比如JWT签名所使用的加密算法,还有类型,这里统一都是JWT。

  • 有效载荷:包括用户名称、令牌发布时间、过期时间、JWT ID等,当然我们也可以自定义添加字段,我们的用户信息一般都在这里存放。

  • 签名:首先需要指定一个密钥,该密钥仅仅保存在服务器中,保证不能让其他用户知道。然后使用Header中指定的算法对Header和Payload进行base64加密之后的结果通过密钥计算哈希值,然后就得出一个签名哈希。这个会用于之后验证内容是否被篡改。

image.png

因此,JWT令牌实际上是一种经过加密的JSON数据,其中包含了用户名字、用户ID等信息,我们可以直接解密JWT令牌得到用户的信息

<dependency>
     <groupId>com.auth0</groupId>
     <artifactId>java-jwt</artifactId>
     <version>4.3.0</version>
</dependency>

image.png

服务器会在收到JWT时对签名进行重新计算,比较是否一致,来验证JWT是否被用户恶意修改,如果被修改肯定也是不能通过的。

SpringSecurity实现JWT校验

客户端发起的请求中会携带这样的的特殊请求头:

Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJzZWxmIiwic3ViIjoidXNlciIsImV4cCI6MTY5MDIxODE2NCwiaWF0IjoxNjkwMTgyMTY0LCJzY29wZSI6ImFwcCJ9.Z5-WMeulZyx60WeNxrQg2z2GiVquEHrsBl9V4dixbRkAD6rFp-6gCrcAXWkebs0i-we4xTQ7TZW0ltuhGYZ1GmEaj4F6BP9VN8fLq2aT7GhCJDgjikaTs-w5BbbOD2PN_vTAK_KeVGvYhWU4_l81cvilJWVXAhzMtwgPsz1Dkd04cWTCpI7ZZi-RQaBGYlullXtUrehYcjprla8N-bSpmeb3CBVM3kpAdehzfRpAGWXotN27PIKyAbtiJ0rqdvRmvlSztNY0_1IoO4TprMTUr-wjilGbJ5QTQaYUKRHcK3OJrProz9m8ztClSq0GRvFIB7HuMlYWNYwf7lkKpGvKDg

这里的Authorization请求头就是携带JWT的专用属性,值的格式为"Bearer Token",前面的Bearer代表身份验证方式,默认情况下有两种:

Basic 和 Bearer 是两种不同的身份验证方式。

Basic 是一种基本的身份验证方式,它将用户名和密码进行base64编码后,放在 Authorization 请求头中,用于向服务器验证用户身份。这种方式不够安全,因为它将密码以明文的形式传输,容易受到中间人攻击。

Bearer 是一种更安全的身份验证方式,它基于令牌(Token)来验证用户身份。Bearer 令牌是由身份验证服务器颁发给客户端的,客户端在每个请求中将令牌放在 Authorization 请求头的 Bearer 字段中。服务器会验证令牌的有效性和权限,以确定用户的身份。Bearer 令牌通常使用 JSON Web Token (JWT) 的形式进行传递和验证。

自行编写JWT校验拦截器来处理这些信息。

public class JwtUtils {
      //Jwt秘钥
    private static final String key = "abcdefghijklmn";

      //根据用户信息创建Jwt令牌
    public static String createJwt(UserDetails user){
        Algorithm algorithm = Algorithm.HMAC256(key);
        Calendar calendar = Calendar.getInstance();
        Date now = calendar.getTime();
        calendar.add(Calendar.SECOND, 3600 * 24 * 7);
        return JWT.create()
                .withClaim("name", user.getUsername())  //配置JWT自定义信息
                .withClaim("authorities", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList())
                .withExpiresAt(calendar.getTime())  //设置过期时间
                .withIssuedAt(now)    //设置创建创建时间
                .sign(algorithm);   //最终签名
    }

      //根据Jwt验证并解析用户信息
    public static UserDetails resolveJwt(String token){
        Algorithm algorithm = Algorithm.HMAC256(key);
        JWTVerifier jwtVerifier = JWT.require(algorithm).build();
        try {
            DecodedJWT verify = jwtVerifier.verify(token);  //对JWT令牌进行验证,看看是否被修改
            Map<String, Claim> claims = verify.getClaims();  //获取令牌中内容
            if(new Date().after(claims.get("exp").asDate())) //如果是过期令牌则返回null
                return null;
            else
                  //重新组装为UserDetails对象,包括用户名、授权信息等
                return User
                        .withUsername(claims.get("name").asString())
                        .password("")
                        .authorities(claims.get("authorities").asArray(String.class))
                        .build();
        } catch (JWTVerificationException e) {
            return null;
        }
    }
}

接着我们需要自行实现一个JwtAuthenticationFilter加入到SpringSecurity默认提供的过滤器链用于处理请求头中携带的JWT令牌,并配置登录状态:

public class JwtAuthenticationFilter extends OncePerRequestFilter {  
//继承OncePerRequestFilter表示每次请求过滤一次,用于快速编写JWT校验规则

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
          //首先从Header中取出JWT
        String authorization = request.getHeader("Authorization");
          //判断是否包含JWT且格式正确
        if (authorization != null && authorization.startsWith("Bearer ")) {
            String token = authorization.substring(7);    
              //开始解析成UserDetails对象,如果得到的是null说明解析失败,JWT有问题
            UserDetails user = JwtUtils.resolveJwt(token);
            if(user != null) {
                  //验证没有问题,那么就可以开始创建Authentication了,这里我们跟默认情况保持一致
                  //使用UsernamePasswordAuthenticationToken作为实体,填写相关用户信息进去
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                  //然后直接把配置好的Authentication塞给SecurityContext表示已经完成验证
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
          //最后放行,继续下一个过滤器
          //可能各位小伙伴会好奇,要是没验证成功不是应该拦截吗?这个其实没有关系的
          //因为如果没有验证失败上面是不会给SecurityContext设置Authentication的,后面直接就被拦截掉了
          //而且有可能用户发起的是用户名密码登录请求,这种情况也要放行的,不然怎么登录,所以说直接放行就好
        filterChain.doFilter(request, response);
    }
}

In Spring Security, Authentication represents the token or credentials that prove the identity of a user. It contains information about the authenticated user, such as their principal (username or user object) and their granted authorities (permissions).

最后我们来配置一下SecurityConfiguration配置类,其实配置方法跟之前还是差不多,用户依然可以使用表单进行登录,并且登录方式也是一样的,就是有两个新增的部分需要我们注意一下:

```Plain Text @Configuration public class SecurityConfiguration {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
                  //其他跟之前一样,就省略掉了
            ...  
            //将Session管理创建策略改成无状态,这样SpringSecurity就不会创建会话了,也不会采用之前那套机制记录用户,因为现在我们可以直接从JWT中获取信息
            .sessionManagement(conf -> {
                conf.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
            })
                  //添加我们用于处理JWT的过滤器到Security过滤器链中,注意要放在UsernamePasswordAuthenticationFilter之前
            .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            .build();
}

  //这个跟之前一样的写法,整合到一起处理,统一返回JSON格式
private void handleProcess(HttpServletRequest request,
                           HttpServletResponse response,
                           Object exceptionOrAuthentication) throws IOException {
    response.setContentType("application/json;charset=utf-8");
    PrintWriter writer = response.getWriter();
    if(exceptionOrAuthentication instanceof AccessDeniedException exception) {
        writer.write(RestBean.failure(403, exception.getMessage()).asJsonString());
    } else if(exceptionOrAuthentication instanceof AuthenticationException exception) {
        writer.write(RestBean.failure(401, exception.getMessage()).asJsonString());
    } else if(exceptionOrAuthentication instanceof Authentication authentication){
          //不过这里需要注意,在登录成功的时候需要返回我们生成的JWT令牌,这样客户端下次访问就可以携带这个令牌了,令牌过期之后就需要重新登录才可以
        writer.write(RestBean.success(JwtUtils.createJwt((User) authentication.getPrincipal())).asJsonString());
    }
}

}

最后我们创建一个测试使用的Controller来看看效果:

Plain Text @RestController public class TestController {

@GetMapping("/test")
public String test(){
    return "HelloWorld";
}

} ```

image.png

image.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

glassy_sky6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值