jwt无状态权限认证(pings-shiro-jwt)

    xiaoxiao2022-07-04  193

    单用户并发访问的问题

    当用户AccessToken失效,用户使用该失效的AccessToken同时发起多个请求,会产生多AccessToken和RefreshToken认证失败问题;

    多AccessToken的问题:

    如果多个请求分别请求成功,则每个请求都会生成一个新的AccessToken,但前面生成的AccessToken都会被后面生成的AccessToken覆盖,导致前面的AccessToken失效,客户端会很难判断下一个请求时应该使用哪个AccessToken。须在短时间内所有的用户请求,都返回同一个AccessToken;

    解决方案:10秒内的用户请求都返回第一个生成的AccessToken,这段逻辑必须要保证原子性; 单机版 synchronized (userName.intern()) { Boolean success = this.redisTemplate.opsForValue().setIfAbsent(accessTokenKey, accessToken, 10, TimeUnit.SECONDS); //**如果缓存新的accessToken成功,则缓存新的refreshToken if (success != null && success) { this.redisTemplate.opsForValue().set(refreshTokenKey, refreshToken, refreshTokenExpireTime, TimeUnit.MINUTES); } else { //**否则,返回缓存的accessToken accessToken = this.redisTemplate.opsForValue().get(accessTokenKey) + ""; } } 分布式版 String script = "if 1 == redis.call('setnx', KEYS[1], ARGV[1]) then " + " redis.call('expire', KEYS[1], ARGV[2]) " + " redis.call('set', KEYS[2], ARGV[3]) " + " redis.call('expire', KEYS[2], ARGV[4]) " + " return ARGV[1] " + "else " + " return redis.call('get', KEYS[1]) " + "end"; DefaultRedisScript<String> sc = new DefaultRedisScript<>(script); sc.setResultType(String.class); accessToken = redisTemplate.execute(sc, Arrays.asList(accessTokenKey, refreshTokenKey), accessToken, 10, refreshToken, refreshTokenExpireTime * 60);

    认证失败的问题:

    第一个请求认证失败,重新生成了RefreshToken,并把新RefreshToken保存到了redis中,第二个请求的RefreshToken还是上一次的,RefreshToken就会不相同,导致认证失败;

    解决方案:10秒内的验证成功过的AccessToken,直接返回验证成功,不需要重新验证; public boolean verify(String token) { String key = this.getKey(JwtUtil.getUserName(token)); //**使用访问令牌的md5,只是为了存储到redis时短一点而已 String tokenMd5 = DigestUtils.md5DigestAsHex(token.getBytes()); Boolean hasKey = this.redisTemplate.hasKey(key); if(hasKey == null || !hasKey) throw new TokenExpiredException("The Token not existed or expired."); long refreshToken = (long)this.redisTemplate.opsForValue().get(key); if(refreshToken != JwtUtil.getSignTimeMillis(token)){ //**访问令牌如果在10秒内验证过,则返回验证成功 Boolean flag = this.redisTemplate.hasKey(tokenMd5); if(flag != null && flag) return true; throw new TokenExpiredException("The Token has expired."); } try { return JwtUtil.verify(token, secret); } catch (TokenExpiredException e){ //**访问令牌过期,但刷新令牌有效时,缓存访问令牌10秒 logger.debug("Cache tokenMd5={}", tokenMd5); this.redisTemplate.opsForValue().set(tokenMd5, null, 10, TimeUnit.SECONDS); throw new TokenExpiredException("The access token has expired."); } }

    pings-shiro-jwt

    简介

    pings-shiro-jwt是基于jwt和shiro的无状态权限认证工具,可实现如下两种认证方式:

    access token无状态权限认证 原理优点 实现简单不外部依赖运行环境如果多个系统之间用户信息和配置的secret相同,某个系统签发的token即可访问所有其它的任意系统 问题 如果token过期时间太短,则每次到期后,都需要用户重新登录如果token过期时间太长,由于token签发后,在有效期内无法注销,存在安全隐患 结合refresh token和access token的无状态权限认证 原理优点 安全性好,可以像session一样管理用户如果多个系统之间用户信息和配置的secret相同,某个系统签发的token即可访问所有其它的任意系统 问题 依赖redis存储refresh token,实现多个系统之间的refresh token共享

    pings-shiro-jwt地址

    示例:dubbo微服务脚手架

    使用

    1.配置

    1).使用access token方式

    application.yml # 系统管理 config sys: jwt: secret: ==SFddfenfV2FuZzkyNjQ1NGRTQkFQSUpXVA== # 访问令牌过期时长(分钟),默认配置600分钟 access-token: expire-time: 300 ShiroConfig.java /** ********************************************************* ** @desc : Shiro配置 ** @author Pings ** @date 2019/1/23 ** @version v1.0 * ******************************************************* */ @Configuration public class ShiroConfig { //**访问令牌过期时间(分钟) @Value("${sys.jwt.access-token.expire-time}") private long accessTokenExpireTime; @Value("${sys.jwt.secret}") private String secret; @Reference(version = "${sys.service.version}") private UserService userService; @Bean public JwtVerifier verifier(RedisTemplate<String, Object> redisTemplate){ return new AccessTokenJwtVerifier(secret, accessTokenExpireTime); } @Bean public JwtRealm jwtRealm(JwtVerifier verifier){ return new JwtRealm(this.userService, verifier); } @Bean @Scope("prototype") public JwtFilter jwtFilter(JwtVerifier verifier){ return new JwtFilter(verifier); } @Bean("securityManager") public DefaultWebSecurityManager securityManager(JwtRealm jwtRealm) { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); //**使用自定义JwtRealm manager.setRealm(jwtRealm); DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); manager.setSubjectDAO(subjectDAO); return manager; } @Bean("shiroFilter") public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager, JwtFilter jwtFilter) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); //**添加自定义过滤器jwt Map<String, Filter> filterMap = new LinkedHashMap<>(); filterMap.put("jwt", jwtFilter); factoryBean.setFilters(filterMap); factoryBean.setSecurityManager(securityManager); //**自定义url规则 Map<String, String> filterRuleMap = new LinkedHashMap<>(); //不拦截请求swagger-ui页面请求 filterRuleMap.put("/webjars/**", "anon"); //jwt过滤器拦截请求 filterRuleMap.put("/**", "jwt"); factoryBean.setFilterChainDefinitionMap(filterRuleMap); return factoryBean; } public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator(); creator.setProxyTargetClass(true); return creator; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }

    2).结合refresh token和access token的方式

    application.yml # 系统管理 config sys: jwt: secret: ==SFddfenfV2FuZzkyNjQ1NGRTQkFQSUpXVA== # 访问令牌过期时长(分钟),默认配置5分钟 access-token: expire-time: 3 # 刷新令牌过期时长(分钟),默认配置60分钟 refresh-token: expire-time: 5 ShiroConfig.java /** ********************************************************* ** @desc : Shiro配置 ** @author Pings ** @date 2019/1/23 ** @version v1.0 * ******************************************************* */ @Configuration public class ShiroConfig { //**访问令牌过期时间(分钟) @Value("${sys.jwt.access-token.expire-time}") private long accessTokenExpireTime; //**刷新信息过期时间(分钟) @Value("${sys.jwt.refresh-token.expire-time}") private long refreshTokenExpireTime; //**密钥 @Value("${sys.jwt.secret}") private String secret; @Reference(version = "${sys.service.version}") private UserService userService; @Bean public JwtVerifier verifier(RedisTemplate<String, Object> redisTemplate){ return RefreshTokenJwtVerifier.Builder.newBuilder(redisTemplate) .accessTokenExpireTime(accessTokenExpireTime) .refreshTokenExpireTime(refreshTokenExpireTime) .secret(secret) .build(); } @Bean public JwtRealm jwtRealm(JwtVerifier verifier){ return new JwtRealm(this.userService, verifier); } @Bean @Scope("prototype") public JwtFilter jwtFilter(JwtVerifier verifier){ return new JwtFilter(verifier); } @Bean("securityManager") public DefaultWebSecurityManager securityManager(JwtRealm jwtRealm) { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); //**使用自定义JwtRealm manager.setRealm(jwtRealm); DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); manager.setSubjectDAO(subjectDAO); return manager; } @Bean("shiroFilter") public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager, JwtFilter jwtFilter) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); //**添加自定义过滤器jwt Map<String, Filter> filterMap = new LinkedHashMap<>(); filterMap.put("jwt", jwtFilter); factoryBean.setFilters(filterMap); factoryBean.setSecurityManager(securityManager); //**自定义url规则 Map<String, String> filterRuleMap = new LinkedHashMap<>(); //不拦截请求swagger-ui页面请求 filterRuleMap.put("/webjars/**", "anon"); //jwt过滤器拦截请求 filterRuleMap.put("/**", "jwt"); factoryBean.setFilterChainDefinitionMap(filterRuleMap); return factoryBean; } public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator(); creator.setProxyTargetClass(true); return creator; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }

    2.自定义shiro realm

    /** ********************************************************* ** @desc : jwt realm ** @author Pings ** @date 2019/5/10 ** @version v1.0 * ******************************************************* */ public class JwtRealm extends AbstractJwtRealm { protected UserService userService; public JwtRealm(UserService userService, JwtVerifier verifier){ super(verifier); this.userService = userService; } /**权限验证*/ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); String userName = verifier.getUserName(principals.toString()); //**获取用户 User user = this.userService.getByUserName(userName); //**用户角色 Set<String> roles = user.getRoles().stream().map(Role::getCode).collect(toSet()); authorizationInfo.addRoles(roles); //**用户权限 Set<String> rights = user.getRoles().stream().map(Role::getRights).flatMap(List::stream).map(Right::getCode).collect(toSet()); authorizationInfo.addStringPermissions(rights); return authorizationInfo; } /**登录验证*/ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException { String token = (String) auth.getCredentials(); //**获取用户名称 String userName = verifier.getUserName(token); //**用户名称为空 if (StringUtils.isBlank(userName)) { throw new UnknownAccountException("The account in Token is empty."); } //**获取用户 User user = this.userService.getByUserName(userName); if (user == null) { throw new UnknownAccountException("The account does not exist."); } //**登录认证 if (verifier.verify(token)) { return new SimpleAuthenticationInfo(token, token, "jwtRealm"); } throw new AuthenticationException("Username or password error."); } }

    3.login and logout

    /** ********************************************************* ** @desc : 登录 ** @author Pings ** @date 2019/1/22 ** @param userName 用户名称 ** @param password 用户密码 ** @return ApiResponse * ******************************************************* */ @ApiOperation(value="登录", notes="验证用户名和密码") @PostMapping(value = "/account") public ApiResponse account(String userName, String password, HttpServletResponse response){ if(StringUtils.isBlank(userName) || StringUtils.isBlank(password)) throw new UnauthorizedException("用户名/密码不能为空"); //**md5加密 password = DigestUtils.md5DigestAsHex(password.getBytes()); User user = this.userService.getByUserName(userName); if(user != null && user.getPassword().equals(password)) { JwtUtil.setHttpServletResponse(response, verifier.sign(userName)); //**用户权限 Set<String> rights = user.getRoles().stream().map(Role::getRights).flatMap(List::stream).map(Right::getCode).collect(toSet()); return new ApiResponse(200, "登录成功", rights); } else return new ApiResponse(500, "用户名/密码错误"); } /** ********************************************************* ** @desc : 退出登录 ** @author Pings ** @date 2019/3/26 ** @return ApiResponse * ******************************************************* */ @ApiOperation(value="退出登录", notes="退出登录") @GetMapping(value = "/logout") public ApiResponse logout(){ this.verifier.invalidateSign(this.getCurrentUserName()); //**退出登录 SecurityUtils.getSubject().logout(); return new ApiResponse(200, "退出登录成功"); }

    更新记录

    2019-05-20 搭建
    最新回复(0)