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