关于什么是JWT,本文就不多叙述,如果不太熟悉的话,可以查阅其他文献进行学习。
本文主要实现了:登录、登出、横向鉴权、Token的有效期管理,并添加了Token黑名单。这些应该够满足一些简单项目针对Token作用的需求。如需满足大型项目,可能还需设计其他模型。
说明:本文仅供学习参考,如有异议,欢迎指出。
关键字:JWT,鉴权。
1. 登录
1.1 颁发Token
登录时,根据自己的业务进行必要的账号验证。验证通过后,颁发Token。这里从Redis中取,可以避免多处登录时出现互斥的情况,后续开发可以实现单点登录等其他功能。如果要实现多端只能同时保持一个客户端登录,则可以直接生成新的Token,让缓存中Redis失效,强迫其他端的Token失效。
这里,我将用户的userId和userName存入了Token中。方便后面的横向鉴权。
String token = JwtUtil.generateToken(String.valueOf(selectConsumerDo.getId()), consumerDo.getUsername());
在生成Token时,首先想到Token的有效期。一般设置Token有效期为30天。
/**
* token过期时间
* 单位:天
*/
public static final Integer EXPIRY_DATE = 30;
1.2 有效期
但如何实现Token在特定时间后过期呢?
这里就借助于Redis实现了。可以将颁发的Token存入Redis缓存,并将过期时间设置为我们所需要的时间。
在颁发Token时,先检查Redis中是否存在有效的Token,有则直接颁发,并延长Token有效期。
Object redisToken = redisUtil.get(JwtTokenConstant.TOKEN_REDIS_KEY + req.getUsername());
if (!StringUtils.isEmpty(redisToken) && !"null".equals(redisToken.toString())) {
tokenResp.setToken(String.valueOf(redisToken));
resultResp.setData(tokenResp);
// 将Token存入Redis,延长Token有效期
redisUtil.set(JwtTokenConstant.TOKEN_REDIS_KEY + req.getUsername(), String.valueOf(redisToken), 12L, TimeUnit.HOURS);
LOGGER.debug("CommentServiceImpl.login end, resultResp = [{}]", resultResp);
return resultResp;
}
在鉴权时,当Redis里的Token失效时,我们即可判断用户当前Token 过期了。
// 取出Redis中的Token,并进行比较
Object redisToken = redisUtil.get(JwtTokenConstant.TOKEN_REDIS_KEY + userName);
if (!token.equals(redisToken)) {
errorToken(servletResponse);
return;
}
我这里用户的userName是唯一的,可根据自己的业务更换其他的key。
1.3 延长有效期
我将Token保存入Redis并设置了有效期为12h,但用户可能在连续使用12h后,被判断Token过期。
这里就需要在每次判断Token有效后,延长Redis中Tokne的有效期。这样可实现当在12h内无操作,再让Token过期。
redisUtil.set(JwtTokenConstant.TOKEN_REDIS_KEY + userName, token, 12L, TimeUnit.HOURS);
1.4 黑名单
Redis中过期的Token,其实这个Token还并未失效(Token的真实有效期为30天)。为了防止Redis同步错误,异常等情况,这里,我们可以添加一个黑名单,来管理这些过期的Token,加一个双重保证。
redisUtil.set(JwtTokenConstant.TOKEN_BLACKLIST_CACHE_PREFIX + token, token, 31L, TimeUnit.DAYS);
2. 鉴权
2.1 鉴定Token是否有效
我实现了一个继承FilterApiPermissionFilter
的实现类,用于过滤所有需要进行鉴权的接口。
基本思路和步骤是:
- 跳过不需要鉴权的接口
- 判断Token是否有效
- 判断Token是否在有效期(借助Redis)
- 判断Token是否在黑名单中
2.1.1 跳过不需要鉴权的接口
//跳过不需要验证的路径
if (urlMatches(request)) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
/**
* url匹配方法
*
* @param request http 请求
* @return 是否成功匹配
*/
private boolean urlMatches(HttpServletRequest request) {
if (CollectionUtils.isEmpty(whiteUrlList)) {
return true;
}
return whiteUrlList.stream().anyMatch(url -> {
AntPathRequestMatcher matcher = new AntPathRequestMatcher(url);
return matcher.matches(request);
});
}
2.1.2 判断Token是否有效
try {
parseToken = JwtUtil.parseToken(token);
}
catch (JWTVerificationException e) {
errorToken(servletResponse);
return;
}
2.1.3 判断Token是否在有效期(借助Redis)
// 取出Redis中的Token,并进行比较
Object redisToken = redisUtil.get(JwtTokenConstant.TOKEN_REDIS_KEY + userName);
if (!token.equals(redisToken)) {
errorToken(servletResponse);
return;
}
2.1.4 判断Token是否在黑名单中
Object blackToken = redisUtil.get(JwtTokenConstant.TOKEN_BLACKLIST_CACHE_PREFIX + token);
if (token.equals(blackToken)) {
redisUtil.expire(JwtTokenConstant.TOKEN_REDIS_KEY + userName, 0L, TimeUnit.SECONDS);
errorToken(servletResponse);
return;
}
经过以上,就可以实现针对接口进行鉴权。
2.2 横向鉴权
在实现业务过程中,有些接口,是需要进行横向鉴权的。例如某些信息,根据userId获取。这就需要用户只能获取自己userID的
信息。那如何判断用户上传的userId就是自己的呢?即如何进行横向鉴权呢?
就可以借助Token实现。
在上文中,我在Token中放入了用户的userId。这里就可以借助这个userId来实现。
以下为我实现横向鉴权的方法:
@Override
public void checkPermission(Long userId, HttpServletRequest request) {
try {
LOGGER.debug("PermissionUtilImpl.checkPermission, userId = [{}]", userId);
String token = request.getHeader(JwtTokenConstant.AUTHORIZATION);
// 解密Token
Map<String, String> tokenMap = JwtUtil.parseToken(token);
long id = Long.parseLong(tokenMap.get(JwtTokenConstant.USER_GUID));
if (id == userId) {
LOGGER.debug("PermissionUtilImpl.checkPermission success.");
return;
}
LOGGER.error("PermissionUtilImpl.checkPermission failure.");
throw new MusicException(MusicErrorCode.NO_PERMISSION, "NO permission!");
} catch (JWTVerificationException | NumberFormatException e) {
LOGGER.error("PermissionUtilImpl.checkPermission failure.");
throw new MusicException(MusicErrorCode.NO_PERMISSION, "NO permission!");
}
}
3. 登出
在用户登出后,要立马让Token失效。
首先就是让保存在Redis中的Token过期。其次就是在登出时,将Token加入黑名单。
String token = request.getHeader(JwtTokenConstant.AUTHORIZATION);
Map<String, String> parseToken = JwtUtil.parseToken(token);
// 让Redis中的Tokne立马失效
redisUtil.expire(JwtTokenConstant.TOKEN_REDIS_KEY + parseToken.get(JwtTokenConstant.USER_NAME), 0L, TimeUnit.SECONDS);
// 将Token加入黑名单。过期时间为31,大于Token的有效期30天。
redisUtil.set(JwtTokenConstant.TOKEN_BLACKLIST_CACHE_PREFIX + token, token, 31L, TimeUnit.DAYS);
以上就是我实现的全部过程和思路。
这里我没有贴上RedisUtil和JwtUtil的代码,可在我项目中获取。
项目还实现了其他功能,比如定时任务;利用对称加密算法对用户的密码等信息进行加密。如果有兴趣的话,后续再做讲解吧。
完整项目可从以下仓库获取:music-client-backend
参考
1.[SpringBoot实现JWT认证]: https://gitee.com/rayfoo/SpringBoot-JWT
2.[开发SpringBoot+Jwt+Vue的前后端分离后台管理系统VueAdmin-后端笔记]: https://www.markerhub.com/post/77