shiro扩展功能学习篇03(动态权限配置,限制登录,多Realm)

4 篇文章 0 订阅
3 篇文章 0 订阅

Shiro-SpringBoot核心功能扩展

realm缓存

  • 缓存Realm作用主要为了减少 认证和授权时,频繁的访问数据库。

  • 之前导入了 Shiro-Redis 桥接的jar包,已经包含了Redis的缓存机制。

  • <dependency>
      <groupId>org.crazycake</groupId>
      <artifactId>shiro-redis-spring-boot-starter</artifactId>
      <version>3.3.1</version>
    </dependency>
    
  • 配置properties文件:

  • # shiro
    shiro-redis.enabled=true
    #shiro-redis.redis-manager.deploy-mode=cluster
    #shiro-redis.redis-manager.host=redis-svc.lifekh-tool-sit.svc.cluster.local:6379
    
    # 开启Shiro-Redis的相关配置
    shiro-redis.enabled=true
    # redis连接配置
    #shiro-redis.redis-manager.deploy-mode=cluster
    #shiro-redis.redis-manager.host=redis-svc.lifekh-tool-sit.svc.cluster.local:6379
    shiro-redis.redis-manager.timeout=5000
    shiro-redis.redis-manager.max-attempts=3
    # shiro-session-dao分布式会话配置
    shiro-redis.session-dao.key-prefix=test:shiro:session:
    shiro-redis.session-dao.expire=3600
    # shiro-realm缓存配置
    shiro-redis.cache-manager.key-prefix=test:shiro:cache:
    shiro-redis.cache-manager.expire=3600
    #spring的session会话存储技术,因为shiro用到了cookie技术,使spring的sesison也分布式会话管理到redis中
    spring.session.store-type=redis
    #ON_SAVE:当web请求返回时更新redis缓存,IMMEDIATE:在请求开始时就实时同步缓存
    spring.session.redis.flush-mode=on_save
    
  • Shiro-Redis jar包底层Redis相关配置类:ShiroRedisAutoConfiguration

  • # Configuration配置文件 Realm设置缓存
    // Realm配置缓存
    // Authorization授权是默认开启 , Authentication认证是默认关闭的
    userRealm.setAuthenticationCachingEnabled(true);
    //userRealm.setAuthenticationCacheName("test:AuthenticationCache");
    userRealm.setAuthorizationCachingEnabled(true);
    //userRealm.setAuthorizationCacheName("AuthorizationCache");
    

分布式会话SessionManager

  • 当项目采用了多服务器分布式部署时,会存在会话不同步问题:
  • 在这里插入图片描述

解决方案

  • 使用redis作为Shiro的 session 共享存储媒介。多服务器共用一个Redis,就能实现多服务器之间session的共享.

  • 在这里插入图片描述

  • Shiro的sesison 和 Web的Session 不是一回事,这里要区分开。Shiro的session与客户端的链接通过Cookie中的 JSESSIONID=df54c19b-5aed-4276-bdb5-ed8de930e55f 键值。然后通过id去Redis中找到对应的缓存授权信息。

手动存储

  • 通过继承接口,来自定义缓存Session的方法:

  • // 继承 AbstractSessionDAO 接口 , 手动实现方法,来存储到redis中
    
    Serializable create(Session session);用于创建一个session
    
    Session readSession(Serializable sessionId) throws UnknownSessionException;用于读取一个session
    
    void update(Session session) throws UnknownSessionException; 用于更新一个session
    
    void delete(Session session);用于删除一个session
    
    Collection<Session> getActiveSessions();用于获取所有存活session
    

第三方jar依赖功能自动缓存

  • 因为使用了 Shiro-Redis 的依赖,里面已经包含了 RedisSessionDao 的实现方法。

  • // 直接spring引入就可以使用,相关的配置都在properties中
    @Autowired
    private RedisSessionDAO redisSessionDAO;
    

修改SessionId获取方式

  • 一般客户端与服务器的Shiro链接,通过Cookie中的JSESSIONID键值对,但是有时不想通过cookie来获取这个SessionId,我们可以通过自定义的方式。

  • Shiro默认获取客户端请求SessionId的方式:

  • 在这里插入图片描述

  • 有时可能想通过请求头header中的 token 来实现shiro的权限校验。

  • // 继承WebSessionManager实现类
    @Slf4j
    public class CustomWebSessionManager extends DefaultWebSessionManager {
    
        @Override
        protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
            log.info("获取SessionId");
            try {
                String token = ((HttpServletRequest) request).getHeader("token");
                if (StringUtils.isEmpty(token)) {
                    // 走父类方法,从cookie中获取
                    // return super.getSessionId(request, response);
                	// 直接返回null,找不到session就会让去登录
                    return null;
                }
                // 父类方法的request传递
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
                request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());
                // 返回获取到的SessionId
                return token;
            }catch (Exception e) {
                log.error("获取SessionId错误" , e);
                return super.getSessionId(request, response);
            }
        }
    }
    

限制密码重试次数

  • 通过密码失败次数来限制用户登录操作。

  • 保证原子性:

    • 单系统: JVM缓存的AtomicLong计数器
    • 集群分布式系统:Redis缓存的RedissionClient提供的RAtomicLong计数器
  • 实现步骤:
    1. 获取系统中是否有登录次数的记录,缓存结构: 用户名-次数
    2. 如果失败了就记录一次次数 , 并设置过期时间
    3. 如果请求发现已经达到限制次数,就驳回请求
    4. 如果用户登录成功,就清空缓存次数。
    // 实现代码 , 在Realm中的Authenticating认证登录中.
    

账号并发控制为唯一登录

业务逻辑

  • 很多业务场景下,一个用户名只允许登录一台设备,或者限制只能N台设备登录。

  • 如果当前登录后,就踢出前面登录的设备。

  • 自定义过滤器继承 AccessControlFilter 实现类

  • 使用Redis队列控制账号在线数量

  • // 实现步骤
    1. 只针对登录用户做处理,首先判断用户是否登录
    2. 使用RedissionClient创建队列
    3. 判断当前SessionId是否存在用户的队列中 key:用户名 value:集合SessionId
    4. 不存在则放在队列最后一位,
    5. 判断队列的在线设备数是否超过限制
    6. 超过:从队列头部拿到第一个SessionId,通过SessionManager剔除此Session会话,否则就记录完成流程.
    

代码实现:

  • 代码注意事项:

    • 关于用户登录限制的方法可以有多种写法:
    • UserFilter 过滤器中
    • HashedCredentialsMatcher 密码比较中
    • Realm 认证方法中等等…
  • 要处理记录过期的问题。找到合适的Redis存储方式,Redis中的List集合不能单独设置一个key的过期时间,只能设置整个List的过期时间,就会出现Session和登录限制的缓存过期不同步问题.

  • 写在Filter过滤器中:

  • @Slf4j
    public class CustomLoginNumFilter extends UserFilter {
    
        private RedisTemplate redisTemplate;
    
        private RedisSessionDAO redisSessionDAO;
    
        private SessionManager sessionManager;
    
        private static final String ACCOUNT_LIMIT = "account_limit:";
    
        public CustomLoginNumFilter(RedisTemplate redisTemplate, RedisSessionDAO redisSessionDAO, SessionManager sessionManager) {
            this.redisTemplate = redisTemplate;
            this.redisSessionDAO = redisSessionDAO;
            this.sessionManager = sessionManager;
        }
    
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
            // 直接返回没有登录,走onAccessDenied方法
            return false;
        }
    
        @Override
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
            log.info("校验用户登录设备数量...");
            // 校验登录数 , 通过Request获取登录信息
            String jsonBody = ((HttpServletRequest) request).getReader().lines().collect(Collectors.joining(System.lineSeparator()));
            // 将bodyJson传递下去
            request.setAttribute("userJson" , jsonBody);
            System.out.println(jsonBody);
            User user = JSONObject.parseObject(jsonBody , User.class);
            // Redis缓存key
            String key = ACCOUNT_LIMIT + user.getLoginName();
            Set<String> keys = redisTemplate.keys(key + ":*");
            if (Objects.nonNull(keys) && keys.size() >= 1) {
                // 获取第一个设备的sessionId
                // 冒泡排序得出 记录时间最早的
                String lastKey = sortLastTimeKey(keys);
                if (StringUtils.isEmpty(lastKey)) {
                    return true;
                }
                String lastSessionId = (String) redisTemplate.opsForValue().get(lastKey);
                log.info("之前登录的SessionId===>{}" , lastSessionId);
                // 限制不让再次登录方法
    //            HttpServletResponse httpServletResponse = (HttpServletResponse)response;
    //            Result<Void> result = Result.error("500", "已经有设备登录了账号,请检查后重试.");
    //            httpServletResponse.setHeader("Content-Type" , "application/json;charset=utf-8");
    //            httpServletResponse.getWriter().write(JSONObject.toJSONString(result));
    //            return false;
    
                // 将之前的登录剔除
                Session lastSession = sessionManager.getSession(new DefaultSessionKey(lastSessionId));
                if (Objects.nonNull(lastSession)) {
                    redisSessionDAO.delete(lastSession);
                }
                // 清除登录次数队列
                Boolean flag = redisTemplate.delete(lastKey);
                if (Boolean.FALSE.equals(flag)) {
                    log.error("删除用户登录次数限制redis失败,key===>{}" , lastKey);
                }
            }
            return true;
        }
    
        /**
        *  冒泡排序得出最早时间记录的key
        * @param keys 1
        * @return java.lang.String
        */
        private static String sortLastTimeKey(Set<String> keys) {
            if (Objects.isNull(keys)) {
                return null;
            }
            Optional<String> first = keys.stream().sorted().findFirst();
            return first.isPresent() ? first.get() : null;
        }
    
        public static void main(String[] args) {
            HashSet<String> set = new HashSet<>();
            set.add("123");
            set.add("456");
            set.add("10");
            String s = sortLastTimeKey(set);
            System.out.println(s);
        }
    }
    
    
  • 配置给login登录路径:listMap.put("/user/login" , “customLoginNum,anon”);

  • 登录方法记录设备登录次数到Redis中:

  • @Service
    public class LoginServiceImpl implements LoginService {
    
        private static final String ACCOUNT_LIMIT = "account_limit:";
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        @Override
        public String login(User user) {
            UserLoginToken loginToken = new UserLoginToken(user.getLoginName(), user.getPassWord());
            try {
                SecurityUtils.getSubject().login(loginToken);
                // 获取SessionId
                Serializable sessionId = SecurityUtils.getSubject().getSession().getId();
                // 记录登录次数到Redis中
                redisTemplate.opsForValue().set(ACCOUNT_LIMIT + user.getLoginName() +":"+System.currentTimeMillis() , sessionId , 3600 , TimeUnit.SECONDS);
                return (String) sessionId;
            } catch (IncorrectCredentialsException e) {
                e.printStackTrace();
                return "passWord Error";
            } catch (AuthenticationException e) {
                e.printStackTrace();
                return "loginError";
            }
        }
    }
    
    

执行顺序

  • 先走获取SessionId的方法判断是否有Session,在走 CustomLoginNumFilter 过滤器,进行判断用户登录设备数,最后通过再走 LoginServiceImpl 登录方法记录登录次数。
  • 在这里插入图片描述

出现问题

  • 这里有一个redis过期时间不同步的问题,记录登录信息的key一小时过期,session会话的key也是一小时过期,但是用户的每次操作都会刷新 session的key的过期时间,而记录登录信息的key没有更新,就会导致不同步问题。

  • 代码实现,自定义一个类SessionManager继承 DefaultWebSessionManager , 每次当更新Session会话过期时间时,同步更新一下记录登录信息的redis-key过期时间

  • @Override
        protected void onChange(Session session) {
            super.onChange(session);
            // 更新用户登录记录的key的缓存时间
            String sessionId = (String) session.getId();
            // 获取key
            SimplePrincipalCollection simplePrincipalCollection = (SimplePrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
            if (Objects.isNull(simplePrincipalCollection)) {return;}
            User user = (User) simplePrincipalCollection.getPrimaryPrincipal();
            StringRedisTemplate redisTemplate = ApplicationBeanUtils.getBean(StringRedisTemplate.class);
            String prefix = ShiroPathCheckConstant.ACCOUNT_LIMIT + user.getLoginName() + ":" + sessionId;
            Set<String> keys = redisTemplate.keys(prefix + ":*");
            // 更新时间
            keys.forEach(key -> {
                redisTemplate.opsForValue().set(key , sessionId , 3600 , TimeUnit.SECONDS);
            });
        }
    
    

Shiro实现多Realm

  • 一个管理系统中可能会出现,不同的登录流程。比如user用户,admin管理员 等Realm实现的过滤鉴权形式不同。
  • 就要实现不同的登录主体类型,对应不同的Realm进行鉴权流程。
  • 代码实现

SecurityManger代码

  • @Bean
        public SessionsSecurityManager securityManager() {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setAuthenticator(new MultiRealmAuthenticator());
            securityManager.setAuthorizer(new MultiRealmAuthorizer());
            List<Realm> realms = new ArrayList<>();
            // 添加多个Realm
            realms.add(userRealm());
            realms.add(merchantRealm());
    //        securityManager.setRealm(userRealm());
            securityManager.setRealms(realms);
            securityManager.setSessionManager(getDefaultWebSessionManager());// session管理
            securityManager.setCacheManager(redisCacheManager);
            return securityManager;
        }
    
    

RealmAuthenticator认证

  • @Slf4j
    @Component
    public class MultiRealmAuthenticator extends ModularRealmAuthenticator {
    
        @Override
        protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
                throws AuthenticationException {
            log.info("MultiRealmAuthenticator:method doAuthenticate() execute ");
            assertRealmsConfigured();
            RealmTypeEnum realmTypeEnum = RealmTypeEnum.GAME_MANAGER;
            if(authenticationToken instanceof MultiRealmToken){
                realmTypeEnum = ((MultiRealmToken) authenticationToken).getTypeEnum();
            }
            // 所有Realm, 循环获取对应枚举类型的Realm
            Collection<Realm> realms = getRealms();
            List<Realm> typeRealms = new ArrayList<>();
            for (Realm realm : realms) {
                if (realm.getName().contains(realmTypeEnum.getTypeName())) {
                    typeRealms.add(realm);
                    break;
                }
            }
            if (CollectionUtils.isEmpty(typeRealms)) {
                throw new CamException(SysCode.SYSTEM_ERROE);
            }
    
            // 不适用 多个realm 一个认证成功就通过,只能使用指定的realm
            return doSingleRealmAuthentication(typeRealms.get(0), authenticationToken);
        }
    
    
    }
    
    

RealmAuthorization鉴权

  • @Slf4j
    @Component
    public class MultiRealmAuthorizer extends ModularRealmAuthorizer {
    
        @Override
        public boolean hasRole(PrincipalCollection principals, String roleIdentifier){
            assertRealmsConfigured();
            RealmTypeEnum realmTypeEnum = RealmTypeEnum.GAME_MANAGER;
            if(principals.getPrimaryPrincipal() instanceof MerchantInfo){
               realmTypeEnum = RealmTypeEnum.MERCHANT;
            }
            // 循环所有的Realm获取对应类型的Realm进行鉴权操作
            for (Realm realm : getRealms()) {
                if (!(realm instanceof Authorizer)) continue;
                if(RealmTypeEnum.MERCHANT.equals(realmTypeEnum) && realm.getName().contains(realmTypeEnum.getTypeName())){
                    return ((MerchantRealm) realm).hasRole(principals, roleIdentifier);
                }
                if(RealmTypeEnum.GAME_MANAGER.equals(realmTypeEnum) && realm.getName().contains(realmTypeEnum.getTypeName())){
                    return ((ModuleSysUserRealm) realm).hasRole(principals, roleIdentifier);
                }
    
            }
            return false;
        }
    }
    
    

注意事项

  • ModularRealmAuthenticator 是Realm认证的方法执行器,可以支持多Realm,它有默认的多Realm执行策略

    • AuthenticationStrategy 接口有三个实现类代表多个Realm不同策略
    • AllSuccessfulStrategy 所有的Realm都要认证成功
    • AtLeastOneSuccessfulStrategy 一个认证成功即可
    • FirstSuccessfulStrategy 第一个Realm认证成功就返回,不执行后面的
  • ModularRealmAuthorizer 是Realm授权的方法

    • hasRole 是Realm判断是否存在某角色方法

    • // 源代码基础方法是所有的Realm都进行一遍hasRole判断,所以要重写
      public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
              assertRealmsConfigured();
              for (Realm realm : getRealms()) {
                  if (!(realm instanceof Authorizer)) continue;
                  if (((Authorizer) realm).hasRole(principals, roleIdentifier)) {
                      return true;
                  }
              }
              return false;
      }
      
      

动态权限过滤链路

  • 在项目当管理员在后台对一个角色 进行用户的新增和删除,对权限更新修改。都要修改shiro中的过滤器链路,使用最新的数据库配置的链路。
  • 一般重启项目可以更新最新的过滤器链路,但是项目运行中并不可取。就需要自动化动态的更新过滤器链路。

Role角色修改方法

  • @Service
    public class RoleServiceImpl implements RoleService {
    
        @Autowired
        private RoleMapper roleMapper;
    
        @Autowired
        private RoleResourceMapper roleResourceMapper;
    
        @Autowired
        private UserRoleMapper userRoleMapper;
    
        @Autowired
        private UserMapper userMapper;
    
        @Autowired
        private ShiroFilterSupport shiroFilterSupport;
    
        @Autowired
        private StringRedisTemplate redisTemplate;
    
        @Override
        public void add(RoleAddReqDTO reqDTO) {
            // 查询是否重复
            List<Role> roleList = roleMapper.selectList(new QueryWrapper<Role>().eq("LABEL", reqDTO.getLabel()));
            if (CollectionUtils.isNotEmpty(roleList)) {
                throw new CamAirException("角色重复啦");
            }
            // 添加角色不影响当前登录的用户设备
            Role role = new Role();
            BeanUtils.copyProperties(reqDTO , role);
            role.setId(SequenceGenerator.generateId());
            roleMapper.insert(role);
        }
    
        @Override
        public void delete(String roleId) {
            // 判断是否删除的是默认角色
            if ("1".equals(roleId)) {
                throw new CamAirException("不能删除默认角色");
            }
            // 角色下用户信息
            List<User> userList = userMapper.queryByRoleId(roleId);
            // 删除角色
            Role role = roleMapper.queryByRoleId(roleId);
            if (Objects.isNull(role)) {
                throw new CamAirException("角色数据不存在");
            }
            roleMapper.deleteById(roleId);
            // 删除角色用户连接表
            roleResourceMapper.deleteByRoleId(roleId);
            userRoleMapper.deleteByRoleId(roleId);
            // 清除用户连接
            userList.forEach(p -> {
                shiroFilterSupport.delLoginSession(p.getLoginName());
            });
            // 更新动态过滤器链路 , 异步方法这里可以使用 MQ消息,或记录一个redis值通过shiro的onPreHandle更新权限
            redisTemplate.opsForValue().set(ShiroPathCheckConstant.PATH_REDIS_PREFIX , String.valueOf(System.currentTimeMillis()));
        }
    
        @Override
        public void enableRole(String roleId) {
            Role role = roleMapper.queryByRoleId(roleId);
            if (Objects.isNull(role)) {
                throw new CamAirException("角色数据不存在");
            }
            Role roleUpdate = new Role();
            roleUpdate.setId(roleId);
            roleUpdate.setEnableFlag("NO");
            roleMapper.updateRole(roleUpdate);
            // 清除用户登录连接
            List<User> userList = userMapper.queryByRoleId(roleId);
            userList.forEach(p -> {
                shiroFilterSupport.delLoginSession(p.getLoginName());
            });
            // 更新动态过滤器链路 , 异步方法这里可以使用 MQ消息,或记录一个redis值通过shiro的onPreHandle更新权限
            redisTemplate.opsForValue().set(ShiroPathCheckConstant.PATH_REDIS_PREFIX , String.valueOf(System.currentTimeMillis()));
        }
    
        /**
        *  用户-角色 绑定不需要清除登录连接 和 更新过滤链路
        * @param reqDTO 1
        */
        @Override
        public void settingUser(SettingUserReqDTO reqDTO) {
            // 查询角色是否禁用了
            Role role = roleMapper.queryByRoleId(reqDTO.getRoleId());
            if (Objects.isNull(role) || "NO".equals(role.getEnableFlag())) {
                throw new CamAirException("角色数据不存在或已禁用");
            }
            // 清除之前旧的用户中间表信息
            userRoleMapper.deleteByRoleId(reqDTO.getRoleId());
            // 添加最新绑定用户
            List<UserRole> userRoleList = reqDTO.getUserIds().stream().map(p -> {
                UserRole userRole = new UserRole();
                userRole.setId(SequenceGenerator.generateId());
                userRole.setEnableFlag("YES");
                userRole.setUserId(p);
                userRole.setRoleId(reqDTO.getRoleId());
                return userRole;
            }).collect(Collectors.toList());
            userRoleMapper.batchInsert(userRoleList);
        }
    
        @Override
        public void settingResource(SettingResourceReqDTO reqDTO) {
            // 查询角色是否禁用了
            Role role = roleMapper.queryByRoleId(reqDTO.getRoleId());
            if (Objects.isNull(role) || "NO".equals(role.getEnableFlag())) {
                throw new CamAirException("角色数据不存在或已禁用");
            }
            // 清除之前旧的中间表信息
            roleResourceMapper.deleteByRoleId(reqDTO.getRoleId());
            // 添加最新绑定权限
            List<RoleResource> roleResourceList = reqDTO.getResourceIds().stream().map(p -> {
                RoleResource roleResource = new RoleResource();
                roleResource.setId(SequenceGenerator.generateId());
                roleResource.setEnableFlag("YES");
                roleResource.setResourceId(p);
                roleResource.setRoleId(reqDTO.getRoleId());
                return roleResource;
            }).collect(Collectors.toList());
            if (CollectionUtils.isNotEmpty(roleResourceList)) {
                roleResourceMapper.batchInsert(roleResourceList);
            }
            // 设置redis更新权限
            redisTemplate.opsForValue().set(ShiroPathCheckConstant.PATH_REDIS_PREFIX , String.valueOf(System.currentTimeMillis()));
        }
    }
    
    

Support更新过滤器链路方法

  • @Component
    @Slf4j
    public class ShiroFilterSupport {
    
        @Autowired
        private RedisSessionDAO redisSessionDAO;
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        @Autowired
        private DefaultWebSessionManager sessionManager;
    
        @Autowired
        private ResourceMapper resourceMapper;
    
        @Autowired
        private RoleMapper roleMapper;
    
        private static final String ACCOUNT_LIMIT = "account_limit:";
    
        /**
        *  权限用户信息变动时,将对应的登录session剔除
        */
        public void delLoginSession(String loginName) {
            // 找到用户loginName下所有的session
            Set<String> keys = redisTemplate.keys(ACCOUNT_LIMIT + loginName + ":*");
            if (CollectionUtils.isEmpty(keys)) {
                return;
            }
            // 剔除
            keys.forEach(key -> {
                try {
                    String sessionId = (String) redisTemplate.opsForValue().get(key);
                    Session session = sessionManager.getSession(new DefaultSessionKey(sessionId));
                    redisSessionDAO.delete(session);
                    // 清除当前记录登录的key
                    redisTemplate.delete(key);
                }catch (Exception e) {
                    log.error("清除用户登录连接失败" , e);
                }
            });
        }
    
        public Map<String, String> getFilterChain() {
            HashMap<String, String> listMap = new LinkedHashMap<>();
            // anon默认授权 , authc登录鉴权
            listMap.put("/static/**" , "anon");
            listMap.put("/user/add" , "anon");
            listMap.put("/manager/login" , "customLoginNum,anon");
            // 使用动态过滤器链路 , 从数据库查询 , 只查询 3级(功能授权)和4级(菜单下默认权限)的即可
            List<Resource> resourceList = resourceMapper.queryAll();
            resourceList.forEach(resource -> {
                // 如果是 4 级静默授权就查询父节点的角色 进行过滤器链路
                List<Role> roleList;
                if ("4".equals(resource.getLeaf())) {
                    roleList = roleMapper.queryByResourceId(resource.getParentId());
                } else {
                    // 查询role角色信息
                    roleList = roleMapper.queryByResourceId(resource.getId());
                }
                if (CollectionUtils.isNotEmpty(roleList)) {
                    List<String> roleLabelList = roleList.stream().map(Role::getLabel).collect(Collectors.toList());
                    listMap.put(resource.getServiceName() , "customUser,customCheck,customRole" + roleLabelList.toString());
                }
            });
            listMap.put("/**" , "customUser,customCheck,customRole[admin]");
            return listMap;
        }
    
    
        /**
        *  更新url权限过滤配置
        * @param  1
        * @return void
        */
        public void updatePathCheck() {
            log.info("更新url过滤器链路===>{}" , System.currentTimeMillis());
            // 更新url过滤配置
            synchronized (this) {
                try {
                    // 获取过滤器工厂
                    ShiroFilterFactoryBean filterFactoryBean = ApplicationBeanUtils.getBean(ShiroFilterFactoryBean.class);
                    // 获取具体的过滤器
                    AbstractShiroFilter shiroFilter = (AbstractShiroFilter) filterFactoryBean.getObject();
                    // 获取url权限路由匹配的解析器
                    PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) shiroFilter.getFilterChainResolver();
    
                    // 通过解析器获取拦截器链管理器,管理者 url和filter拦截器的关系
                    DefaultFilterChainManager filterChainManager = (DefaultFilterChainManager) filterChainResolver.getFilterChainManager();
                    // 清空旧的权限过滤链配置
                    filterChainManager.getFilterChains().clear();
                    // 清空工厂中配置的FilterChainDefinitionMap
                    filterFactoryBean.getFilterChainDefinitionMap().clear();
    
                    // 重新构建过滤器链路信息到 ShiroFilterBean工厂中
                    Map<String, String> filterChainMap = getFilterChain();
                    filterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
    
                    // 重新构建url过滤器校验 到过滤器管理器中
                    for (Map.Entry<String, String> entry : filterChainMap.entrySet()) {
                        log.info("添加过滤器链路,url=>{},chain=>{}" , entry.getKey() , entry.getValue());
                        filterChainManager.createChain(entry.getKey() , entry.getValue().trim().replace(" ",""));
                    }
                }catch (Exception e) {
                    log.error("更新url过滤器权限报错" , e);
                }
            }
        }
    }
    
    

Filter是否更新过滤器链路

  • @Slf4j
    public class CustomPathCheckFilter extends PathMatchingFilter {
    
        private String flag = "success";
    
        @Override
        protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
            // 将此校验过滤器,在链路中优先级提高,先进行过滤器链路的动态更新,在往下走下面的过滤器角色校验
            // 每次判断redis存入的更新url过滤器链路的 比较值进行判断,如果更新了与当前 flag 变量不同就更新
            log.info("更新权限判断流程....flag===>{}" , flag);
            String time = ApplicationBeanUtils.getBean(StringRedisTemplate.class).opsForValue().get(ShiroPathCheckConstant.PATH_REDIS_PREFIX);
            if (!flag.equals(time) && !StringUtils.isEmpty(time)) {
                ShiroFilterSupport support = ApplicationBeanUtils.getBean(ShiroFilterSupport.class);
                try {
                    support.updatePathCheck();
                    flag = time;
                }catch (Exception e) {
                    log.error("更新权限过滤失败" , e);
                }
            }
            return true;
        }
    }
    
    

ApplicationBeanUtils

  • @Component
    public class ApplicationBeanUtils implements ApplicationContextAware {
    
        private static ApplicationContext context;
    
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            context = applicationContext;
        }
    
        public static <T> T getBean(Class<T> clazz) {
            return context.getBean(clazz);
        }
    }
    
    

注意事项

  • 在使用动态更新过滤器链路时,需要分清楚 什么业务需要角色下的用户退出登录,什么业务场景下需要更新过滤器链路
    • 禁用了或者删除了Role角色的业务就需要更新过滤器链路信息,并且需要角色下用户等登出。
    • 给角色添加了新用户,就不要做任何操作。
    • 给角色添加了新的资源,就只需要更新过滤器链路即可,不需要用户退出。
  • 代码更新是否更新过滤器链路用了redis和filter进行判断是否更新,主要是因为在于分布式集群系统中时,需要把每一个服务都要执行,主要用springboot的异步方法或者MQ消息可能就 因为负载均衡 只更新了某一台服务.

开启Realm的Authorization授权缓存

  • /**
         *  核心权限管理器
         */
        @Bean
        public DefaultWebSecurityManager webSecurityManager(UserRealm userRealm) {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            // Realm配置缓存
            // Authorization授权是默认开启 , Authentication认证是默认关闭的
            userRealm.setAuthenticationCachingEnabled(false);
    //        userRealm.setAuthenticationCacheName("test:AuthenticationCache");
    //        userRealm.setAuthorizationCachingEnabled(false);
     userRealm.setAuthorizationCacheName(ShiroPathCheckConstant.AUTHORIZATION_CACHE_PREFIX);
            // 设置realm
            securityManager.setRealm(userRealm);
            // 缓存管理器
            securityManager.setCacheManager(redisCacheManager);
            // 会话session管理
            securityManager.setSessionManager(getDefaultWebSessionManager());
            return securityManager;
        }
    
    
  • userRealm.setAuthorizationCachingEnabled(true); Realm的授权缓存是默认开启的,当开启时,就会通过缓存管理器,将授权信息缓存到redis中。

  • 如果我们将某个用户的角色删除了,就需要清除redis中缓存的授权信息,让其重新走 Realm的doGetAuthorizationInfo 方法获取最新的用户角色信息。

    • 需要清除授权缓存的场景业务:
    • 给角色更新关联用户
    • 禁用/删除/更新 角色等。
    • 给角色更新权限资源关联时不需要清除缓存,只是更新过滤器链路的角色判断,而对角色与用户的关联没有影响,缓存的是用户层面的角色信息。
  • /**
    *  清除所有用户的Authorization授权缓存
    */
    public void delAuthorizationCache() {
      // 查询前缀开头所有keys 前缀名:test:shiro:cache:authorizationCache
      Set keys = redisTemplate.keys(cachePrefix + ShiroPathCheckConstant.AUTHORIZATION_CACHE_PREFIX + ":*");
      redisTemplate.delete(keys);
    }
    
    

记住我

  • shiro提供了记住我用户名密码功能,直接登录,

  • 服务端会返回一个 cookie 存储在客户端浏览器,通过这个cookie与服务端进行交互,从而访问接口功能时可以直接访问,不用再次登录。但是目前这个功能只适用于单机系统,因为暂时没找到将这个 RememberMe-Cookie 存储在redis中的方法。是存储在web的session会话中。

  • 因为Shiro的记住我功能局限性,一般不会用。还是自己业务中定制实现比较好。

  • /**
        *  记住我登录管理器
        * @return CookieRememberMeManager
        */
        @Bean
        public CookieRememberMeManager cookieRememberMeManager() {
            CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
            SimpleCookie simpleCookie = new SimpleCookie();
            // 设置过期时间7天
            simpleCookie.setMaxAge(60 * 60 * 24 * 7);
            // 设置cookie的key名称
            simpleCookie.setName("rememberMe");
            // 设置仅http传输
            simpleCookie.setHttpOnly(false);
            simpleCookie.setPath("/");
            cookieRememberMeManager.setCookie(simpleCookie);
            //这个地方有点坑,不是所有的base64编码都可以用,长度过大过小都不行,没搞明白,官网给出的要么0x开头十六进制,要么base64
    //        cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
            return cookieRememberMeManager;
        }
    
    
  • 核心安全管理器

  • /**
         *  核心权限管理器
         */
        @Bean
        public DefaultWebSecurityManager webSecurityManager(UserRealm userRealm , CookieRememberMeManager rememberMeManager) {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            // Realm配置缓存
            // Authorization授权是默认开启 , Authentication认证是默认关闭的
            userRealm.setAuthenticationCachingEnabled(false);
    //        userRealm.setAuthenticationCacheName("test:AuthenticationCache");
    //        userRealm.setAuthorizationCachingEnabled(false);
            userRealm.setAuthorizationCacheName(ShiroPathCheckConstant.AUTHORIZATION_CACHE_PREFIX);
            // 设置realm
            securityManager.setRealm(userRealm);
            // 缓存管理器
            securityManager.setCacheManager(redisCacheManager);
            // 会话session管理
            securityManager.setSessionManager(getDefaultWebSessionManager());
            // 记住我功能
            securityManager.setRememberMeManager(rememberMeManager);
            return securityManager;
        }
    
    
  • 登录方法是判断赋值 LoginToken 是否记住我

  • @Override
        public String login(User user) {
            UserLoginToken loginToken = new UserLoginToken(user.getLoginName(), user.getPassWord());
            try {
                // 设置记住我功能
                loginToken.setRememberMe(true);
                SecurityUtils.getSubject().login(loginToken);
                // 获取SessionId
                String sessionId = (String) SecurityUtils.getSubject().getSession().getId();
                // 记录登录次数到Redis中
                log.info("登录完成,SessionId===>{}" , sessionId);
                redisTemplate.opsForValue().set(ShiroPathCheckConstant.ACCOUNT_LIMIT + user.getLoginName() +":"+sessionId+":"+System.currentTimeMillis() , sessionId , 3600 , TimeUnit.SECONDS);
                return (String) sessionId;
            } catch (IncorrectCredentialsException e) {
                e.printStackTrace();
                return "passWord Error";
            } catch (AuthenticationException e) {
                e.printStackTrace();
                return "loginError";
            }
        }
    
    
  • 返回结果:

  • 在这里插入图片描述

项目路径 shiro-springboot

  • 链接:https://pan.baidu.com/s/1OotLAOubzxpZDpPLpjdFuw
    提取码:bk3p
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值