精华内容
下载资源
问答
  • 文章目录Shiro管理用户认证时jwt续签和注销的问题1. 场景一:token的注销问题(黑名单)2. 场景二:token的续签问题3. 项目中的实现3.1 封装JWT工具类3.2 配置Shiro的自定义认证类3.3 登录和退出登录(token注销)...


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

    参考: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;
        }
    }
    
    展开全文
  • <p>I have an issue with logging out using <a href="https://github.com/tymondesigns/jwt-auth" rel="nofollow noreferrer">JWT package</a>. On Angular side I am removing the token from local storage and ...
  • 不好意思~ jwt是不支持主动使用户token过期的。要知道用户登录身份的校验,基本就两种:①客户端存储一个随机串,发送请求时携带该串。服务器接收到之后拿着这个串去存储中做比对,找到对应的数据。②将所有数据都...

    不好意思~ jwt是不支持主动使用户token过期的。

    要知道用户登录身份的校验,基本就两种:

    ①客户端存储一个随机串,发送请求时携带该串。服务器接收到之后拿着这个串去存储中做比对,找到对应的数据。

    ②将所有数据都存储到客户端。这样就有会安全问题,因为服务器无法确定那些数据是有效的,那些数据是伪造的。所以就引入了签名的概念。用算法的方式来保证数据的可信性。

    早期的网站基本就一个主站,也没有分布式的部署。所以多数都采用第一种方式。

    但随着访问量的增加,开始做分布式,就涉及到了session共享的问题。比如①中的存储,使用redis来达到共享数据的效果。但是这样会在服务器中存储大量的数据。

    然后呢,就出来另外一种。不在服务器存储数据。其中一个就是jwt。

    jwt在生成时,可以设置有效期。理论上 一个用户可以产生无数个jwt,且jwt的有效期独立。

    如果你想将一个还在有效期内的jwt置为无效,那就必须要在服务器存储数据,这就违背了他的设计原则~

    其实。他的退出,就是客户端主动将jwt扔掉(假设不会被其他人捡到)~~~~~ 那么这个jwt不就不存在了吗~ 这不就完成注销功能了吗~

    展开全文
  • I am using jwt plugin and strategy in hapijs.I am able to create jwt token while login user and authenticate other API using the same token through 'jwt' strategy.I am setting the token in request.sta...

    I am using jwt plugin and strategy in hapijs.

    I am able to create jwt token while login user and authenticate other API using the same token through 'jwt' strategy.

    I am setting the token in request.state.USER_SESSION as a cookie where USER_SESSION is a token name. Also, I am not saving these token in the database.

    But how can I destroy jwt token at the time of logout?

    Please suggest a way.

    解决方案

    The JWT is stored on browser, so remove the token deleting the cookie at client side

    If you need also to invalidate the token from server side before its expiration time, for example account deleted/blocked/suspended, password changed, permissions changed, user logged out by admin, take a look at Invalidating JSON Web Tokens for some commons techniques like creating a blacklist or rotating tokens

    展开全文
  • JWT简介 :json Web Token(缩写JWT)是目前最流行的跨域认证解决方案session登录的认证方案是看,用户从客户端传递用户名和密码登录信息,服务端认证后将信息储存在session中,将session_id...JWT的解决方案是,将认...

    JWT简介 :

    json Web Token(缩写JWT)是目前最流行的跨域认证解决方案

    session登录的认证方案是看,用户从客户端传递用户名和密码登录信息,服务端认证后将信息储存在session中,将session_id放入cookie中,以后访问其他页面,服务器都会带着cookie,服务端会自动从cookie中获取session_id,在从session中获取认证信息。

    JWT的解决方案是,将认证信息返回个客户端,储存在客户端,下次访问其他页面,需要从客户端传递认证信息回服务器端。

    那么有人会问,将认证信息个客户端了,那么有坏人恶意修改,然后传递个服务器端怎么办呢,这个问题下面说JWT原理就会说到

    JWT原理

    JWT原理就是,服务器认证后,生成一个json格式的对象 ,发送个客户端,

    {

    "用户名": "admin",

    "角色": "超级管理员",

    "到期时间": "2019-07-13 00:00:00"

    }

    以后,客户端域服务器通信的时候,都要发回这个json对象,服务器完全靠这个对象认定用户身份,(但肯定不会像上面那样,那么简单的发送一个对象)这样的话,session中就没有数据了,后面更容易实现扩展

    JWT的数据结构

    JWT分为三个部分,header(头部) payload (负载) signature (签名)

    一个完整的JWT数据是这样的

    eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjNmMmc1N2E5MmFhIn0.

    eyJpYXQiOjE1NjI4MzM0MDgsImlzcyI6Imh0dHA6XC9cL3d3dy5weWcuY29tIiwiYXVkIjoiaHR0cDpcL1wvd3d3LnB5Zy5jb20iLCJuYmYiOjE1NjI4MzM0MDcsImV4cCI6MTU2MjkxOTgwOCwianRpIjoiM2YyZzU3YTkyYWEiLCJ1c2VyX2lkIjoxfQ.

    NFq1qQ-Z5c4pwit8ZkyWEwX6SBXmnHJcc6ZDgSD5nhU

    中间是有三个点的,分别就是 头部 负载 和签名 (点在每一行的最后)

    头部 是一个json对象 作用是描述JWT元数据,一般是这样的

    {

    "alg": "HS256", //表示签名的算法默认是 HMAC SHA256(写成 HS256)

    "typ": "JWT" //表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT

    }

    最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。

    负载 也是一个 JSON 对象 ,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用

    除了官方字段,你还可以在这个部分定义私有字段

    这个 JSON 对象也要使用 Base64URL 算法转成字符串(防止除了用户的人看见嘛)。

    注意:JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。(虽然加密了,防止解密的坏人解密后修改在加密)

    签名 是对前两部分的签名(可以理解成在加密一份),防止数据篡改。

    首先,需要指定一个密钥(自己设置),这个密钥只有服务器才知道,不能泄露给用户。

    使用 Header 里面指定的签名算法(默认是 HMAC SHA256)产生签名

    如:HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret) //使用Header 里面指定的签名算法 将头部和负载部用逗号拼接起来,在加上自己设置的秘钥

    那么签名就出来的。

    签名出来后,现在有了 头部的字符串,和负载的字符串 ,还有签名的,在将这三个字符串用. 拼接出来,就可以将这个拼接好的字符串,返回给客户端的

    JWT数据就返回得了客户端,需要注意的是,头部和负载部,是用base64URL转成字符串的,签名是用头部指定的算法转成字符串的 不用弄混掉

    JWT 的使用方式

    客户端,接受到了服务器返回的jwt,可以储存到cookie中,也可以储存在 localStorage。

    此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。

    一旦我们使用的JWT,JWT的类别人封装好的,会自动将生成的token放入响应头中去,然后再次去访问页面的时候会带着响应回来token去访问页面

    JWT在请求头中发送,如:会多了个请求头

    Authorization: Bearer

    然后,在服务器中,需要验证这个token,是否有效

    下面是实际应用在tp框架中

    当然需要通过composer安装JWT类

    composer require lcobucci/jwt 3.3 //安装jwt

    定义一个专门生成,和验证JWT的token

    namespace tools\jwt;

    use Lcobucci\JWT\Builder; //生成token类use Lcobucci\JWT\Parser;//解析tokenuse Lcobucci\JWT\Signer\Hmac\Sha256; //签名加密类use Lcobucci\JWT\ValidationData;//检测token

    class Token //在扩展中加入一个叫工具和jwt的文件夹,里面写入一个叫token的类{

    private static $_config = [ //生成token的基本参数 'audience' => 'http://www.mypyg.com',//接收人 'id' => 'zyt6b',//token的唯一www.pyg.com标识,这里只是一个简单示例 'sign' => 'zyt6b',//签名密钥 'issuer' => 'http://adminapi.pyg.com',//签发人 'expire' => 3600*24 //有效期 ];

    //生成token public static function getToken($user_id){//传来的用户id生成token

    //签名对象 $signer = new Sha256(); //获取签名对象数据 //获取当前时间戳 $time = time();

    //设置签发人、接收人、唯一标识、签发时间、立即生效、过期时间、用户id、签名 $token = (new Builder())->issuedBy(self::$_config['issuer'])//签发人 ->canOnlyBeUsedBy(self::$_config['audience'])//接收人 ->identifiedBy(self::$_config['id'], true)//标识id ->issuedAt($time)//签发时间 ->canOnlyBeUsedAfter($time-1) //生效时间 ->expiresAt($time + self::$_config['expire'])//过期时间 ->with('user_id', $user_id) //用户id加入token ->sign($signer, self::$_config['sign']) //签名秘钥 ->getToken(); //用上面信息生成token return (string)$token; //强制返回字符串token }

    //从请求信息中获取token令牌 public static function getRequestToken()

    {//在public里重写apache ,因为不处理,php中接收不到HTTP_AUTHORAZATION字段信息 //在public/.htaccess 中添加以下两行代码 //RewriteCond %{HTTP:Authorization} ^(.+)$ //RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

    if (empty($_SERVER['HTTP_AUTHORIZATION'])) { //判断有没有AUTHORIZATION字段的信息 return false; //server可以获得请求头信息 }

    //如果有AUTHORIZATION:就写入变量 $header = $_SERVER['HTTP_AUTHORIZATION'];

    $method = 'bearer';

    //去除token中可能存在的bearer标识 return trim(str_ireplace($method, '', $header));//忽略大小写,去除字符串首尾的空白字符 //将 $header 中的所有$method替换成''; //获取的时候去除多余的东西,只留一个token }

    //从token中获取用户id (包含token的校验) public static function getUserId($token = null)

    {

    $user_id = null;

    //判断$token是否是空,如果是空的话就从请求信息中获取token,否则就用自己 $token = empty($token)?self::getRequestToken():$token;

    //判断$token是否是空 return $token;die;

    if (!empty($token)) { //如果token不是空 //为了注销token 加以下if判断代码 $delete_token = cache('delete_token') ?: [];

    //从缓存中取出被标注被删除的token if(in_array($token, $delete_token)){//判断当前的token是否缓存中的删除中的token //token已被删除(注销) return $user_id;//如果在里面,返回用户id }

    $token = (new Parser())->parse((string) $token);//解析token $data = new ValidationData();//验证token的类 $data->setIssuer(self::$_config['issuer']);//验证的签发人 $data->setAudience(self::$_config['audience']);//验证的接收人 $data->setId(self::$_config['id']);//验证token标识

    if (!$token->validate($data)) { //如果验证失败 return $user_id; //返回用户id }

    $signer = new Sha256(); //验证签名秘钥的类 if (!$token->verify($signer, self::$_config['sign'])) {//验证签名秘钥 //签名验证失败 //token通过秘钥和加密的类的 看看能不能解析出来东西 return $user_id;

    }

    //从token中获取用户id $user_id = $token->getClaim('user_id');

    }

    return $user_id; //返回用户id 这里应该有值 }

    }

    展开全文
  • 什么是 JWT概念JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。JWT 原理JWT 组成JWT 由三部分组成:Header,Payload,Signature 三个部分组成,并且最后由.拼接而成。HeaderHeader 部分是一个 JSON 对象,...
  • 您可以简单地在客户端销毁会话并在后端使令牌无效...如果您无效,则需要在收到Laravel的回复后销毁令牌.JWTAuth::invalidate(JWTAuth::getToken())):然后在角度方面function logout(){UserService.logout().$promise...
  • 然而,如果使用JWT实现,用户主动注销、强制登出(禁止登陆)、忘记密码、修改密码,JWT续签等方面的需求,就会让人头疼。按照网上的一些讨论,我概括如下:1、将每一个签发的JWT保存在REDIS上,当出现上述的需求时,就...
  • JWT(JSON Web Token)是目前最流行的认证方案之一。博客园、各种技术公众号隔三差五就会推一篇JWT相关的文章,真的多如牛毛。但我对JWT有点困惑,今天写出来跟大家探讨探讨,不要喷哈。JWT原理本文默认读者已经对...
  • 然而,如果使用JWT实现,用户主动注销、强制登出(禁止登陆)、忘记密码、修改密码,JWT续签等方面的需求,就会让人头疼。按照网上的一些讨论,我概括如下:1、将每一个签发的JWT保存在REDIS上,当出现上述的需求时,就...
  • token在客户端与服务器端的交互流程Token的优点和思考参考代码:核心代码使用参考,不是全部代码JWT token的组成头部(Header),格式如下:{“typ”: “JWT”,“alg”: “HS256”}由上可知,该token使用HS256加密算法...

空空如也

空空如也

1 2 3 4 5 ... 11
收藏数 214
精华内容 85
关键字:

jwt注销