评论

收藏

[Java] SpringBoot JWT实现token登录刷新功能

编程语言 编程语言 发布于:2022-01-21 17:08 | 阅读数:546 | 评论:0

JWT本身是无状态的,这点有别于传统的session,不在服务端存储凭证。这种特性使其在分布式场景,更便于扩展使用。接下来通过本文给大家分享SpringBoot JWT实现token登录刷新功能,感兴趣的朋友一起看看吧
目录

  • 1. 什么是JWT
  • 2. JWT组成部分
  • 3. JWT加密方式
  • 4.实战
  • 5.总结

1. 什么是JWT
Json web token (JWT) 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。简答理解就是一个身份凭证,用于服务识别。
JWT本身是无状态的,这点有别于传统的session,不在服务端存储凭证。这种特性使其在分布式场景,更便于扩展使用。

2. JWT组成部分
JWT有三部分组成,头部(header),载荷(payload),是签名(signature)。

  • 头部
头部主要声明了类型(jwt),以及使用的加密算法( HMAC SHA256)

  • 载荷
载荷就是存放有自定义信息的地方,例如用户标识,截止日期等

  • 签名
签名进行对之前的数据添加一层防护,防止被篡改。
签名生成过程: base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密。
// base64加密后的header和base64加密后的payload使用.连接组成的字符串
String str=base64(header).base64(payload);
// 加盐secret进行加密
String sign=HMACSHA256(encodedString, 'secret');
3. JWT加密方式
jwt加密分为两种对称加密和非对称加密。

  • 对称加密
对称加密指使用同一秘钥进行加密,解密的操作。加密解密的速度比较快,适合数据比较长时的使用。常见的算法为DES、3DES等

  • 非对称加密
非对称指通过公钥进行加密,通过私钥进行解密。加密和解密花费的时间长、速度相对较慢,但安全性更高,只适合对少量数据的使用。常见的算法RSA、ECC等。
两种加密方法没有谁更好,只有哪种场景更合适。

4.实战
本例采用了spring2.x,jwt使用了nimbus-jose-jwt版本,当然其他的jwt版本也都类似,封装的都是不错的。
1.maven关键配置如下
<dependency>
      <groupId>com.nimbusds</groupId>
      <artifactId>nimbus-jose-jwt</artifactId>
      <version>9.12.1</version>
    </dependency>
     <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.2.72</version>
    </dependency>
2.jwt工具类
对于这里的秘钥:采用了userId+salt+uuid的方式保证,即使是同一个用户每次生成的serect都是不同的
对于校验token有效性,包含三个过程:

  • 格式是否合法
  • token是否在有效期内
  • token是否在刷新的有效期内
对于token超过有效期,但在刷新有效期内,返回特定的code,前端进行识别,发起请求刷新token,达到用户无感知的过程。
public class JwtUtil {
  private static final Logger log = LoggerFactory.getLogger(JwtUtil.class);
 
  private static final String BEARER_TYPE = "Bearer";
  private static final String PARAM_TOKEN = "token";
  /**
   * 秘钥
   */
  private static final String SECRET = "dfg#fh!Fdh3443";
  /**
   * 有效期12小时
   */
  private static final long EXPIRE_TIME = 12 * 3600 * 1000;
  /**
   * 刷新时间7天
   */
  private static final long REFRESH_TIME = 7 * 24 * 3600 * 1000;
 
 
  public static String generate(PayloadDTO payloadDTO)  {
    //创建JWS头,设置签名算法和类型
    JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256)
        .type(JOSEObjectType.JWT)
        .build();
    //将负载信息封装到Payload中
    Payload payload = new Payload(JSON.toJSONString(payloadDTO));
    //创建JWS对象
    JWSObject jwsObject = new JWSObject(jwsHeader, payload);
    try {
      //创建HMAC签名器
      JWSSigner jwsSigner = new MACSigner(payloadDTO.getUserId() + SECRET+payloadDTO.getJti());
      //签名
      jwsObject.sign(jwsSigner);
      return jwsObject.serialize();
    } catch (JOSEException e) {
      log.error("jwt生成器异常",e);
      throw new BizException(TOKEN_SIGNER);
    }
  }
 
 
  public static String freshToken(String token)   {
    PayloadDTO payloadDTO;
    try {
      //从token中解析JWS对象
      JWSObject jwsObject = JWSObject.parse(token);
      payloadDTO = JSON.parseObject(jwsObject.getPayload().toString(), PayloadDTO.class);
      // 校验格式是否合适
      verifyFormat(payloadDTO, jwsObject);
    }catch (ParseException e) {
      log.error("jwt解析异常",e);
      throw new BizException(TOKEN_PARSE);
    } catch (JOSEException e) {
      log.error("jwt生成器异常",e);
      throw new BizException(TOKEN_SIGNER);
    }
    // 校验是否过期,未过期直接返回原token
    if (payloadDTO.getExp() >= System.currentTimeMillis()) {
      return token;
    }
    // 校验是否处于刷新时间内,重新生成token
    if (payloadDTO.getRef() >= System.currentTimeMillis()) {
      getRefreshPayload(payloadDTO);
      return generate(payloadDTO);
    }
    throw new BizException(TOKEN_EXP);
  }
 
 
 
  private static void verifyFormat(PayloadDTO payloadDTO, JWSObject jwsObject) throws JOSEException {
    //创建HMAC验证器
    JWSVerifier jwsVerifier = new MACVerifier(payloadDTO.getUserId() + SECRET+payloadDTO.getJti());
    if (!jwsObject.verify(jwsVerifier)) {
      throw new BizException(TOKEN_ERROR);
    }
  }
 
 
  public static String getTokenFromHeader(HttpServletRequest request) {
    // 先从header取值
    String value = request.getHeader("Authorization");
    if (!StringUtils.hasText(value)) {
      // header不存在从参数中获取
      value = request.getParameter(PARAM_TOKEN);
      if (!StringUtils.hasText(value)) {
        throw new BizException(TOKEN_MUST);
      }
    }
    if (value.toLowerCase().startsWith(BEARER_TYPE.toLowerCase())) {
      return value.substring(BEARER_TYPE.length()).trim();
    }
    return value;
  }
 
 
  public static PayloadDTO verify(String token)  {
    PayloadDTO payloadDTO;
    try {
      //从token中解析JWS对象
      JWSObject jwsObject = JWSObject.parse(token);
      payloadDTO = JSON.parseObject(jwsObject.getPayload().toString(), PayloadDTO.class);
      // 校验格式是否合适
      verifyFormat(payloadDTO, jwsObject);
    }catch (ParseException e) {
      log.error("jwt解析异常",e);
      throw new BizException(TOKEN_PARSE);
    } catch (JOSEException e) {
      log.error("jwt生成器异常",e);
      throw new BizException(TOKEN_SIGNER);
    }
    // 校验是否过期
    if (payloadDTO.getExp() < System.currentTimeMillis()) {
      // 校验是否处于刷新时间内
      if (payloadDTO.getRef() >= System.currentTimeMillis()) {
        throw new BizException(TOKEN_REFRESH);
      }
      throw new BizException(TOKEN_EXP);
    }
    return payloadDTO;
  }
 
  public static PayloadDTO getDefaultPayload(Long userId) {
    long currentTimeMillis = System.currentTimeMillis();
    PayloadDTO payloadDTO = new PayloadDTO();
    payloadDTO.setJti(UUID.randomUUID().toString());
    payloadDTO.setExp(currentTimeMillis + EXPIRE_TIME);
    payloadDTO.setRef(currentTimeMillis + REFRESH_TIME);
    payloadDTO.setUserId(userId);
    return payloadDTO;
 
  }
 
  public static void getRefreshPayload(PayloadDTO payload) {
    long currentTimeMillis = System.currentTimeMillis();
    payload.setJti(UUID.randomUUID().toString());
    payload.setExp(currentTimeMillis + EXPIRE_TIME);
    payload.setRef(currentTimeMillis + REFRESH_TIME);
  }
}
3.权限拦截
本例中采用了自定义注解+切面的方式来实现token的校验过程。
自定义Auth注解提供了是否开启校验token,sign的选项,实际操作中可以添加更多的功能。
@Target(value = ElementType.METHOD)
@Documented
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Auth {
  /**
   * 是否校验token,默认开启
   */
  boolean token() default true;
 
  /**
   * 是否校验sign,默认关闭
   */
  boolean sign() default false;
}
切面部分指定了对Auth进行切面,这种方法比采用拦截器方式更加灵活些。
@Component
@Aspect
public class AuthAspect {
  @Autowired
  private HttpServletRequest request;
 
  @Pointcut("@annotation(com.rain.jwt.config.Auth)")
  private void authPointcut(){}
 
  @Around("authPointcut()")
  public Object handleControllerMethod(ProceedingJoinPoint joinPoint) throws Throwable {
    //获取目标对象对应的字节码对象
    Class<?> targetCls=joinPoint.getTarget().getClass();
    //获取方法签名信息从而获取方法名和参数类型
    MethodSignature ms= (MethodSignature) joinPoint.getSignature();
    //获取目标方法对象上注解中的属性值
    Auth auth=ms.getMethod().getAnnotation(Auth.class);
    // 校验签名
    if (auth.token()) {
      String token = JwtUtil.getTokenFromHeader(request);
      JwtUtil.verify(token);
    }
    // 校验签名
    if (auth.sign()) {
      // todo
    }
    return joinPoint.proceed();
  }
}
4.测试接口
@RestController
@RequestMapping(value="/user")
@Api(tags = "用户")
public class UserController {
 
 
  @PostMapping(value = "/login")
  @Auth(token = false)
  @ApiOperation("登录")
  public Result<String> login(String username,String password) {
    // 用户常规校验
    Long userId = 100L;
    // 用户信息存入缓存
    // 生成token
    String token = JwtUtil.generate(JwtUtil.getDefaultPayload(userId));
    return Result.success(token);
  }
 
  @GetMapping(value = "refreshToken")
  @Auth
  @ApiOperation("刷新token")
  public Result<String> refreshToken(String token) {
    String freshToken = JwtUtil.freshToken(token);
    return Result.success(freshToken);
  }
 
  @GetMapping(value = "test")
  @Auth
  @ApiOperation("测试")
  public Result<String> test() {
    return Result.success("测试成功");
  }
}
5.总结
许多同学使用jwt经常将获取到的token放在redis中,在服务器端控制其有效性。这是一种处理token的方式,但这种方式跟jwt的思路是背道而去的,jwt本身就提供了过期的信息,将token的生命周期放入服务器中,又何必采用jwt的方式呢?直接来个uuid不香么。
最后来个项目地址。
到此这篇关于SpringBoot JWT实现登录刷新token的文章就介绍到这了,更多相关SpringBoot JWT实现token登录内容请搜索CodeAE代码之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持CodeAE代码之家!
原文链接:https://blog.csdn.net/qq_34789577/article/details/120518212

关注下面的标签,发现更多相似文章