使用JWT 实现完整登录鉴权

226 阅读4分钟

关于什么是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的实现类,用于过滤所有需要进行鉴权的接口。

基本思路和步骤是:

  1. 跳过不需要鉴权的接口
  2. 判断Token是否有效
  3. 判断Token是否在有效期(借助Redis)
  4. 判断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

参考

[SpringBoot实现JWT认证] gitee.com/rayfoo/Spri…

[开发SpringBoot+Jwt+Vue的前后端分离后台管理系统VueAdmin-后端笔记] www.markerhub.com/post/77