文章目录

  • 1. 场景一:token的注销问题(黑名单)
  • 2. 场景二:token的续签问题
  • 3. 项目中的实现
    • 3.1 封装JWT工具类
    • 3.2 配置Shiro的自定义认证类
    • 3.3 登录和退出登录(token注销)
      • 3.3.1 登录接口
      • 3.3.2 退出登录
      • 3.3.3 在shiro的自定义认证类中添加认证规则
    • 3.4 修改密码(token注销)
    • 3.5 token续签问题(token续签)
    • 3.6 用户的角色发生了变化(token注销)
      • 3.6.1 更新角色
      • 3.6.2 删除角色
      • 3.6.3 在shiro的自定义认证类中添加认证规则
    • 3.7 用户的权限发生了变化(token注销)
      • 3.7.1 编辑权限
      • 3.7.2 删除权限
    • 3.8 用户被禁用(token注销)
      • 3.8.1 编辑用户
      • 3.8.2 在shiro的自定义认证类中添加认证规则
    • 3.9 用户被删除(token注销)
      • 3.9.1 删除用户
      • 3.9.2 在shiro的自定义认证类中添加认证规则

我觉得这个问题是一个很常见的问题,为了讲清楚这篇文章,参考了不少资料,也结合了实习时做的项目来讲,所以如果没聊清楚,请见谅;如有问题,请多指教;

参考:https://stackoverflow.com/questions/21978658/invalidating-json-web-tokens

1. 场景一:token的注销问题(黑名单)

注销登录等场景下 token 还有效的场景:

① 退出登录;

② 修改密码;

③ 用户的角色或者权限发生了改变;

④ 用户被禁用;

④ 用户被删除;

⑤ 用户被锁定;

⑥ 管理员注销用户;

这个问题不存在于 Session 认证方式中,因为在 Session 认证方式中,我们只需要删除服务端session中的记录即可。但是,使用 token 认证的方式就不好解决了,因为token是一次性的,token 一旦派发出去,如果后端不增加其他逻辑的话,它在失效之前都是有效的;

解决方法:

① 将 token 存入内存数据库:将 token 存入 DB 或redis中。如果需要让某个 token 失效就直接从 redis 中删除这个 token 即可。但是,这样会导致每次使用 token 发送请求都要先从redis中查询 token 是否存在的步骤,而且违背了 JWT 的无状态原则,不可取。

② 黑名单机制:使用内存数据库比如 redis 维护一个黑名单,如果想让某个 token 失效的话就直接将这个 token 加入到 黑名单 即可。然后,每次使用 token 进行请求的话都会先判断这个 token 是否存在于黑名单中。

说明:JWT 最适合的场景是不需要服务端保存用户状态的场景,但是如果考虑到 token 注销和 token 续签的场景话,没有特别好的解决方案,大部分解决方案都给 token 加上了状态,这就有点类似 Session 认证了。

2. 场景二:token的续签问题

token 有效期一般都建议设置的不太长,那么 token 过期后如何认证,如何实现动态刷新 token,避免用户经常需要重新登录?

① 类似于 Session 认证中的做法: 假设服务端给的 token 有效期设置为30分钟,服务端每次进行校验时,如果发现 token 的有效期马上快过期了,服务端就重新生成 token 给客户端。客户端每次请求都检查新旧token,如果不一致,则更新本地的token。这种做法的问题是仅仅在快过期的时候请求才会更新 token ,对客户端不是很友好。每次请求都返回新 token :这种方案的的思路很简单,但是,很明显,开销会比较大。

② 用户登录返回两个 token :第一个是 acessToken ,它的过期时间比较短,不如1天;另外一个是 refreshToken 它的过期时间更长一点比如为10天。客户端登录后,将 accessToken和refreshToken 保存在客户端本地,每次访问将 accessToken 传给服务端。服务端校验 accessToken 的有效性,如果过期的话,就将 refreshToken 传给服务端。如果 refreshToken 有效,服务端就生成新的 accessToken 给客户端。否则,客户端就重新登录即可。

该方案的不足是:① 需要客户端来配合;② 用户注销的时候需要同时保证两个 token 都无效;③ 重新请求获取 token 的过程中会有短暂 token 不可用的情况(可以通过在客户端设置定时器,当accessToken 快过期的时候,提前去通过 refreshToken 获取新的accessToken)。

3. 项目中的实现

在项目中对于token的注销问题使用了黑名单机制,对于token的续签问题使用了accessToken和refreshToken;接下来对上面提到的各种场景进行说明

3.1 封装JWT工具类

我们需要封装jWt的工具类,用来操作token,主要包括的方法,token的签发,生成accessToken和refreshToken,获取token的过期时间,token的剩余过期时间,解析token等等方法;

@Slf4j
@ConfigurationProperties(prefix = "jwt")
public class JwtTokenUtil {//token的秘钥private static String securityKey;private static Duration accessTokenExpireTime;private static Duration refreshTokenExpireTime;private static Duration refreshTokenExpireAppTime;private static String issuer;/*** 签发token*/public static String generateToken(String issuer, String subject, Map<String, Object> claims, long ttlMillis, String secret) {JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT").setSubject(subject).setIssuer(issuer).setIssuedAt(System.currentTimeMillis()).setClaims(claims).signWith(SignatureAlgorithm.HS256,  DatatypeConverter.parseBase64Binary(secret));if (ttlMillis >= 0) {//过期时间=当前时间+过期时长long nowMillis = System.currentTimeMillis();long expMillis = nowMillis + ttlMillis;Date exp = new Date(expMillis);builder.setExpiration(exp);}return builder.compact();}/*** 生成 access_token:这个过期时间比较短,token的过期时间是2小时*/public static String getAccessToken(String subject, Map<String, Object> claims) {return generateToken(issuer, subject, claims, accessTokenExpireTime.toMillis(), securityKey);}/*** 生成 PC refresh_token:这个过期时间比较长,是8小时*/public static String getRefreshToken(String subject, Map<String, Object> claims) {return generateToken(issuer, subject, claims, refreshTokenExpireTime.toMillis(), securityKey);}/*** 解析token:从token中获取claims*/public static Claims getClaimsFromToken(String token) {Claims claims = null;try {claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(securityKey)).parseClaimsJws(token).getBody();} catch (Exception e) {if (e instanceof ClaimJwtException) {claims = ((ClaimJwtException) e).getClaims();}}return claims;}/*** 获取用户id*/public static String getUserId(String token) {String userId = null;try {Claims claims = getClaimsFromToken(token);userId = claims.getSubject();} catch (Exception e) {log.error("eror={}", e);}return userId;}/*** 获取用户名*/public static String getUserName(String token) {String username = null;try {Claims claims = getClaimsFromToken(token);username = (String) claims.get(Constant.JWT_USER_NAME);} catch (Exception e) {log.error("eror={}", e);}return username;}/*** 验证token 是否过期(true:已过期 false:未过期)*/public static Boolean isTokenExpired(String token) {try {Claims claims = getClaimsFromToken(token);//token的过期时间 = 签发token时的时间 + 过期时长Date expiration = claims.getExpiration();return expiration.before(new Date());} catch (Exception e) {log.error("error={}", e);return true;}}/*** 验证token是否有效 (true:验证通过 false:验证失败)*/public static Boolean validateToken(String token) {Claims claimsFromToken = getClaimsFromToken(token);return (claimsFromToken != null && !isTokenExpired(token));}/*** 获取token的剩余过期时间*/public static long getRemainingTime(String token) {long result = 0;try {long nowMillis = System.currentTimeMillis();//剩余过期时间 = token的过期时间-当前时间result = getClaimsFromToken(token).getExpiration().getTime() - nowMillis;} catch (Exception e) {log.error("error={}", e);}return result;}
}

3.2 配置Shiro的自定义认证类

这个配置我在上一篇文章中Shiro+jwt实现认证和授权有讲到,这里不再赘述,主要想说这里面配置的比较重要的一个类,自定义的token的认证类,我们在使用Shiro 进行认证时会认证token,配置token的认证方式;

后面当我们解决token的续签问题和token的注销问题时,都会在这儿认证token,比如:退出登录时我们使用黑名单机制将 token 放入redis缓存,认证token的时候,就会去黑名单(redis缓存)中看看,如果黑名单中有,则验证失败。而这个认证的逻辑就是在这个自定义的类中配置的,并由Shiro完成认证;

public class CustomHashedCredentialsMatcher extends HashedCredentialsMatcher {@Autowiredprivate RedisService redisService;@Overridepublic boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {CustomUsernamePasswordToken customUsernamePasswordToken= (CustomUsernamePasswordToken) token;String accessToken = (String) customUsernamePasswordToken.getCredentials();String userId = JwtTokenUtil.getUserId(accessToken);//校验token,判断token是否有效if (!JwtTokenUtil.validateToken(accessToken)) {throw new BusinessException(BaseResponseCode.TOKEN_PAST_DUE);}return true;}
}

3.3 登录和退出登录(token注销)

为了凸显我想说的主题,所以一些类的封装代码和不重要的代码代码会省略掉,后文也是的;

3.3.1 登录接口

在第一次登录时,服务端会签发两个token分别是accessToken和refreshToken,并返回给客户端,保存在客户端本地,refreshToken(8小时)的过期时间比accessToken(2小时)的过期时间要长

@Service
public class UserServiceImpl implements UserService {@Overridepublic LoginRespVO login(LoginReqVO vo) {//一些密码用户认证的不重要信息已省略......Map<String, Object> claims = new HashMap<>();//向claims中存放用户信息和权限信息claims.put(Constant.ROLES_INFOS_KEY, getRoleByUserId(userInfoByName.getId()));claims.put(Constant.PERMISSIONS_INFOS_KEY, getPermissionByUserId(userInfoByName.getId()));claims.put(Constant.JWT_USER_NAME, userInfoByName.getUsername());//服务端生成accessTokenString accessToken = JwtTokenUtil.getAccessToken(userInfoByName.getId(), claims);//服务端生成refreshToke String refreshToken = JwtTokenUtil.getRefreshToken(userInfoByName.getId(), claims);//将accessToken和refreshToken返回给客户端并保存在客户端本地loginRespVO.setAccessToken(accessToken);loginRespVO.setRefreshToken(refreshToken);return loginRespVO;}
}

3.3.2 退出登录

退出登录时需要将accessToken和refreshToken同时失效,放入黑名单中:

@Service
public class UserServiceImpl implements UserService {@Overridepublic void logout(String accessToken, String refreshToken) {//从请求中获取accessToken和refreshTokenif (StringUtils.isEmpty(accessToken) || StringUtils.isEmpty(refreshToken)) {throw new BusinessException(BaseResponseCode.DATA_ERROR);}Subject subject = SecurityUtils.getSubject();if (subject != null) {//退出登录subject.logout();}String userId = JwtTokenUtil.getUserId(accessToken);//退出登录后需要保证accessToken和refreshToken都无效//把accessToken 加入黑名单,设置redis的过期时间和token的剩余过期时间相同redisService.set(Constant.JWT_ACCESS_TOKEN_BLACKLIST + accessToken, userId, JwtTokenUtil.getRemainingTime(accessToken), TimeUnit.MILLISECONDS);//把refreshToken 加入黑名单redisService.set(Constant.JWT_REFRESH_IDENTIFICATION + refreshToken, userId, JwtTokenUtil.getRemainingTime(refreshToken), TimeUnit.MILLISECONDS);}
}

3.3.3 在shiro的自定义认证类中添加认证规则

我们已经把accessToken和refreshToken加入了redis中(黑名单中),当用户再次访问时,我们需要判断这个黑名单中有没有token对应的key,如果有的话,token认证失败。

public class CustomHashedCredentialsMatcher extends HashedCredentialsMatcher {@Autowiredprivate RedisService redisService;@Overridepublic boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {//从用户的登录请求中获取accessTokenCustomUsernamePasswordToken customUsernamePasswordToken= (CustomUsernamePasswordToken) token;String accessToken = (String) customUsernamePasswordToken.getCredentials();String userId = JwtTokenUtil.getUserId(accessToken);//校验token,判断token是否有效if (!JwtTokenUtil.validateToken(accessToken)) {throw new BusinessException(BaseResponseCode.TOKEN_PAST_DUE);}//判断黑名单中有没有accessToken对应的key,如果有的话,认证失败抛出异常if (redisService.hasKey(Constant.JWT_ACCESS_TOKEN_BLACKLIST + accessToken)) {throw new BusinessException(BaseResponseCode.TOKEN_ERROR);}return true;}
}

3.4 修改密码(token注销)

当用户修改密码时,我们需要注销还没失效的token,因为之前的token已经不能在使用了,因此当用户修改密码后,将accessToken和refreshToken加入黑名单中,然后当用户再次访问时,判断黑名单中有没有对应的token,如果有,禁止访问,需重新登录。

@Service
public class UserServiceImpl implements UserService {@Overridepublic void userUpdatePwd(UserUpdatePwdReqVO vo, String accessToken, String refreshToken) {//判断token是否失效String userId = JwtTokenUtil.getUserId(accessToken);SysUser sysUser = sysUserMapper.selectByPrimaryKey(userId);if (sysUser == null) {throw new BusinessException(BaseResponseCode.TOKEN_ERROR);}//判断旧的密码是否正确if (!PasswordUtils.matches(sysUser.getSalt(), vo.getOldPwd(), sysUser.getPassword())) {throw new BusinessException(BaseResponseCode.OLD_PASSWORD_ERROR);}//保存新密码sysUser.setUpdateTime(new Date());sysUser.setUpdateId(userId);sysUser.setPassword(PasswordUtils.encode(vo.getNewPwd(), sysUser.getSalt()));int i = sysUserMapper.updateByPrimaryKeySelective(sysUser);if (i != 1) {throw new BusinessException(BaseResponseCode.OPERATION_ERROR);}//把token 加入黑名单 禁止再访问我们的系统资源,设置redis的过期时间和token的剩余过期时间相同redisService.set(Constant.JWT_ACCESS_TOKEN_BLACKLIST + accessToken, userId, JwtTokenUtil.getRemainingTime(accessToken), TimeUnit.MILLISECONDS);//把 refreshToken 加入黑名单 禁止再拿来刷新tokenredisService.set(Constant.JWT_REFRESH_TOKEN_BLACKLIST + refreshToken, userId, JwtTokenUtil.getRemainingTime(refreshToken), TimeUnit.MILLISECONDS);}
}

因为我们redis的key和退出登录时设置的key相同,因此不用再在shiro的自定义认证类中添加认证规则

3.5 token续签问题(token续签)

jwt 刷新有两种情况要考虑?

① 一种是管理员修改了该用户的角色/权限(需要主动去刷新)。角色和权限发生变化时之前签发的token就失效了,需要主动刷新token获取最先的角色和权限;
② 一种是之前签发的accessToken过期了,需要自动刷新通过refreshToken换取(生成)新的accessToken,自动刷新当前请求接口。

在刷新token时,前端请求需要携带之前保留的refreshToken,交给服务端去校验,服务端校验成功后,就会生成一个新的token,返回给前端,前端得到后就会保留在客户端本地(logstorage )中。

@Service
public class UserServiceImpl implements UserService {@Autowiredprivate RedisService redisService;//刷新token@Overridepublic String refreshToken(String refreshToken) {//它是否过期、是否被加如了黑名if (!JwtTokenUtil.validateToken(refreshToken) || redisService.hasKey(Constant.JWT_REFRESH_TOKEN_BLACKLIST + refreshToken)) {throw new BusinessException(BaseResponseCode.TOKEN_ERROR);}//从token中获取userId和userNameString userId = JwtTokenUtil.getUserId(refreshToken);String username = JwtTokenUtil.getUserName(refreshToken);//向claims中存放角色和权限等信息Map<String, Object> claims = new HashMap<>();claims.put(Constant.ROLES_INFOS_KEY, getRoleByUserId(userId));claims.put(Constant.PERMISSIONS_INFOS_KEY, getPermissionByUserId(userId));claims.put(Constant.JWT_USER_NAME, username);//生成新的token,token中包含了用户的最新角色和权限,userId、userNameString newAccessToken = JwtTokenUtil.getAccessToken(userId, claims);return newAccessToken;}
}

3.6 用户的角色发生了变化(token注销)

3.6.1 更新角色

这里涉及了jwt的自动刷新问题,也是我们上面提到的问题,当用户的角色发生变化时,旧的token中的角色信息已经不正确,我们需要主动刷新token,在token中保存更新过的角色信息。

因此当用户的角色发生变化时,需要标记该角色对应的用户,即放入redis的缓存中,认证的时候判断redis中有没有对应的key,如果有,再判断token有没有主动刷新过,如果主动刷新过则认证成功,否则认证失败;

@Service
public class RoleServiceImpl implements RoleService {@Overridepublic void updateRole(RoleUpdateReqVO vo) {//保存角色基本信息SysRole sysRole = sysRoleMapper.selectByPrimaryKey(vo.getId());if (null == sysRole) {throw new BusinessException(BaseResponseCode.DATA_ERROR);}BeanUtils.copyProperties(vo, sysRole);sysRole.setUpdateTime(new Date());int count = sysRoleMapper.updateByPrimaryKeySelective(sysRole);if (count != 1) {throw new BusinessException(BaseResponseCode.OPERATION_ERROR);}//修改该角色和菜单权限关联数据RolePermissionOperationReqVO reqVO = new RolePermissionOperationReqVO();reqVO.setRoleId(vo.getId());reqVO.setPermissionIds(vo.getPermissions());rolePermissionService.addRolePermission(reqVO);List<String> userIdsBtRoleId = userRoleService.getUserIdsBtRoleId(vo.getId());if (!userIdsBtRoleId.isEmpty()) {for (String userId :userIdsBtRoleId) {// 用户角色发生了变化,需要将该角色对应的用户标记起来,认证时判断token有没有主动刷新过// 设置redis的失效时间为accessToken的过期时长(配置的2h)redisService.set(Constant.JWT_REFRESH_KEY + userId, userId, JwtTokenUtil.getAccessTokenExpireTime().toMillis(), TimeUnit.MILLISECONDS);}}}
}

3.6.2 删除角色

删除角色原理和更新角色原理相同,不再赘述

@Service
public class RoleServiceImpl implements RoleService {@Override@Transactional(rollbackFor = Exception.class)public void deletedRole(String roleId) {//更新删除的角色数据SysRole sysRole = new SysRole();sysRole.setId(roleId);sysRole.setDeleted(0);sysRole.setUpdateTime(new Date());int i = sysRoleMapper.updateByPrimaryKeySelective(sysRole);if (i != 1) {throw new BusinessException(BaseResponseCode.OPERATION_ERROR);}//角色菜单权限关联数据删除rolePermissionService.removeByRoleId(roleId);List<String> userIdsBtRoleId = userRoleService.getUserIdsBtRoleId(roleId);//角色用户关联数据删除userRoleService.removeUserRoleId(roleId);//把跟该角色关联的用户标记起来,需要刷新tokenif (!userIdsBtRoleId.isEmpty()) {for (String userId :userIdsBtRoleId) {//用户角色发生了变化,标记用户 在用户认证的时候判断token是否主动刷过redisService.set(Constant.JWT_REFRESH_KEY + userId, userId, JwtTokenUtil.getAccessTokenExpireTime().toMillis(), TimeUnit.MILLISECONDS);}}}
}

3.6.3 在shiro的自定义认证类中添加认证规则

当用户角色发生变化时,token就需要重新认证,在Shiro的自定义认证类中,增加认证规则,步骤:

① 判断用户是否被标记了,如果被标记了说明用户的角色或者权限发生了变化

② 判断用户是否已经互动刷新过token

角色发生变化时,设置的redis的过期时长就是accessToken的过期时长即2小时,若角色变化后,用户主动刷新过token,那么redis的剩余过期时间一定小于新生成的accessToken的剩余过期时间,如果redis的剩余过期时间一定大于新生成的accessToken的剩余过期时间,说明没有刷新过,认证失败。

public class CustomHashedCredentialsMatcher extends HashedCredentialsMatcher {@Autowiredprivate RedisService redisService;@Overridepublic boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {//从用户的登录请求中获取accessTokenCustomUsernamePasswordToken customUsernamePasswordToken= (CustomUsernamePasswordToken) token;String accessToken = (String) customUsernamePasswordToken.getCredentials();String userId = JwtTokenUtil.getUserId(accessToken);//校验token,判断token是否有效if (!JwtTokenUtil.validateToken(accessToken)) {throw new BusinessException(BaseResponseCode.TOKEN_PAST_DUE);}//判断黑名单中有没有accessToken对应的key,如果有的话,认证失败抛出异常if (redisService.hasKey(Constant.JWT_ACCESS_TOKEN_BLACKLIST + accessToken)) {throw new BusinessException(BaseResponseCode.TOKEN_ERROR);}//判断用户是否被标记了,如果被标记了说明用户的角色或者权限发生了变化if (redisService.hasKey(Constant.JWT_REFRESH_KEY + userId)) {//判断用户是否已经互动刷新过token//角色发生变化时,设置的redis的过期时长就是accessToken的过期时长即2小时//若角色变化后,用户主动刷新过token,//那么redis的剩余过期时间一定小于新生成的accessToken的剩余过期时间//如果redis的剩余过期时间一定大于新生成的accessToken的剩余过期时间,说明没有刷新过,认证失败if (redisService.getExpire(Constant.JWT_REFRESH_KEY + userId, TimeUnit.MILLISECONDS) > JwtTokenUtil.getRemainingTime(accessToken)) {throw new BusinessException(BaseResponseCode.TOKEN_PAST_DUE);}}return true;}
}

3.7 用户的权限发生了变化(token注销)

3.7.1 编辑权限

原理和编辑角色相同,不再赘述

@Service
public class PermissionServiceImpl implements PermissionService {@Overridepublic void updatePermission(PermissionUpdateReqVO vo) {//校验数据SysPermission update = new SysPermission();BeanUtils.copyProperties(vo, update);verifyForm(update);update.setUpdateTime(new Date());int i = sysPermissionMapper.updateByPrimaryKeySelective(update);if (i != 1) {throw new BusinessException(BaseResponseCode.OPERATION_ERROR);}//判断授权标识符是否发生了变化(权限标识符发生了变化,或者权限状态发生了变化)if (!sysPermission.getPerms().equals(vo.getPerms()) || sysPermission.getStatus() != vo.getStatus()) {List<String> roleIdsByPermissionId = rolePermissionService.getRoleIdsByPermissionId(vo.getId());if (!roleIdsByPermissionId.isEmpty()) {List<String> userIdsByRoleIds = userRoleService.getUserIdsByRoleIds(roleIdsByPermissionId);if (!userIdsByRoleIds.isEmpty()) {for (String userId : userIdsByRoleIds) {//用户权限发生变化时,标记该权限对应的用户,在用户认证的时候判断token有没主动刷新redisService.set(Constant.JWT_REFRESH_KEY + userId, userId, JwtTokenUtil.getAccessTokenExpireTime().toMillis(), TimeUnit.MILLISECONDS);}}}}}
}

3.7.2 删除权限

@Override
@Transactional(rollbackFor = Exception.class)
public void deletedPermission(String permissionId) {//判断是否有子集菜单权限关联List<SysPermission> sysPermissions = sysPermissionMapper.selectChild(permissionId);//如果存在子集关联,那么就不能删除该权限if (!sysPermissions.isEmpty()) {throw new BusinessException(BaseResponseCode.ROLE_PERMISSION_RELATION);}SysPermission sysPermission = new SysPermission();sysPermission.setUpdateTime(new Date());sysPermission.setDeleted(0);sysPermission.setId(permissionId);//将数据库中的权限数据更新,即删除菜单权限int i = sysPermissionMapper.updateByPrimaryKeySelective(sysPermission);if (i != 1) {throw new BusinessException(BaseResponseCode.OPERATION_ERROR);}//通过permissionId获取roleId--->通过roleId获取userId--->标记该用户,重新签发tokenList<String> roleIdsByPermissionId= rolePermissionService.getRoleIdsByPermissionId(permissionId);//解除相关角色和该菜单权限的关联rolePermissionService.removeRoleByPermissionId(permissionId);if (!roleIdsByPermissionId.isEmpty()) {List<String> userIdsByRoleIds = userRoleService.getUserIdsByRoleIds(roleIdsByPermissionId);if (!userIdsByRoleIds.isEmpty()) {for (String userId : userIdsByRoleIds) {//用户权限发生变化时,标记该权限对应的用户,在用户认证的时候判断token有没主动刷新redisService.set(Constant.JWT_REFRESH_KEY + userId, userId, JwtTokenUtil.getAccessTokenExpireTime().toMillis(), TimeUnit.MILLISECONDS);}}}
}

因为用户角色发生变化和用户权限发生变化时,我们使用的是同一个key,因此不需要再 在shiro的自定义认证类中添加认证规则。

3.8 用户被禁用(token注销)

3.8.1 编辑用户

@Service
public class UserServiceImpl implements UserService {//编辑用户@Overridepublic void updateUserInfo(UserUpdateReqVO vo, String operationId) {SysUser sysUser = new SysUser();BeanUtils.copyProperties(vo, sysUser);sysUser.setUpdateTime(new Date());sysUser.setUpdateId(operationId);if (StringUtils.isEmpty(vo.getPassword())) {sysUser.setPassword(null);} else {String salt = PasswordUtils.getSalt();String endPwd = PasswordUtils.encode(vo.getPassword(), salt);sysUser.setSalt(salt);sysUser.setPassword(endPwd);}//更新用户int i = sysUserMapper.updateByPrimaryKeySelective(sysUser);if (i != 1) {throw new BusinessException(BaseResponseCode.OPERATION_ERROR);}//如果用户状态设置为2,说明被禁用,需要标记,认证时判断redis是否有这个key如果有认证不通过if (vo.getStatus() == 2) {redisService.set(Constant.ACCOUNT_LOCK_KEY + vo.getId(), vo.getId());} else {//如果用户状态不是2,需要将这个key从redis中删除redisService.delete(Constant.ACCOUNT_LOCK_KEY + vo.getId());}}
}

3.8.2 在shiro的自定义认证类中添加认证规则

当用户被禁用时,我们将这个key加入到redis中,认证的时候需要判断redis中有没有这个key,如果有的话,就认证失败。

public class CustomHashedCredentialsMatcher extends HashedCredentialsMatcher {@Autowiredprivate RedisService redisService;@Overridepublic boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {//从用户的登录请求中获取accessTokenCustomUsernamePasswordToken customUsernamePasswordToken= (CustomUsernamePasswordToken) token;String accessToken = (String) customUsernamePasswordToken.getCredentials();String userId = JwtTokenUtil.getUserId(accessToken);//校验token,判断token是否有效if (!JwtTokenUtil.validateToken(accessToken)) {throw new BusinessException(BaseResponseCode.TOKEN_PAST_DUE);}//判断黑名单中有没有accessToken对应的key,如果有的话,认证失败抛出异常if (redisService.hasKey(Constant.JWT_ACCESS_TOKEN_BLACKLIST + accessToken)) {throw new BusinessException(BaseResponseCode.TOKEN_ERROR);}//判断用户是否被标记了,如果被标记了说明用户的角色或者权限发生了变化if (redisService.hasKey(Constant.JWT_REFRESH_KEY + userId)) {//判断用户是否已经互动刷新过token//角色发生变化时,设置的redis的过期时长就是accessToken的过期时长即2小时//若角色变化后,用户主动刷新过token,//那么redis的剩余过期时间一定小于新生成的accessToken的剩余过期时间//如果redis的剩余过期时间一定大于新生成的accessToken的剩余过期时间,说明没有刷新过,认证失败if (redisService.getExpire(Constant.JWT_REFRESH_KEY + userId, TimeUnit.MILLISECONDS) > JwtTokenUtil.getRemainingTime(accessToken)) {throw new BusinessException(BaseResponseCode.TOKEN_PAST_DUE);}}//判断是否被锁定,入股redis中含有这个key,就认证失败if (redisService.hasKey(Constant.ACCOUNT_LOCK_KEY + userId)) {throw new BusinessException(BaseResponseCode.ACCOUNT_LOCK);}return true;}
}

3.9 用户被删除(token注销)

3.9.1 删除用户

这里需要注意的是redis的有效期问题,当用户被删除的时候,之前的签发的token都不能被使用了,因此需要设置redis的过期时长为refreshToken的过期时长,保证之前签发的refreshToken也会失效。

@Service
public class UserServiceImpl implements UserService {@Overridepublic void deletedUsers(List<String> list, String operationId) {SysUser sysUser = new SysUser();sysUser.setUpdateId(operationId);sysUser.setUpdateTime(new Date());//批量删除用户int i = sysUserMapper.deletedUsers(sysUser, list);if (i == 0) {throw new BusinessException(BaseResponseCode.OPERATION_ERROR);}/*** 当用户删除时,需要标记用户,认证的时候判断该用户是否被删除* redis的过期时间为refreshToken的过期时间,因为refreshToken的过期时间最长,* 需要保证在redis的有效期内,之前签发的所有的token都失效*/for (String userId : list) {redisService.set(Constant.DELETED_USER_KEY + userId, userId, JwtTokenUtil.getRefreshTokenExpireTime().toMillis(), TimeUnit.MILLISECONDS);}}
}

3.9.2 在shiro的自定义认证类中添加认证规则

public class CustomHashedCredentialsMatcher extends HashedCredentialsMatcher {@Autowiredprivate RedisService redisService;@Overridepublic boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {//从用户的登录请求中获取accessTokenCustomUsernamePasswordToken customUsernamePasswordToken= (CustomUsernamePasswordToken) token;String accessToken = (String) customUsernamePasswordToken.getCredentials();String userId = JwtTokenUtil.getUserId(accessToken);//校验token,判断token是否有效if (!JwtTokenUtil.validateToken(accessToken)) {throw new BusinessException(BaseResponseCode.TOKEN_PAST_DUE);}//判断黑名单中有没有accessToken对应的key,如果有的话,认证失败抛出异常if (redisService.hasKey(Constant.JWT_ACCESS_TOKEN_BLACKLIST + accessToken)) {throw new BusinessException(BaseResponseCode.TOKEN_ERROR);}//判断用户是否被标记了,如果被标记了说明用户的角色或者权限发生了变化if (redisService.hasKey(Constant.JWT_REFRESH_KEY + userId)) {//判断用户是否已经互动刷新过token//角色发生变化时,设置的redis的过期时长就是accessToken的过期时长即2小时//若角色变化后,用户主动刷新过token,//那么redis的剩余过期时间一定小于新生成的accessToken的剩余过期时间//如果redis的剩余过期时间一定大于新生成的accessToken的剩余过期时间,说明没有刷新过,认证失败if (redisService.getExpire(Constant.JWT_REFRESH_KEY + userId, TimeUnit.MILLISECONDS) > JwtTokenUtil.getRemainingTime(accessToken)) {throw new BusinessException(BaseResponseCode.TOKEN_PAST_DUE);}}//判断是否被锁定,如果redis中含有这个key,就认证失败if (redisService.hasKey(Constant.ACCOUNT_LOCK_KEY + userId)) {throw new BusinessException(BaseResponseCode.ACCOUNT_LOCK);}//判断用户是否被删除,如果redis中含有这个key,那么认证失败if (redisService.hasKey(Constant.DELETED_USER_KEY + userId)) {throw new BusinessException(BaseResponseCode.ACCOUNT_HAS_DELETED_ERROR);}return true;}
}

Shiro整合JWT:解决jwt注销和续签的问题相关推荐

  1. java shiro jwt_Springboot实现Shiro整合JWT的示例代码

    写在前面 之前想尝试把JWT和Shiro结合到一起,但是在网上查了些博客,也没太有看懂,所以就自己重新研究了一下Shiro的工作机制,然后自己想了个(傻逼)办法把JWT和Shiro整合到一起了 另外接 ...

  2. Springboot -Shiro整合JWT(注解形式)

    Springboot -Shiro整合JWT(注解形式) 在这里只展示核心代码,具体的请访问github 参考timo 依赖导入 <dependencies><dependency& ...

  3. jwt用户注销 PHP,关于JWT用户主动注销、强制登出、忘记密码、修改密码的一些思考...

    JWT(JSON WEB TOKEN)的特点是无状态,通常用来作为验证登录以及鉴权,在这一方面,JWT体现出了他的优点. 然而,如果使用JWT实现,用户主动注销.强制登出(禁止登陆).忘记密码.修改密 ...

  4. springBoot整合spring security+JWT实现单点登录与权限管理前后端分离

    在前一篇文章当中,我们介绍了springBoot整合spring security单体应用版,在这篇文章当中,我将介绍springBoot整合spring secury+JWT实现单点登录与权限管理. ...

  5. springBoot整合spring security+JWT实现单点登录与权限管理前后端分离--筑基中期

    写在前面 在前一篇文章当中,我们介绍了springBoot整合spring security单体应用版,在这篇文章当中,我将介绍springBoot整合spring secury+JWT实现单点登录与 ...

  6. Shiro 整合SpringMVC 并实现权限管理,登录和注销

    Shiro 整合SpringMVC 并且实现权限管理,登录和注销 Apache Shiro是Java的一个安全框架.目前,使用Apache Shiro的人越来越多,因为它相当简单,对比Spring S ...

  7. Spring boot 整合Spring Security Jwt

    记录学习Spring boot 整合Spring Security Jwt 学习参考 – 慢慢的干货 https://shimo.im/docs/OnZDwoxFFL8bnP1c/read 首先创建S ...

  8. GitHub开源项目学习 电商系统Mall (五) mall整合SpringSecurity和JWT实现认证和授权(二)

    mall整合SpringSecurity和JWT实现认证和授权(二) https://github.com/macrozheng/mall 登录注册功能实现 UmsAdminController类 实 ...

  9. GitHub开源项目学习 电商系统Mall (四) mall整合SpringSecurity和JWT实现认证和授权(一)

    mall整合SpringSecurity和JWT实现认证和授权(一) https://github.com/macrozheng/mall 跳过了官方Learning中较简单的Swagger-UI的实 ...

  10. 商城项目(三)整合SpringSecurity和JWT实现认证和授权

    整合SpringSecurity和JWT实现认证和授权 环境搭建 SpringSecurity JWT Hutool 项目使用表说明 ums_admin:后台用户表 ums_role:后台用户角色表 ...

最新文章

  1. 安卓平分位置layout_weight学习记录
  2. eclipse中出现Context startup failed due to previous errors错误解决方法
  3. Redisbook学习笔记(3)数据类型之集合
  4. (转贴) C#编码标准--编码习惯
  5. 一文带你了解MySQL中的各种锁机制!
  6. 11-Elasticsearch的X-Pack
  7. 红外传感器型号和参数_浅析温度传感器原理
  8. CCF 201812-2小明放学
  9. 详解:Sqoop的安装
  10. 贝叶斯估计原理及流程
  11. 为什么手工drop_caches之后cache值并未减少?
  12. 自己写的代码太low?想提升代码质量但是不知道怎么做?IDEA安这个插件~
  13. Oo0代码混淆实现方法
  14. c语言基础题(笔记四)
  15. Visual Studio 2022
  16. kafka 消费者的消费策略以及再平衡
  17. 高数 | 多元函数求极限 使用极坐标代换的条件与细节
  18. JAVA 编辑工具JCreator的环境配置说明(jdk、jdk help)
  19. SSM框架整合完整案例
  20. 十二、详解计算网络中的流量控制和差错控制、HDLC

热门文章

  1. python自动化办公入门书籍-视频教程-零基础Python自动化办公(漫画版)-Python
  2. 40+ 新鲜漂亮的大背景网站设计
  3. 《工程伦理》网课第十一章课后习题答案
  4. linux excel转csv
  5. 董明珠引领格力电器,让中国制造赢得全球认可!
  6. 亚马逊、沃尔玛如何提升产品listing?自养号测评如何操作更安全?
  7. Bootstrap_03之全局CSS
  8. idea 新手遭遇战
  9. 关于NPM下载源的总结
  10. 阿里技术三板斧:关于技术规划、管理、架构的思考