一、简介
JWT (Json Web Token)是一种基于JSON的开放标准。JWT本身没有定义任何技术实现,它只是定义了一种基于Token的会话管理的规则,涵盖Token需要包含的标准内容和Token的生成过程,广泛用于用户认证方面。
二、构成与特点
- 构成
一个完整JWT token 如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIxMjM0NSIsImlhdCI6MTY2NjYzNzYzM30._wyhiKoiOOmhz_2UaSskkepHx0wlIm3ug8HjJzp2J08
由 . 分割的三部分组成,依次分别是头部(Header)、负载(Payload)、签名(Signature),都分别经过Base64编码组成
-
Header
Header是一个json,它存储了所使用的加密算法和Token类型 -
Payload
Payload也是一个json,有7个官方字段可选用,也可自定义字段,但要注意不能放隐私信息,因为这里是经过Base64编码的,别人可以通过解码读取到- iss (issuer) : 签发人
- exp (expiration time) : 过期时间
- sub (subject) : 主题
- aud (audience) : 受众
- nbf (Not Before) : 生效时间
- iat (Issued At) : 签发时间
- jti (JWT ID) : 编号
-
Signature
这部分是对前面两部分的签名,签名公式
base64url(
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
your-256-bit-secret (秘钥加盐)
)
)
- 特点
-
安全性
JWT的payload使用Base64进行编码,所以是jwt中不能存储敏感数据,也不建议使用http协议传输,而是采用加密的https传输 -
无状态
JWT是无状态的,生成的token无法修改,想要修改只能重新签发新token -
无法废弃
签发的token在到期之前都是一直有效的,无法中途废弃,可以考虑结合redis实现废弃token功能 -
性能
JWT性能开销比传统大,因为编码后的JWT比较长,通常JWT存放在header里,请求时有可能出现header比body还大,而如果是传统token就是很短的一个字符串
JWT旨在验证时不用和数据库交互,但实际很多人都结合redis使用,以补齐token废弃、续签等短板,而JWT就成了一个token操作工具,说白了怎么方便怎么来。常见应用选型,例如管理后台这些使用传统token,而像有需要兼备ios android h5多端验证时,使用jwt明显会方便很多,还不用考虑跨域问题。
三、认证流程
四、实战
- 依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.1.0</version>
</dependency>
- Token类
@Service
public class TokenService {
@Value("{jwt.secret}")
private String secret;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 生成Token
* @param userId
* @return
*/
public String buildToken(String userId) {
if (StringUtils.isEmpty(userId)) {
throw new LoginException(StateCode.USER_NOT_FOUND);
}
String key = RedisEnum.TOKEN_VERSION.buildKey(userId);
Long value = redisTemplate.opsForValue().increment(key);
redisTemplate.expire(key, RedisEnum.TOKEN_VERSION.getExpiredTime(), TimeUnit.SECONDS);
Date now = new Date();
return JWT.create()
.withAudience(userId)
.withJWTId(String.valueOf(value))
.withIssuedAt(now)
// .withExpiresAt()
.sign(Algorithm.HMAC256(userId + secret));
}
/**
* 校验Token
* @param token
* @return
*/
public String verifyToken(String token) {
DecodedJWT decoded = JWT.decode(token);
String userId = decoded.getAudience().get(0);
String tokenVersion = decoded.getClaim("jti").asString();
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(userId + secret)).build();
verifier.verify(token);
// 从redis校验token version
String key = RedisEnum.TOKEN_VERSION.buildKey(userId);
String value = redisTemplate.opsForValue().get(key);
// 登录已过期
if (StringUtils.isEmpty(value) || !tokenVersion.equals(value)) {
throw new LoginException(StateCode.TOKEN_INVALID);
}
return userId;
}
/**
* 清除token
* @param userId
*/
public void clearToken(String userId) {
String key = RedisEnum.TOKEN_VERSION.buildKey(userId);
redisTemplate.opsForValue().increment(key);
redisTemplate.expire(key, RedisEnum.TOKEN_VERSION.getExpiredTime(), TimeUnit.SECONDS);
}
}
- 拦截器
/**
* 拦截器
*/
public class AuthenticationInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//检查是否有PassToken注释,有则跳过认证
Annotation[] annotations = method.getAnnotations();
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
if (!StringUtils.isEmpty(token)) {
try {
String userId = JWT.decode(token).getAudience().get(0);
request.setAttribute(UserConstant.USER_ID, userId);
} catch (JWTDecodeException j) {
//允许无token
}
}
return true;
}
}
if (StringUtils.isEmpty(token)) {
throw new LoginException(StateCode.LOGIN_NOT_TOKEN);
}
try {
//校验token
String userId = tokenService.verifyToken(token);
// 校验黑名单...
request.setAttribute(UserConstant.USER_ID, userId);
} catch (JWTDecodeException d) {
throw new LoginException(StateCode.LOGIN_PARAM_ERROR);
} catch (JWTVerificationException v) {
throw new LoginException(StateCode.TOKEN_INVALID);
}
return true;
}
}
- 拦截器配置类
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticateInterceptor())
.excludePathPatterns("/user/accountLogin")
.addPathPatterns("/**");
}
@Bean
public AuthenticationInterceptor authenticateInterceptor() {
return new AuthenticationInterceptor();
}
}
- PassToken注解
/**
*注解绕过token认证
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
- 登录业务
/**
* 账号登录
* @param apiRequest
* @return
*/
@PostMapping("/accountLogin")
public ApiResponse<String> accountLogin(@RequestBody ApiRequest<AccountLoginReq> apiRequest) {
AccountLoginReq data = apiRequest.getData();
UserDTO userDTO = userService.getUserByName(data.getUserName());
//用户登录验证...
//验证通过生成token
String userId = "zyz";//临时写死
String token = tokenService.buildToken(userId);
return ApiResponse.ok(token);
}
- 登出业务
/**
* 退出登录
* @param apiRequest
* @return
*/
@PostMapping("/logout")
public ApiResponse<String> logout(@RequestBody ApiRequest<Void> apiRequest, HttpServletRequest request) {
String userId = (String) request.getAttribute(UserConstant.USER_ID);
tokenService.clearToken(userId);
return ApiResponse.ok("登出成功!");
}
Github项目地址:https://github.com/zyz1130083243/JwtDemo.git