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

单用户并发访问的问题

当用户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
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值