文章目录
我觉得这个问题是一个很常见的问题,为了讲清楚这篇文章,参考了不少资料,也结合了实习时做的项目来讲,所以如果没聊清楚,请见谅;如有问题,请多指教;
参考: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 {
@Autowired
private RedisService redisService;
@Override
public 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 {
@Override
public 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());
//服务端生成accessToken
String 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 {
@Override
public void logout(String accessToken, String refreshToken) {
//从请求中获取accessToken和refreshToken
if (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 {
@Autowired
private RedisService redisService;
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
//从用户的登录请求中获取accessToken
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);
}
//判断黑名单中有没有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 {
@Override
public 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 加入黑名单 禁止再拿来刷新token
redisService.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 {
@Autowired
private RedisService redisService;
//刷新token
@Override
public String refreshToken(String refreshToken) {
//它是否过期、是否被加如了黑名
if (!JwtTokenUtil.validateToken(refreshToken) || redisService.hasKey(Constant.JWT_REFRESH_TOKEN_BLACKLIST + refreshToken)) {
throw new BusinessException(BaseResponseCode.TOKEN_ERROR);
}
//从token中获取userId和userName
String 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、userName
String 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 {
@Override
public 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);
//把跟该角色关联的用户标记起来,需要刷新token
if (!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 {
@Autowired
private RedisService redisService;
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
//从用户的登录请求中获取accessToken
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);
}
//判断黑名单中有没有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 {
@Override
public 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--->标记该用户,重新签发token
List<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 {
//编辑用户
@Override
public 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 {
@Autowired
private RedisService redisService;
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
//从用户的登录请求中获取accessToken
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);
}
//判断黑名单中有没有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 {
@Override
public 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 {
@Autowired
private RedisService redisService;
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
//从用户的登录请求中获取accessToken
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);
}
//判断黑名单中有没有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;
}
}