为什么选择JWT?
其最大的优点就是不需要存储介质,比如redis,JWT给我的感觉就是特别轻巧、简单便捷。
JWT是无状态的,token也都是一次性的,如果你想让这个token立即失效,是没办法的,只能等过期。
我也查阅了一下发现会有以下问题
注销登录等场景下token还有效,比如
- 退出登录 (这个可以清除客户端cookie存储的token)
- 修改密码 (由于我的JWT加的盐是包含了用户密码,所以这个问题也规避了)
- 用户的帐户被删除/暂停 (这个正常的话直接删掉token,让用户重新登陆,JWT的话就需要在拦截器里验证token对应的用户时,判断下状态,ps:好像正常也需要每次判断下,我之前都给忽略了)
- 用户由管理员注销 (这个。。。总不能注销时将用户加到一个集合里,拦截器里每次验证token时都判断下这集合吧,太不好了,真这样的话用Redis搞个黑名单)
综合来说,还是得利用其它介质辅助存储,比如Redis,还是给token加上了状态
JWT最适合的场景是不需要服务端保存用户状态的场景,比如如果考虑到 token 注销和 token 续签等场景的话,没有特别好的解决方案,大部分解决方案都给 token 加上了状态
JWT根据密钥进行加密组成一段字符串作为token,客户端每次请求时带上这个token,后端拦截器取到token后,会取出token包含的用户id等信息,然后用JWT解密验证是否有效即可。
JWT生成的token如下,可以看到由两个”.“所隔开,一共是三部分
- 第一部分称为头部(header), 声明类型以及加密的算法,然后base64加密得到
- 第二部分称为载荷(payload),存放的就是有效信息,比如签发者,过期时间,标识等, 然后base64加密得到
- 第三部分称为签证(signature),由 header +payload+secret(盐), 然后base64加密得到
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIxMyIsImV4cCI6MTU4NzMxNjQwM30.EFbQK_qocj1kQLt19mJKGulya2o9LQ_jHTRp3p7iCzk
要配合一些注解,拦截器的使用就不多说了,和下面这篇文章大概一样,最下面放下我所使用的代码,由于以前用的是Redis存储token,而我又不想直接删掉以前的代码,所以就让共存了,根据配置选择是Redis还是JWT。
首先添加JWT的以来,在pom.xml中
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.2</version>
</dependency>
TokenModel
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TokenModel implements Serializable {
private SysUserEntity user;
private String token;
}
JWT token操作类
@Component
public class JwtToken {
private final SysUserService sysUserService;
private final RedisUtil redisUtil;
public JwtToken(RedisUtil redisUtil, SysUserService sysUserService) {
this.redisUtil = redisUtil;
this.sysUserService = sysUserService;
}
/**
* 过期时长(秒)
*/
private static final int EXPIRE_DURATION = 60 * 60;
/**
* 生成token
*
* @param user
* @return
*/
public TokenModel makeToken(SysUserEntity user) {
//过期时间
Calendar nowTime = Calendar.getInstance();
nowTime.add(Calendar.SECOND, EXPIRE_DURATION);
Date expireTime = nowTime.getTime();
//生成token
String token = JWT.create().withAudience(user.getId().toString())
.withExpiresAt(expireTime)
.sign(Algorithm.HMAC256(user.getPassword()));
user.setPassword("");
return new TokenModel(user, token);
}
/**
* 根据token查询
*
* @param token
* @return
*/
public TokenModel query(String token) {
Object tokenStatus = redisUtil.get(token);
if (tokenStatus != null && "0".equals(tokenStatus.toString())) {
return null;
}
String userId = "";
try {
//解密并获得用户id
userId = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException e) {
return null;
}
if (StringUtils.isEmpty(userId)) {
return null;
}
var user = sysUserService.getById(userId);
if (user == null) {
return null;
}
// 验证token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
try {
jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
return null;
}
return new TokenModel(user, token);
}
/**
* 删除token
* @param token
*/
public void deleteToken(String token) {
//设置该token禁用
redisUtil.set(token, "0", EXPIRE_DURATION);
}
}
Redis token操作类
@Component
public class RedisToken {
/**
* 过期时长(秒)
*/
private static final long EXPIRE_DURATION = 60 * 60L;
private final RedisUtil redisUtil;
public RedisToken(RedisUtil redisUtil) {
this.redisUtil = redisUtil;
}
/**
* @param user
* @描述: 生成token并放到redis中
* @返回值:
* @创建人: LoneKing
* @创建时间: 19:56 2020/1/12
*/
public TokenModel makeToken(SysUserEntity user) {
//主要是根据用户密码和当前时间戳生成token
String tokenString = SecurityUtil.getMD5(user.getPassword() + System.currentTimeMillis());
TokenModel tokenModel = new TokenModel();
tokenModel.setToken(tokenString);
user.setPassword("");
tokenModel.setUser(user);
redisUtil.set(tokenString, tokenModel, EXPIRE_DURATION);
return tokenModel;
}
/**
* @param token
* @描述: 获取token Model
* @返回值:
* @创建人: LoneKing
* @创建时间: 19:57 2020/1/12
*/
public TokenModel query(String token) {
Object tokenModelJson = redisUtil.get(token);
if (tokenModelJson == null) {
return null;
}
try {
return JsonUtil.deserialize(tokenModelJson.toString(), TokenModel.class);
} catch (Exception e) {
return null;
}
}
/**
* @param token
* @描述: 删除token
* @返回值:
* @创建人: LoneKing
* @创建时间: 19:58 2020/1/12
*/
public void deleteToken(String token) {
redisUtil.del(token);
}
}
TokenService
public interface TokenService {
SysUserEntity queryUser(String token);
void deleteUser(String token);
TokenModel makeToken(SysUserEntity user);
}
@Service
public class TokenServiceImpl implements TokenService {
/**
* 是否使用JWT
*/
public static boolean useJwt = true;
private final RedisToken redisToken;
private final JwtToken jwtToken;
public TokenServiceImpl(RedisToken redisToken, JwtToken jwtToken) {
this.redisToken = redisToken;
this.jwtToken = jwtToken;
}
@Override
public SysUserEntity queryUser(String token) {
TokenModel tokenModel = null;
if (useJwt) {
tokenModel = jwtToken.query(token);
} else {
redisToken.query(token);
}
if (tokenModel == null) {
return null;
} else {
return tokenModel.getUser();
}
}
@Override
public void deleteUser(String token) {
if (useJwt) {
jwtToken.deleteToken(token);
} else {
redisToken.deleteToken(token);
}
}
@Override
public TokenModel makeToken(SysUserEntity user) {
if (useJwt) {
return jwtToken.makeToken(user);
} else {
return redisToken.makeToken(user);
}
}
}
拦截器
@Component
public class TokenHandleInterceptor implements HandlerInterceptor {
private static final String TOKEN = "Token";
private static final String USER = "User";
private static final String RANDOM = "Random";
private final TokenService tokenService;
public TokenHandleInterceptor(TokenService tokenService) {
this.tokenService = tokenService;
}
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
//如果不是映射到方法,直接通过
if (!(o instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = ((HandlerMethod) o);
Method method = handlerMethod.getMethod();
//获取方法和控制器上的TokenValid注解
var methodTokenValid = AnnotationUtils.findAnnotation(method, TokenValid.class) != null;
var methodNoTokenValid = AnnotationUtils.findAnnotation(method, NoTokenValid.class) != null;
var controllerTokenValid = AnnotationUtils.findAnnotation(handlerMethod.getBean().getClass(), TokenValid.class) != null;
var allowValid = (!methodNoTokenValid && controllerTokenValid) || methodTokenValid;
//如果控制器有注解但方法无 无需token验证
if (allowValid) {
//获取请求的token信息
String token = httpServletRequest.getHeader(TOKEN);
if (token == null || token.isEmpty()) {
token = httpServletRequest.getParameter(TOKEN);
}
if (token == null || token.isEmpty()) {
throw new SystemException(ErrorEnum.ERR_100);
}
//根据token获取到user
var user = tokenService.queryUser(token);
if (user != null) {
if (!UserStatusEnum.已激活.getValue().equals(user.getStatus())) {
throw new SystemException(ErrorEnum.ERR_100, "账户状态为" + UserStatusEnum.getNameByValue(user.getStatus()));
}
//将user 添加到 request中,以便后续操作获取user
httpServletRequest.setAttribute(USER, user);
return true;
} else {
throw new SystemException(ErrorEnum.ERR_100);
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
}
}