• springboot环境下Shiro+Token+Redis安全认证方案


    springboot环境下Shiro+Token+Redis安全认证方案

    什么是shiro

    Apache Shiro 是一个强大灵活的开源安全框架,可以完全处理身份验证、授权、加密和会话管理。

    Realm是Shiro的核心组建,也一样是两步走,认证和授权,在Realm中的表现为以下两个方法。

    • 认证:doGetAuthenticationInfo,核心作用判断登录信息是否正确
    • 授权:doGetAuthorizationInfo,核心作用是获取用户的权限字符串,用于后续的判断

    Shiro过滤器

    当Shiro被运用到web项目时,Shiro会自动创建一些默认的过滤器对客户端请求进行过滤。以下是 Shiro 提供的部分过滤器:

    过滤器描述
    anon表示可以匿名使用
    authc表示需要认证(登录)才能使用
    authcBasic表示httpBasic认证
    perms当有多个参数时必须每个参数都通过才通过 perms[“user:add:”]
    portport[8081] 跳转到schemal://serverName:8081?queryString
    rest权限
    roles角色
    ssl表示安全的url请求
    user表示必须存在用户,当登入操作时不做检查

    springboot整合shiro

    添加maven依赖

    
    <dependency>
      <groupId>org.apache.shirogroupId>
      <artifactId>shiro-springartifactId>
      <version>1.4.0version>
    dependency>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    redis工具准备

    @Bean
        @DependsOn("ConfigUtil")
        public JedisClusterClient getClient() {
    
            ml.ytooo.redis.RedisProperties.expireSeconds = redisProperties.getExpireSeconds();
            ml.ytooo.redis.RedisProperties.clusterNodes = redisProperties.getClusterNodes();
            ml.ytooo.redis.RedisProperties.connectionTimeout = redisProperties.getConnectionTimeout();
            ml.ytooo.redis.RedisProperties.soTimeout = redisProperties.getSoTimeout();
            ml.ytooo.redis.RedisProperties.maxAttempts = redisProperties.getMaxAttempts();
    
            if (StringUtils.isNotBlank(redisProperties.password)) {
                ml.ytooo.redis.RedisProperties.password = redisProperties.password;
            }else {
                ml.ytooo.redis.RedisProperties.password = null;
            }
    
            return JedisClusterClient.getInstance();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    
    @Data
    @Component
    @ConfigurationProperties(prefix = "redis.cache")
    public class RedisProperties {
    
        private int expireSeconds;
        private String clusterNodes;
        private int  connectionTimeout;
        private String password;
        private int soTimeout;
        private int maxAttempts;
    }```
    
    ### shiro自定义认证token
    AuthenticationToken 用于收集用户提交的身份(如用户名)及凭据(如密码)。Shiro会调用CredentialsMatcher对象的doCredentialsMatch方法对AuthenticationInfo对象和AuthenticationToken进行匹配。匹配成功则表示主体(Subject)认证成功,否则表示认证失败。
    Shiro 仅提供了一个可以直接使用的 UsernamePasswordToken,用于实现基于用户名/密码主体(Subject)身份认证。UsernamePasswordToken实现了 RememberMeAuthenticationTokenHostAuthenticationToken,可以实现“记住我”及“主机验证”的支持。
    > 我们的业务逻辑是每次调用接口,不使用session存储登录状态,使用在head里面存token的方式,所以不使用session,并不需要用户密码认证。
    **自定义token如下:**
    ```java
    /**
     * Created by Youdmeng on 2020/6/24 0024.
     */
    public class YtoooToken implements AuthenticationToken {
        private String token;
        public YtoooToken(String token) {
            this.token = token;
        }
        @Override
        public Object getPrincipal() {
            return token;
        }
        @Override
        public Object getCredentials() {
            return token;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    shiro自定义Realm

    Realm是shiro的核心组件,主要处理两大功能:

    • 认证 我们接收filter传过来的token,并认证login操作的token
    • 授权 获取到登录用户信息,并取得用户的权限存入roles,以便后期对接口进行操作权限验证
    @Slf4j
    public class UserRealm extends AuthorizingRealm {
        @Autowired
        private JedisClusterClient jedis;
        /**
         * 大坑!,必须重写此方法,不然Shiro会报错
         */
        @Override
        public boolean supports(AuthenticationToken token) {
            return token instanceof YtoooToken;
        }
         /**
         * 授权
         *
         * @param principals
         * @return
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            log.info("Shiro权限配置");
            String token = principals.toString();
    
            UserDetailVO userDetailVO = JSON.parseObject(jedis.get(token), UserDetailVO.class);
    
            Set<String> roles = new HashSet<>();
            roles.add(userDetailVO.getAuthType() + "");
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            info.setRoles(roles);
            return info;
        }
        /**
         * 认证
         *
         * @param token
         * @return
         * @throws AuthenticationException
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            log.info("Shiror认证");
            YtoooToken usToken = (YtoooToken) token;
            //获取用户的输入的账号.
            String sid = (String) usToken.getCredentials();
            if (StringUtils.isBlank(sid)) {
                return null;
            }
            log.info("sid: " + sid);
            return new SimpleAccount(sid, sid, "userRealm");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    shiro自定义拦截器

    自定义shiro拦截器来控制指定请求的访问权限,并登录shiro以便认证

    我们自定义shiro拦截器主要使用其中的两个方法:

    • isAccessAllowed() 判断是否可以登录到系统
    • onAccessDenied() 当isAccessAllowed()返回false时,登录被拒绝,进入此接口进行异常处理
    /**
     * Created by Youdmeng on 2020/6/24 0024.
     */
    @Slf4j
    public class TokenFilter extends FormAuthenticationFilter {
        private String errorCode;
        private String errorMsg;
        private static JedisClusterClient jedis = JedisClusterClient.getInstance();
        /**
         * 如果在这里返回了false,请求onAccessDenied()
         */
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            String sid = httpServletRequest.getHeader("sid");
            if (StringUtils.isBlank(sid)) {
                this.errorCode = ResponseEnum.TOKEN_UNAVAILABLE.getCode();
                this.errorMsg = ResponseEnum.TOKEN_UNAVAILABLE.getMessage();
                return false;
            }
            log.info("sid: " + sid);
            UserDetailVO userInfo = null;
            try {
                userInfo = JSON.parseObject(jedis.get(sid), UserDetailVO.class);
            } catch (Exception e) {
                this.errorCode = ResponseEnum.TOKEN_EXPIRE.getCode();
                this.errorMsg = ResponseEnum.TOKEN_EXPIRE.getMessage();
                return false;
            }
            if (userInfo == null) {
                this.errorCode = ResponseEnum.TOKEN_EXPIRE.getCode();
                this.errorMsg = ResponseEnum.TOKEN_EXPIRE.getMessage();
                return false;
            }
            //刷新超时时间
            jedis.expire(sid, 30 * 60); //30分钟过期
            YtoooToken token = new YtoooToken(sid);
            // 提交给realm进行登入,如果错误他会抛出异常并被捕获
            getSubject(request, response).login(token);
            // 如果没有抛出异常则代表登入成功,返回true
            return true;
        }
        @Override
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) {
            ResponseMessage result = Result.error(this.errorCode,this.errorMsg);
            String reponseJson = (new Gson()).toJson(result);
            response.setContentType("application/json; charset=utf-8");
            response.setCharacterEncoding("utf-8");
            ServletOutputStream outputStream = null;
            try {
                outputStream = response.getOutputStream();
                outputStream.write(reponseJson.getBytes());
            } catch (IOException e) {
                log.error("权限校验异常",e);
            } finally {
                if (outputStream != null){
                    try {
                        outputStream.flush();
                        outputStream.close();
                    } catch (IOException e) {
                        log.error("权限校验,关闭连接异常",e);
                    }
                }
            }
            return false;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68

    配置ShiroConfig

    springboot中,组件通过@Bean的方式交由spring统一管理,在这里需要配置 securityManager,shiroFilter,AuthorizationAttributeSourceAdvisor

    注入realm

    
    @Bean
    public UserRealm userRealm() {
        UserRealm userRealm = new UserRealm();
        return userRealm;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    注入securityManager

    @Bean("securityManager")
    public DefaultWebSecurityManager getManager(UserRealm realm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        // 使用自己的realm
        manager.setRealm(realm);
        /*
          * 关闭shiro自带的session,详情见文档
          * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
          */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);
    
        return manager;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    注入shiroFilter

    此处将自定义过滤器添加到shiro中,并配置具体哪些路径,执行shiro的那些过滤规则

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
    
        // 添加自己的过滤器并且取名为token
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("token", new TokenFilter());
        factoryBean.setFilters(filterMap);
    
        factoryBean.setSecurityManager(securityManager);
        /*
          * 自定义url规则
          * http://shiro.apache.org/web.html#urls-
          */
        Map<String, String> filterRuleMap = new HashMap<>();
    
        //swagger
        filterRuleMap.put("/swagger-ui.html", "anon");
        filterRuleMap.put("/**/*.js", "anon");
        filterRuleMap.put("/**/*.png", "anon");
        filterRuleMap.put("/**/*.ico", "anon");
        filterRuleMap.put("/**/*.css", "anon");
        filterRuleMap.put("/**/ui/**", "anon");
        filterRuleMap.put("/**/swagger-resources/**", "anon");
        filterRuleMap.put("/**/api-docs/**", "anon");
        //swagger
        //登录
        filterRuleMap.put("/login/login", "anon");
        filterRuleMap.put("/login/verifyCode", "anon");
        // 所有请求通过我们自己的JWT Filter
        filterRuleMap.put("/**", "token");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return factoryBean;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    配置DefaultAdvisorAutoProxyCreator

    解决在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。

    @Bean
    public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
        /**
          * setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
          * 在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。
          * 加入这项配置能解决这个bug
          */
        defaultAdvisorAutoProxyCreator.setUsePrefix(true);
        return defaultAdvisorAutoProxyCreator;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    配置AuthorizationAttributeSourceAdvisor 使doGetAuthorizationInfo()Shiro权限配置生效

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在接口中控制权限
    使用RequiresRoles注解来配置该接口需要的权限
    当配置logical=Logical.OR时,登录这配置的权限在1,2,3中任意一个,即可以成功访问接口

    @ApiOperation("任务调度")
    @PostMapping("/dispatch")
    @RequiresRoles(value = { "1", "2", "3" }, logical = Logical.OR)
    public ResponseMessage dispatch(@RequestBody @Valid DispatchVO dispatchVO) {
    
        log.info("任务调度开始 入参:" + JSON.toJSONString(dispatchVO));
        try {
            service.dispatch(dispatchVO);
            return Result.success(ResponseEnum.SUCCESS.getCode(), ResponseEnum.SUCCESS.getMessage());
        } catch (RuntimeException e) {
            log.error("任务调度失败", e);
            return Result.error(ResponseEnum.ERROR.getCode(), e.getMessage());
        } catch (Exception e) {
            log.error("任务调度失败", e);
            return Result.error(ResponseEnum.ERROR.getCode(), ResponseEnum.ERROR.getMessage());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    同一异常处理

    配置全局异常处理

    @ControllerAdvice
    @Order(value=1)
    public class ShiroExceptionAdvice {
    
        private static final Logger logger = LoggerFactory.getLogger(ShiroExceptionAdvice.class);
        @ResponseStatus(HttpStatus.UNAUTHORIZED)
        @ExceptionHandler({AuthenticationException.class, UnknownAccountException.class,
                UnauthenticatedException.class, IncorrectCredentialsException.class})
        @ResponseBody
        public ResponseMessage unauthorized(Exception exception) {
            logger.warn(exception.getMessage(), exception);
            logger.info("catch UnknownAccountException");
            return Result.error(ResponseEnum.NOT_AUTHORIZED.getCode(), ResponseEnum.NOT_AUTHORIZED.getMessage());
        }
    
        @ResponseStatus(HttpStatus.UNAUTHORIZED)
        @ExceptionHandler(UnauthorizedException.class)
        @ResponseBody
        public ResponseMessage unauthorized1(UnauthorizedException exception) {
            logger.warn(exception.getMessage(), exception);
            return Result.error(ResponseEnum.NOT_AUTHORIZED.getCode(), ResponseEnum.NOT_AUTHORIZED.getMessage());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
  • 相关阅读:
    React state(及组件) 的保留与重置
    linux读写锁
    数字孪生技术栈的应用场景的优点
    EXCEL 打印设置公共表头
    树莓派镜像安装 + 设置 + 镜像批量化操作 - 自动化烧写工具 (四)
    数据结构之单向链表
    C++ - 右值引用 和 移动拷贝
    教程六 在Go中使用Energy创建跨平台GUI - 应用下载事件
    高通平台(Qualcomm) Android 10 user版本默认打开adb 调试小结
    Selenium中WebDriver最新Chrome驱动安装教程
  • 原文地址:https://blog.csdn.net/qq_38785977/article/details/126014978