若依项目学习笔记05——JWT

1. 什么是JWT

关于JWT,在上一篇博文中有一个学习链接,大家看一下;本学习系列对涉及到的技术不会过多去讲解,这里只对JWT进行大致介绍,大家自行去学习吧;

JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案,通过客户端保存数据,而服务器根本不保存会话数据,每个请求都被发送回服务器;JWT由三段信息构成的,将这三段信息文本用 . 链接一起就构成了Jwt字符串。这三段分别为头部、载荷和签证,就像这样==> eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

1.1 JWT组成

  1. header 头部
{
    'typ': 'JWT',
    'alg': 'HS256'
}

由两个部分组成,分别是声明类型(JWT)和声明加密的算法(一般为HMAC SHA256),上面是加密前

1.2 playload 载荷

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

保存用户信息,分别包括:

  1. 标准中注册的声明(建议但不强制)

iss: jwt签发者
sub: 主题
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 生效时间
iat: 签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击

  1. 公共的声明

可以添加任意的信息,一般添加用户的相关信息或其他业务需要的必要信息;不建议添加敏感信息,因为该部分在客户端可解密

  1. 私有的声明

是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,则着该部分信息可以归类为明文信息

1.3 Signature 签证

将前面两个部分(加密后)使用头部声明的加密方式进行secret组合加密,就得到三段加密后,用 . 进行分割的jwt字符串

注意! secret和jwt的签发生成都是在服务器端,secret是用于进行jwt的签发和jwt的验证,≈你服务端的私钥,千万不可泄漏!。不然客户端知道这个secret,则客户端可以自我签发jwt


2. JWT实现

在登录验证SysLoginController的最后,会生成token(即jwt,会传到前端,前端根据这个jwt来和后端进行交互),这里调用的是token验证处理framework.web.service.TokenService,我们先来看她的 createToken() 方法,相关讲解在注释中给出,大家自行点进各方法进行查看

2.1 创建token

public String createToken(LoginUser loginUser)
    {
        //设置token唯一标识
        String token = IdUtils.fastUUID();
        loginUser.setToken(token);
        //设置浏览器相关信息,如IP地址,系统版本等等
        setUserAgent(loginUser);
        //设置缓存,如有效期及时长,组装唯一标识:Constants.LOGIN_TOKEN_KEY + uuid
        refreshToken(loginUser);
        //给前端返回jwt的值
        Map<String, Object> claims = new HashMap<>();
        //这里的token是标识
        claims.put(Constants.LOGIN_USER_KEY, token);
        //创建,见下
        return createToken(claims);
}

最后是返回创建一个token,使用的是 createToken() 方法,我们点进去looklook

private String createToken(Map<String, Object> claims)
{
    //这里所调用的是Jwts的api,我们已经在pom中导入了,用于token的生成和解析
    String token = Jwts.builder()
            .setClaims(claims)
             //设置加密方法,密钥
            .signWith(SignatureAlgorithm.HS512, secret).compact();
    return token;
}

这个方法所返回的字符串就是前端所拿到的token,我们可以运行项目看一下(大家也可以去redis中去看)

jwt

2.2 获取token

我们找到 getLoginUser()

public LoginUser getLoginUser(HttpServletRequest request)
{
    // 获取请求携带的令牌,用getToken从header(已在本类和配置文件中定义,)中获取,判断是否为空并且是否有以 Bearer 为前缀,然后替换(我们只要jwt,不用前缀)
    String token = getToken(request);
    if (StringUtils.isNotEmpty(token))
    {
        //获取token,secret已在本类和配置文件定义,secret尽量不可重复和无规律
        Claims claims = parseToken(token);
        // 解析对应的权限以及用户信息
        String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
        String userKey = getTokenKey(uuid);
        //根据key从缓存中获取
        LoginUser user = redisCache.getCacheObject(userKey);
        return user;
    }
    return null;
}

2.2 其他方法

这部分就不详细讲了,大致讲一下其作用

  • setLoginUser() : 设置用户信息,比如我们更新了用户信息,密码等,就会刷新token

  • delLoginUser(S) : 删除用户身份信息,根据token删除缓存中的用户数据

  • verifyToken() : 验证令牌有效期,相差不足20分钟,自动刷新缓存,这样就不会说,操作这个系统的人还在使用,结果到了一定实现就掉线了

  • refreshToken() : 刷新令牌有效期,令牌有效期expireTime已在本类和配置文件定义了


3. JWT过滤器

我们回到最初的起点,回到 SecurityConfig,我们添加了jwt过滤器


...

@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;//token认证过滤器

...

// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

...

我们进到自定义的 JwtAuthenticationTokenFilter 当中,

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter//继承OncePerRequestFilter,确保一次请求只会通过一次
{
    @Autowired
    private TokenService tokenService;//这个就是上面刚刚说的jwt实现啦

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException
    {
        //获取loginUser,具体操作上面已讲
        LoginUser loginUser = tokenService.getLoginUser(request);
        if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
        {
            //验证,刷新令牌有效期
            tokenService.verifyToken(loginUser);
            //通用设置,如loginUser,权限等
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            //设置request信息
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            //将上面的信息放置到上下文当中
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        chain.doFilter(request, response);
    }
}

如果所需信息没有设置进上下文,直接进入过滤器处理 chain.doFilter(request, response); 的话,会出现异常,我们可以进入security总过滤器类 FilterSecurityInterceptor.invoke 方法,其中有一个预检查方法 beforeInvocation() ,会验证角色或菜单是否有配置对应权限,或者URL是否合法等等;如果有异常会进入错误异常过滤器 ExceptionTranslationFilter ,如果异常不为空,会进入 handleSpringSecurityException 异常,然后调用 sendStartAuthentication()方法,其中, commence() 方法被我们在 AuthenticationEntryPointImpl 重写了

@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
   {
       //设置错误code和错误描述
       int code = HttpStatus.UNAUTHORIZED;
       String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
       //通过流,发送给前端
       ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
   }
}

总结图示

jwt流程

posted @ 2020-11-28 11:00  刘条条  阅读(1263)  评论(0编辑  收藏  举报