JWT的加密解密原理,token登出、改密失效、自动续期

时间:2024-10-20 07:01:00

文章目录

    • 1. 两种token认证方式
    • 2. jwt的token加密解密过程
    • 3. token登出、改密后失效
    • 4. token的自动续期、一定时间内无操作掉线

1. 两种token认证方式

传统的token认证

用户登录,服务端给前端返回token,并将token保存在服务端。
以后用户再来访问时,需要携带token,服务端获取token后再去数据库获取token做校验。

JWT的token认证

用户登录,服务端给用户返回一个token(服务端不保存)
以后用户再来访问时,需要携带token,服务端获取token做校验

两种认证方式对比:
jwt相对于传统的token认证,无需将token保存在服务端。


2. jwt的token加密解密过程

2.1 生成token

用户登录成功后,使用jwt创建一个token,并返回给用户,token格式如下

 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9  //第一段
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ //第二段
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c //第三段

注意:jwt生成的token是由三段字符串拼接而成,使用 . 连接起来

  • ①:token的第一段字符串:由下面的json数据通过base64(可逆)加密算法得到。
{
  "alg": "HS256",   //第三段字符串的不可逆加密类型HS256
  "typ": "JWT"   //token类型JWT
}
  • ② token的第二段字符串:是由下面的payload信息通过base64(可逆)加密算法得到
// payload信息 为自定义值,一般不放敏感信息
{
  "sub": "1234567890",   //用户id
  "name": "John Doe",		//用户名
  "exp": 1516239022		//token过期时间
}
  • ③:token的第三段字符串构成:
    1.先将第一段和第二段的密文拼接起来
    2.对拼接起来的密文字符串和自定义的盐进行 上边指定的HS256加密
    3.对HS256加密后的密文再做base64加密

注意:第一、二部分可以通过Base64解密得到,但第三部分不可以!

生成token代码如下

/**
     * 生成token
     * @return token
     * @Param  对象map结构
     */

    public static String generateToken(Map<String,Object> param){
        Date date = new Date();
        Date expireTime = new Date(date.getTime() + expire * 1000);
        String token = Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setClaims(param)	//数据
//                .setSubject(userId + "")
                .setIssuedAt(date)	
                .setExpiration(expireTime)	//过期时间
                .signWith(SignatureAlgorithm.HS256, secret) //秘钥
                .compact();

        return token;
    }


2.2 验证(解密)token

当用户再来访问时,需要携带token,后端需要对token进行校验

  • ①:获取token
  • ②:对token进行切割成三部分
  • ③:对第二段字符串进行base64解密,检测token是否超时?
  • ④:对第一二段字符串拼接,再次进行HS256加密,得到密文字符串
  • ⑤:对token的第三段HS256加密

解密验证代码:

/**
     * 验证token
     * @return  token正确返回对象,token不正确返回null
     */
    public static Claims getClaimByToken(String token){
        try {
          return Jwts.parser()
                    .setSigningKey(secret)  //获取秘钥
                    .parseClaimsJws(token)	//解析验证token
                    .getBody();
        }catch (Exception e){
            log.info("token验证失败",e);
            return  null;
        }
    }

ps : token一旦生成,在过期时间内永久有效,即使项目重启!想要失效token必须等待过期,或者重置盐值!
          
          

3. token登出、改密后失效

使用jwt时,一般修改密码或退出登录时,需要把正在使用的token做失效处理,防止别的客户端使用失效token访问信息。

  • 方案一:在每次修改密码或者退出登录后,修改一下自定义的盐值。当进行下次访问时,会根据自定义盐值验证token,修改了自定义盐值,自然访问不通过。
  • 方案二:利用数据库,存放一个修改或者登出的时间,在创建token时,标注上创建时间。如果这个创建时间小于修改或登出的时间,就表示它是修改或者登出之前的token,为过期token
    /**
     * 生成jwt token
     */
    public String generateToken(String userId,Boolean rememberMe) {
        Date nowDate = new Date();
        long expiration = rememberMe ? expireRemember : expire;
        //过期时间
        Date expireDate = new Date(nowDate.getTime() + expiration * 1000);
        System.out.println("登录过期时间  >>>>"+DateUtils.formatDateTime(expireDate));
        return Jwts
        		//创建一个JwtBuilder对象
        		.builder()
        		//设置头信息
                .setHeaderParam("typ", "JWT")
                //设置主题信息
                .setSubject(userId)
                .setIssuedAt(nowDate)//创建时间
                .setExpiration(expireDate)//过期时间
                //签名手段,参数1:算法,参数2:盐
                .signWith(SignatureAlgorithm.HS256, secret)
                //获取token
                .compact();
    }
    /**
     * token是否过期
     * @return  true:过期
     * lastLoginDate 数据库记录的最后一次登出时间
     * issueDate token 创建时间
     */
    public boolean isTokenExpired(Date expiration,Date lastLoginDate,Date issueDate) {
        //token创建时间小于数据库记录的最后一次登出时间 过期
        if(lastLoginDate == null){
            return expiration.before(new Date());
        }else{
            return issueDate.before(lastLoginDate);
        }
    }

拦截器的判断:

 if(jwtUtils.isTokenExpired(claims.getExpiration(),user.getLoginDate(),claims.getIssuedAt())){
               Result result = ResultGenerator.genFailResult(ResultCode.UNAUTHORIZED,"token失效,请重新登录");
               SendMsgUtil.sendJsonMessage(response,result);
               return false;
           }

          
          

4. token的自动续期、一定时间内无操作掉线

场景:用户登陆后,token的过期时间为30分钟,如果在这30分钟内没有操作,则重新登录,如果30分钟内有操作,就给token自动续一个新的时间。避免用户正在操作时掉线重登!
          

实现①:在jwt生成token时先不设置过期时间,过期时间的操作放在redis中。

  • ①:在登陆时,把用户信息(或者token)放进redis,并设置过期时间

  • ②:如果30分钟内用户有操作,前端带着token来访问,过滤器解析token得到用户信息,去redis中验证用户信息,验证成功则在redis中增加过期时间,验证失败,返回token错误。实现了token时间的自动更新。

  • ③:如果30分钟内用户无操作,redis中的用户信息已过期,此时再进行操作,token解析出的用户信息在redis中验证失败,则重新登录。实现了一定时间内无操作掉线!

实现②:使用access_token、refresh_token 解决

  • 登录获取token(包括访问令牌access_token,刷新令牌refresh_token),其中access_token设置过期时间为5分钟,refresh_token设置过期时间为30分钟。不能同时过期
  • 前端保存access_tokenrefresh_token,每次请求带着access_token去访问服务器资源
  • 服务器校验access_token有效性,通过解析access_token看是否能解析出用户信息。如果用户信息为null,说明token无效,返回401,让用户重新登录
  • 服务器端校验access_token是否过期
    • 如果access_token没有过期,则token正常,继续执行业务逻辑
    • 如果access_token过期,计算 过期后到当前的时间大小 是否在refresh_token过期时间之内(是否大于30 - 5 - 5 = 20分钟,为什么不是30 - 5 = 25分钟呢?主要是想对正在请求的用户token做一个缓存,保证在最后五分钟内,新、老token都有效!防止正在进行的请求token突然失效!),
      • 如果大于refresh_token的过期时间,则表示用户长时间无操作,token真正过期了,返回401,让用户重新登录
      • 如果小于refresh_token的过期时间,则继续让该access_token访问业务,但返回给前端标识,提示token已过期,让前端带着refresh_token去服务器获取新的access_token,并保存在前端,后续使用新的access_token去访问!