• spring cloud + shiro 权限认证


    shiro是一个功能强大,简单的安全框架。对传统的单机系统支持较好,但与微服务整合后比较麻烦,网上资料比较散乱。本文主要介绍我做这一块儿的方法以及遇到的一些坑。

    思路

    微服务架构下的权限认证方案最简单的是分布式session,前端去登录认证模块请求登录,登录成功后shiro会生成session并将sessionId返回前端,session中包含用户基本信息及权限信息。shiro会将session放入redis中供其他服务查看。
    在这里插入图片描述

    实现

    基本思路有了,接下来是实现步骤,
    首先引入shiro相关依赖

    
        org.apache.shiro
        shiro-spring
        1.3.2
        
          
          
             shiro-quartz
                org.apache.shiro
             
          
    
    
        org.crazycake
        shiro-redis
        2.4.2.1-RELEASE
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    公共realm

    shiro的核心部分,包含认证和授权逻辑,此realm放在公共模块,便于其他模块授权。

    /**
     * 公共授权realm域
     */
    public class RealmCommon extends AuthorizingRealm {
    
        @Override
        public void setName(String name) {
            super.setName("RealmCommon");
        }
    
        /**
         * 只重写授权方法
         * @param principalCollection 身份信息集合
         * @return 授权信息
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            //1.获取认证的用户数据 | devtools冲突导致无法强转,需更改类加载器:resources/META-INF/spring-devtools.properties
            UserEntity user = (UserEntity)principalCollection.getPrimaryPrincipal();
            //2.构造认证数据
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    
            Set roles = user.getRoleList();
            if (CollectionUtils.isEmpty(roles)) {
                //用户没有角色
                throw new AuthorizationException();
            }
    
            for (RoleEntity role:roles){
                //添加角色信息
                info.addRole(role.getRoleName());
                //角色权限
                Set permissions = role.getPermissions();
                for (PermissionEntity permissionEntity : permissions) {
                    info.addStringPermission(permissionEntity.getPermissionname());
                }
            }
    
            return info;
        }
    
        /**
         * 认证方法在登录模块中补全
         * @param authenticationToken
         * @return
         * @throws AuthenticationException
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            return null;
        }
    }
    
    • 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

    这里有个坑,如果项目中引入了spring-boot-devtools会发生报错

    java.lang.ClassCastException: com.common.pojo.UserEntity cannot be cast to com.common.pojo.UserEntity
    
    • 1

    同类型无法强转。原因是shiro-redis使用的类加载器与其他类的类加载器不同,要解决这个问题有两种办法。
    1).直接移除devtools依赖
    2).让所有类的类加载器为同一个:在common下创建 resources/META-INF/spring-devtools.properties,修改热部署配置。

    restart.include.shiro-redis=/shiro-[\w-\.]+jar
    
    • 1

    session管理器

    自定义session管理器,指定sessionid生成方式

    /**
     * 自定义sessionManager
     */
    public class CommonWebSessionManager extends DefaultWebSessionManager {
        private static final String AUTHORIZATION = "Authorization";
        public CommonWebSessionManager(){
            super();
        }
    
        @Override
        protected Serializable getSessionId(ServletRequest request, ServletResponse response){
            String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
            if (StringUtils.isEmpty(id)){
                //如果没有携带id参数则按照父类的方式在cookie进行获取
                return super.getSessionId(request,response);
            }else {
                //如果请求头中有 authToken 则其值为sessionId
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,"header");
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID,id);
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID,Boolean.TRUE);
                return id;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    认证过滤器

    自定义认证过滤器,由于shiro本来是支持传统系统的,若未登录则会默认跳到内置的login.jsp,现在项目大多采用前后端分离模式,因此需要重写过滤器,返回未登录信息给前端,由前端实现跳转。即使后端指定到前端的登录页面,也会产生许多坑。

    /**
     * 自定义过滤器,处理shiro重定向问题
     * @author sunqiyan
     */
    public class CustomAuthenticationFilter extends FormAuthenticationFilter {
    
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
            return super.isAccessAllowed(request, response, mappedValue);
        }
    
        @Override
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
    
            Subject subject = SecurityUtils.getSubject();
            Object principal = subject.getPrincipal();
    
            if (ObjectUtils.isEmpty(principal)) {
                Map map = ResultUtil.genResult(ResultUtil.Status.NOT_LOGIN, "未登录");
                httpServletResponse.setCharacterEncoding("UTF-8");
                httpServletResponse.setContentType("application/json");
                httpServletResponse.getWriter().write(JSONObject.toJSONString(map, SerializerFeature.WriteMapNullValue));
            }
    
            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

    公共shiro配置

    接下来是公共的shiro配置类

    /**
     * shiro配置类
     */
    public class ShiroConfig {
    
        @Value("${spring.redis.host}")
        private String host;
        @Value("${spring.redis.port}")
        private int port;
        @Value("${spring.redis.password}")
        private String password;
    
        /**
         * 自定义realm
         * @return
         */
        @Bean
        public RealmCommon getRealm() {
            return new RealmCommon();
        }
    
        /**
         * 安全管理器
         * @param realm realm域
         * @return SecurityManager
         */
        @Bean
        public SecurityManager securityManager(RealmCommon realm) {
            //默认安全管理器
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realm);
            //将自定义的realm交给安全管理器管理
            securityManager.setRealm(realm);
            //自定义session管理器
            securityManager.setSessionManager(sessionManager());
            //自定义缓存实现
            securityManager.setCacheManager(cacheManager());
            return securityManager;
        }
    
        /**
         * shiro过滤器工厂
         * @param securityManager
         * @return
         */
        @Bean
        public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
            //shiro过滤器工厂
            ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
            //设置安全管理器
            filterFactory.setSecurityManager(securityManager);
            LinkedHashMap filterMap = new LinkedHashMap<>();
            //自定义认证过滤器
            filterMap.put("auth",new CustomAuthenticationFilter());
            filterFactory.setFilters(filterMap);
            //设置过滤链
            Map filterChainMap = new LinkedHashMap<>();
            //anon  游客即可访问
            filterChainMap.put("/css/**","anon");
            filterChainMap.put("/js/**","anon");
            filterChainMap.put("/image/**","anon");
            filterChainMap.put("favicon.ico","anon");
            //authc 需经过验证才能访问  auth自定义的过滤策略
            filterChainMap.put("/**","auth");
            filterFactory.setFilterChainDefinitionMap(filterChainMap);
    
            return filterFactory;
        }
    
        /**
         * 开启shiro aop注解支持
         * @param securityManager
         * @return
         */
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
            advisor.setSecurityManager(securityManager);
            return advisor;
        }
    
        @Bean
        public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
            DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
            advisorAutoProxyCreator.setProxyTargetClass(true);
            return advisorAutoProxyCreator;
        }
    
        /**
         * redis管理器
         * @return
         */
        public RedisManager redisManager(){
            RedisManager redisManager = new RedisManager();
            //设置redis ip 端口 密码
            redisManager.setHost(host);
            redisManager.setPort(port);
            redisManager.setPassword(password);
            return redisManager;
        }
    
        /**
         * 配置redis缓存管理器,用户、角色、权限实体类需序列化
         * @return
         */
        public RedisCacheManager cacheManager() {
            RedisCacheManager redisCacheManager = new RedisCacheManager();
            //设置redis管理器
            redisCacheManager.setRedisManager(redisManager());
            return redisCacheManager;
        }
    
        /**
         * redisSessiondao,实现redis的增删改查,交给shiro管理,shiro使用的是jedis
         * 也可自定义
         * @return
         */
        public RedisSessionDAO redisSessionDAO() {
            RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
            redisSessionDAO.setRedisManager(redisManager());
            return redisSessionDAO;
        }
    
        /**
         * session管理器
         * @return
         */
        public DefaultWebSessionManager sessionManager(){
           	CommonWebSessionManager sessionManager = new CommonWebSessionManager();
            sessionManager.setSessionDAO(redisSessionDAO());
            //设置session超时时间(单位毫秒),设置为-1000L永不过期
            sessionManager.setGlobalSessionTimeout(1000*60*30);
            //删除过期的session
            sessionManager.setDeleteInvalidSessions(true);
            //定时检查session
            sessionManager.setSessionValidationSchedulerEnabled(true);
            //可自定义sessionId
            //sessionManager.setSessionIdCookie(new SimpleCookie("fs_session"));
            return sessionManager;
        }
    
    }
    
    • 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
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141

    登录模块realm

    登录模块添加realm实现认证

    public class CustomRealm extends CommonRealm {
    
        @Autowired
        private UserService userService;
    
        @Override
        public void setName(String name) {
            super.setName("customRealm");
        }
    
        /**
         * 认证匹配用户是否存在
         * @param authenticationToken 		shiro subject的认证信息
         * @return 							认证成功
         * @throws AuthenticationException 	认证失败
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            //1.获取登录的token
            UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
    
            //2.获取用户名
            String username = token.getUsername();
            if (StringUtils.isBlank(username)) {
                //账户异常
                throw new AccountException("用户名不能为空");
            }
    
            //3.数据库查询用户
            UserEntity userEntity = this.userService.queryUserByName(username);
            if (userEntity == null) {
                throw new UnknownAccountException();
            }
            if (userEntity.getStatus()!=1) {
                //用户锁定
                throw new LockedAccountException();
            }
    
            return new SimpleAuthenticationInfo(userEntity,userEntity.getPassword(),this.getName());
        }
    
    }
    
    • 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

    匹配器

    由于项目中需要实现异地登录顶出功能,因此需要自定义匹配器实现认证逻辑。gai

    /**
     * 自定义验证器
     */
    @Component
    public class CustomCredentialsMatcher extends SimpleCredentialsMatcher {
    
        @Autowired
        private StringRedisTemplate redisTemplate;
        /**
        * 最大重试次数
        */
        @Value("#{'${cus.matcher.maxRetryNum:5}'}")
        private int maxRetryNum;
        /**
        *超时时间
        */
        @Value("#{'${cus.matcher.timeOutNum:20}'}")
        private int timeOutNum;
        /**
         * redis键
         */
        private static final String PREFIX = "LOGIN_ERROR:";
    
        @Override
        public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
            //获取token中的用户名密码
            UsernamePasswordToken token1 = (UsernamePasswordToken) token;
            String username = token1.getUsername();
            String password = new String(token1.getPassword());
            //获取凭证中的信息
            UserEntity user = (UserEntity)info.getPrincipals().getPrimaryPrincipal();
            String infoPassword = getCredentials(info).toString();
            //失败次数初始化
            AtomicInteger errorNum = new AtomicInteger(0);
            String o = redisTemplate.opsForValue().get(PREFIX + username);
            if (StringUtils.isNotBlank(o)){
                errorNum = new AtomicInteger(Integer.parseInt(o));
            }
            //失败次数超标
            if (errorNum.get() >=maxRetryNum) {
                throw new ExcessiveAttemptsException();
            }
            //密码校验
            boolean match = infoPassword.equals(password);
            if (match) {
                //登录成功,删除缓存
                redisTemplate.delete(PREFIX+username);
                DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
                DefaultWebSessionManager sessionManager = (DefaultWebSessionManager) securityManager.getSessionManager();
                //异地登录顶出
                //获取在线的session,判断登录用户是否已存在 | shiro分布式session弊端,影响性能
                Collection sessions = sessionManager.getSessionDAO().getActiveSessions();
                for (Session session:sessions) {
                    //强转为SimplePrincipalCollection
                    SimplePrincipalCollection attribute = (SimplePrincipalCollection)session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
                    if (ObjectUtils.isEmpty(attribute)) {
                        continue;
                    }
                    UserEntity userEntity = (UserEntity) attribute.getPrimaryPrincipal();
                    if (user.getUserId()==userEntity.getUserId()){
                    //session中存在用户则删除
                        sessionManager.getSessionDAO().delete(session);
                    }
                }
            }else {
                //设置超时时间,到时自动解锁
                redisTemplate.opsForValue().set(PREFIX+username,errorNum.incrementAndGet()+"",timeOutNum, TimeUnit.MINUTES);
                throw new IncorrectCredentialsException();
            }
    
            return match;
    
        }
    }
    
    • 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
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74

    此处实现挤出功能的方法是遍历session,用户少的情况下还行,用户多的话会影响性能,暂时没有想到解决办法。
    匹配器完成后需要在登录模块shiroConfig中设置:

    	/**
         * 自定义匹配器
         */
        @Bean(name = "credentialsMatcher")
        public CredentialsMatcher customCredentialsMatcher(){
            return new CustomCredentialsMatcher();
        }
    	/**
         * 自定义realm
         * @return
         */
        @Bean
        public CommonRealm getRealm() {
            CommonRealm customRealm = new CustomRealm();
            customRealm.setCredentialsMatcher(customCredentialsMatcher());
            return customRealm;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    其他模块只需要授权功能,每个模块继承公共模块的shiroConfig即可。

    feign拦截器

    shiro与springcloud整合还有个坑,就是使用feign远程调用时,feign默认会过滤到cookie,导致远程调用失败。失败返回值还不为空,而是正常的对象,对象里的属性都为空,直接跳过了判空操作,这就很麻烦。因此,需要自定义个拦截器,在远程调用时将cookie设置进请求里

    /**
     * 公共拦截器,处理feign远程调用过滤cookie问题
     */
    public class FeignCookieInterceptor implements RequestInterceptor {
    
        @Override
        public void apply(RequestTemplate requestTemplate) {
            if (null == getHttpServletRequest()){
                return;
            }
            requestTemplate.header("Cookie",getHttpServletRequest().getHeader("Cookie"));
        }
    
        private HttpServletRequest getHttpServletRequest(){
            try{
                return ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
            }catch (Exception e){
                return null;
            }
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    此拦截器也可放入公共模块中,在其他模块使用是注入即可。

    异常处理

    因为做登录功能时要求把登录失败的原因记录到日志中,因此需要捕获各种异常,需要捕获的异常有以下几类

    ExcessiveAttemptsException     操作频繁异常
    LockedAccountException     	   账户锁定异常
    IncorrectCredentialsException  密码错误异常
    UnknownAccountException		   未知账户异常
    UnknownSessionException       
    未知session异常,该异常本来是判断session是否存在,
    由于在做异地登录功能时直接把session删除了,因此账户被顶出会抛出该异常。
    也可以不把session删除,设置session立马过期,但是我没看到效果,只好暴力删除了。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    以上就是shiro+springcloud的整合过程,感觉shiro对微服务的支持不是太好。

    shiro可以使用注解控制权限,但是注解的value不支持动态获取,后期万一该角色或权限会比较麻烦,暂时没找到解决办法。
    shiro的注解也不支持加在类上,这也是比较坑的点。

    总的来说,shiro用起来还是比较简单的,不过个人认为分布式系统还是用其他方案好些,当然大佬也可以尝试修改源码o( ̄︶ ̄)o.

  • 相关阅读:
    AI系统源码ChatGPT网站源码+ai绘画系统/支持GPT4.0/支持Midjourney局部编辑重绘
    央媒发稿不能改?媒体发布新闻稿有哪些注意点
    编程(47)----------Spring AOP
    一台电脑生成两个ssh,绑定两个GitHub账号
    Java学习笔记5.4.3 Map接口 - Properties类
    C# OpenCvSharp Mat操作-创建Mat-zeros
    计算机毕业设计选题推荐-一周穿搭推荐微信小程序/安卓APP-项目实战
    编程面试_动态规划
    如何在海外通过A/B测试来优化应用
    知识经济时代的基石:知识协同
  • 原文地址:https://blog.csdn.net/m0_52789121/article/details/126496758