1. 什么是JWT
Json web token (JWT) 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。简答理解就是一个身份凭证,用于服务识别。 JWT本身是无状态的,这点有别于传统的session,不在服务端存储凭证。这种特性使其在分布式场景,更便于扩展使用。
2. JWT组成部分
JWT有三部分组成,头部(header),载荷(payload),是签名(signature)。
- 头部
头部主要声明了类型(jwt),以及使用的加密算法( HMAC SHA256) - 载荷
载荷就是存放有自定义信息的地方,例如用户标识,截止日期等 - 签名
签名进行对之前的数据添加一层防护,防止被篡改。 签名生成过程: base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密。
String str=base64(header).base64(payload);
String sign=HMACSHA256(encodedString, 'secret');
3. JWT加密方式
jwt加密分为两种对称加密和非对称加密。
- 对称加密
对称加密指使用同一秘钥进行加密,解密的操作。加密解密的速度比较快,适合数据比较长时的使用。常见的算法为DES、3DES等 - 非对称加密
非对称指通过公钥进行加密,通过私钥进行解密。加密和解密花费的时间长、速度相对较慢,但安全性更高,只适合对少量数据的使用。常见的算法RSA、ECC等。 两种加密方法没有谁更好,只有哪种场景更合适。
4.实战
本例采用了spring2.x,jwt使用了nimbus-jose-jwt版本,当然其他的jwt版本也都类似,封装的都是不错的。
- 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>
- 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";
private static final long EXPIRE_TIME = 12 * 3600 * 1000;
private static final long REFRESH_TIME = 7 * 24 * 3600 * 1000;
public static String generate(PayloadDTO payloadDTO) {
JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256)
.type(JOSEObjectType.JWT)
.build();
Payload payload = new Payload(JSON.toJSONString(payloadDTO));
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
try {
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 {
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()) {
return 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 {
JWSVerifier jwsVerifier = new MACVerifier(payloadDTO.getUserId() + SECRET+payloadDTO.getJti());
if (!jwsObject.verify(jwsVerifier)) {
throw new BizException(TOKEN_ERROR);
}
}
public static String getTokenFromHeader(HttpServletRequest request) {
String value = request.getHeader("Authorization");
if (!StringUtils.hasText(value)) {
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 {
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);
}
}
- 权限拦截
本例中采用了自定义注解+切面的方式来实现token的校验过程。 自定义Auth注解提供了是否开启校验token,sign的选项,实际操作中可以添加更多的功能。
@Target(value = ElementType.METHOD)
@Documented
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Auth {
boolean token() default true;
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()) {
}
return joinPoint.proceed();
}
}
- 测试接口
@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;
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不香么。 最后来个项目地址。
|