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


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

参考: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;
    }
}
  • 2
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明
Shiro整合 JWT(JSON Web Token),可以实现基于令牌的身份验证和授权机制。下面是一个简单的示例代码,演示了如何在 Shiro 中使用 JWT: 首先,需要创建一个 JWTRealm 类,继承自 Shiro 的 AuthenticatingRealm 类。在 JWTRealm 中,我们可以通过重写 doGetAuthenticationInfo 和 doGetAuthorizationInfo 方法来实现身份验证和授权逻辑。 ```java public class JWTRealm extends AuthenticatingRealm { @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { JWTToken jwtToken = (JWTToken) authenticationToken; String token = jwtToken.getToken(); // 解析 JWT Token,获取用户名 String username = JWTUtil.getUsername(token); // 根据用户名查询数据库或其他存储,获取用户信息 User user = userService.findByUsername(username); // 如果用户不存在,抛出 UnknownAccountException 异常 if (user == null) { throw new UnknownAccountException("用户不存在"); } // 验证 Token 是否有效 if (!JWTUtil.verify(token, username, user.getPassword())) { throw new AuthenticationException("Token 验证失败"); } // 构造 SimpleAuthenticationInfo 对象,并返回 return new SimpleAuthenticationInfo(user, token, getName()); } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { // 从 PrincipalCollection 中获取当前用户信息 User user = (User) principalCollection.getPrimaryPrincipal(); // 查询用户的角色和权限信息 Set<String> roles = userService.findRolesByUsername(user.getUsername()); Set<String> permissions = userService.findPermissionsByUsername(user.getUsername()); // 构造 SimpleAuthorizationInfo 对象,并设置角色和权限信息 SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); authorizationInfo.setRoles(roles); authorizationInfo.setStringPermissions(permissions); return authorizationInfo; } } ``` 然后,在 Shiro 的配置文件中,需要将 JWTRealm 注册为一个 Realm,并关闭默认的 Session 和 Cookie 管理: ```xml <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="jwtRealm" /> </bean> <bean id="jwtRealm" class="com.example.JWTRealm" /> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" /> <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager" /> <property name="arguments" ref="securityManager" /> </bean> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <!-- ... --> <property name="securityManager" ref="securityManager" /> <!-- ... --> </bean> ``` 最后,需要在登录接口处生成 JWT Token,并返回给前端: ```java @PostMapping("/login") public Map<String, Object> login(@RequestBody Map<String, String> params) { String username = params.get("username"); String password = params.get("password"); // 验证用户名和密码 if (userService.verify(username, password)) { // 生成 JWT Token String token = JWTUtil.sign(username, password); // 将 Token 返回给前端 Map<String, Object> resultMap = new HashMap<>(); resultMap.put("token", token); return resultMap; } else { throw new AuthenticationException("用户名或密码错误"); } } ``` 以上代码只是一个简单的示例,实际应用中可能还需要进行一些优化和增强。希望能对你的 Shiro 整合 JWT 有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我一直在流浪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值