news 2026/10/14 1:02:44

Shiro权限动态刷新:缓存一致性与实时生效实战方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Shiro权限动态刷新:缓存一致性与实时生效实战方案

1. 这个需求背后的真实战场:权限变更不是“改个数据库字段”那么简单

你有没有遇到过这样的场景:后台管理员刚给某位用户加了“财务报表导出”权限,转头用户就反馈“点不动,还是提示无权限”?或者更糟——用户明明已经被移除了管理员角色,却还能继续访问敏感配置页?这类问题在中大型系统里几乎每周都会冒出来,而绝大多数人第一反应是去查Shiro的@RequiresPermissions("finance:export")注解写对没、Realm里doGetAuthorizationInfo方法返回的权限集合对不对。但真相往往是:代码逻辑完全正确,问题出在缓存上。

Shiro默认使用AuthorizationCache机制,它会在用户首次登录或权限校验时,把该用户的全部角色、权限信息(即SimpleAuthorizationInfo对象)缓存起来。后续所有hasRole()、isPermitted()调用,Shiro会优先从缓存读取,而不是每次都重新调用Realm查询数据库。这本是性能优化的黄金实践,可一旦权限发生动态变更——比如运营同学在后台界面点一下“启用审批流”,或者风控系统自动降级某个高危账号——缓存里的旧数据就成了拦路虎。它不会自动感知数据库变化,也不会主动失效。你改了DB,Shiro不知道;你清了缓存,可能误伤其他用户;你关掉缓存,系统响应时间直接翻倍。这就是标题里“修改用户权限后并刷新对应用户缓存授权”所直面的核心矛盾:如何在不牺牲性能的前提下,实现权限数据的强一致性。

这个需求绝非Shiro初学者的玩具实验。它横跨三个关键领域:一是Shiro授权模型的底层机制(AuthorizationCache与CacheManager的协作关系),二是缓存策略的设计哲学(局部刷新 vs 全局清空 vs 延迟失效),三是业务系统的集成深度(你得知道权限在哪改、谁触发改、改完要通知谁)。我参与过的某金融风控平台项目,就因初期只做了“改完DB立刻cache.remove(key)”,结果在高并发下引发缓存雪崩——因为所有用户权限缓存key都基于username生成,而remove操作本身是同步阻塞的,导致大量请求排队等待缓存操作完成,TP99飙升到3秒以上。后来我们才意识到,必须把“缓存刷新”这件事,从一个简单的remove命令,升级为一套带状态追踪、异步执行、失败重试的轻量级事件总线。所以,这篇文章不讲“怎么让Shiro跑起来”,而是聚焦于:当权限必须实时生效时,你手里的那把Shiro钥匙,到底该怎么拧才不卡壳、不打滑、不崩断。

2. Shiro授权缓存的三重门:从原理到陷阱的完整拆解

要精准刷新缓存,你得先看清它的结构。Shiro的授权缓存不是一块铁板,而是由三层组件咬合而成的精密齿轮组:AuthorizationInfo(数据载体)、AuthorizationCache(缓存接口)、CacheManager(缓存管家)。很多人栽跟头,是因为只盯着最后一层,却忽略了前两层才是决定“刷什么、怎么刷”的关键。

2.1 AuthorizationInfo:权限数据的唯一真相源

AuthorizationInfo是Shiro授权体系的基石对象,它封装了用户的所有授权信息:角色列表(getRoles())、权限字符串列表(getStringPermissions())、权限对象列表(getObjectPermissions())等。当你在自定义Realm中重写doGetAuthorizationInfo(PrincipalCollection principals)方法时,最终return的正是这个对象。重点来了:Shiro缓存的,不是数据库里的原始记录,而是这个AuthorizationInfo对象的序列化快照。这意味着,即使你数据库里权限字段已更新,只要缓存里存着的AuthorizationInfo没变,Shiro就永远认为用户拥有旧权限。

我见过最典型的错误写法,是在权限变更后,只更新数据库,却不重建AuthorizationInfo。比如某电商后台,当运营给用户A开通“秒杀活动管理”权限时,后端代码只执行了一条SQL:

UPDATE user_role SET role_id = 'ROLE_SECKILL_ADMIN' WHERE user_id = 'A';

然后就结束了。但Shiro的缓存里,用户A的AuthorizationInfo对象依然只有["ROLE_USER", "ROLE_OPERATOR"],新角色压根没进去。因为AuthorizationInfo只在用户下次登录或显式调用subject.logout()再login()时才会重建。这就造成了“改了等于没改”的假象。正确的做法,是在数据库更新后,立即构造一个新的AuthorizationInfo实例,并将其写入缓存——但这一步,Shiro不会帮你自动做,必须你来驱动。

2.2 AuthorizationCache:缓存行为的契约与边界

AuthorizationCache是一个接口,定义了缓存的基本操作:get(key)、put(key, value)、remove(key)、clear()。Shiro本身不提供具体实现,它依赖你配置的CacheManager(如EhCacheManager、RedisCacheManager)来生成具体的缓存实例。这里埋着第一个大坑:AuthorizationCache的key生成规则,决定了你能否精准定位到“某个用户”的缓存。

Shiro默认的key生成策略,是将PrincipalCollection(通常是用户名)作为key。这看起来很合理,但问题在于:PrincipalCollection可能包含多个principal(比如同时有username和userId),而不同Shiro版本或不同CacheManager实现,对PrincipalCollection的hashCode()和equals()处理方式可能不同。我曾在一个使用Shiro-Redis插件的项目中发现,缓存key被序列化为[username],而手动remove时传入的是username字符串,结果remove永远返回null——因为key其实是ArrayList对象,不是字符串。解决方案是强制统一key格式,在CacheManager配置中指定keySerializer,或在AuthorizationCache包装类中重写getCacheKey方法,确保key始终是String.valueOf(principal)。

第二个陷阱是clear()方法的滥用。很多开发者图省事,在权限变更后直接调用authorizationCache.clear(),以为这样就能“一劳永逸”。但clear()会清空整个授权缓存,影响所有用户。在日活百万的系统里,这相当于在高速公路上突然关闭所有车道——瞬间涌入的数据库查询请求会压垮你的MySQL连接池。某社交App就因此出现过凌晨三点的P0事故:运维同学批量调整了5000个KOL的权限,脚本里写了cache.clear(),结果10秒内数据库CPU飙到98%,首页Feed流大面积超时。所以,clear()只能是最后的保底手段,绝不能作为常规方案。

2.3 CacheManager:缓存管家的选型与定制

CacheManager是Shiro与底层缓存技术(Ehcache、Redis、Caffeine)的桥梁。它的选型,直接决定了你刷新缓存的粒度、速度和可靠性。我们来对比三种主流选择:

缓存类型刷新粒度优势劣势适用场景
Ehcache (本地)用户级(需手动计算key)零网络开销,延迟最低(<1ms)无法跨JVM共享,集群环境下缓存不一致单机部署、小规模系统
Redis (分布式)用户级(key可精确控制)天然支持集群,所有节点缓存一致,支持TTL和Pub/Sub网络IO开销,单次操作约1-5ms中大型分布式系统,必须强一致性
Caffeine (本地+异步加载)用户级(内置refreshAfterWrite)性能接近Ehcache,支持自动刷新、最大容量淘汰仍是本地缓存,集群下需额外同步机制对延迟极度敏感,且能接受短暂不一致

关键洞察是:没有“最好”的CacheManager,只有“最适合你架构”的CacheManager。如果你的系统是Spring Boot + Redis集群,那么RedisCacheManager就是不二之选,因为它天然支持通过redisTemplate.delete("shiro:authc:" + username)进行精准驱逐。但如果你的系统是纯内存部署、且QPS不高,强行上Redis反而增加复杂度和故障点。我建议:先用Caffeine做本地缓存,配合一个轻量级的Redis Pub/Sub通道做集群间缓存失效通知——这样既保证了单节点性能,又解决了分布式一致性问题。

提示:无论选哪种CacheManager,务必在shiro.ini或JavaConfig中显式配置authorizationCacheName。例如:

securityManager.cacheManager = org.apache.shiro.cache.ehcache.EhCacheManager securityManager.cacheManager.cacheManagerConfigFile = classpath:ehcache-shiro.xml securityManager.authorizationCacheName = shiroAuthorizationCache

这样你才能在代码中通过securityManager.getCacheManager().getCache("shiroAuthorizationCache")安全获取到授权缓存实例,避免因名称不匹配导致NullPointerException。

3. 四种刷新策略的实战对比:从“能用”到“稳用”的演进路径

“刷新缓存”听起来简单,但落地时你会面临四种截然不同的技术路径。它们不是并列选项,而是一条从“临时救火”到“生产级稳定”的演进路线。我将用真实项目中的代码片段和压测数据,告诉你每种方案的适用边界和致命缺陷。

3.1 方案一:暴力清除(仅限开发/测试环境)

这是新手最容易想到的方案:权限变更后,直接调用authorizationCache.remove(principal)。代码简洁到只有两行:

// 权限变更后 String username = "userA"; AuthorizationCache authorizationCache = securityManager.getAuthorizationCache(); authorizationCache.remove(username); // 或 remove(new SimplePrincipalCollection(username, "myRealm"))

它确实能让用户下次请求时重新加载权限。但问题在于:remove操作是同步阻塞的。在JMeter压测中,当并发线程数超过200时,remove平均耗时从0.2ms飙升至15ms,成为整个请求链路的瓶颈。更严重的是,如果remove操作抛出异常(比如Redis连接超时),整个权限变更事务就会回滚失败,导致“数据库改了,缓存没清,用户权限错乱”的灾难性状态。某教育SaaS平台就因此出现过“老师被降级为学生后,仍能进入教师后台”的线上Bug。所以,此方案仅推荐在单机开发环境验证逻辑,生产环境绝对禁止使用。

3.2 方案二:异步清理(解决阻塞,引入可靠性风险)

为了解决同步阻塞,自然想到用线程池异步执行remove:

private final ExecutorService cacheCleaner = Executors.newFixedThreadPool(5); // ... cacheCleaner.submit(() -> { try { authorizationCache.remove(username); } catch (Exception e) { log.error("Async remove auth cache failed for user: {}", username, e); } });

这确实把主线程的耗时降到了微秒级,TPS从800提升到2200。但新问题浮现:异步任务没有事务上下文,无法保证“数据库更新成功”与“缓存删除成功”的原子性。如果数据库更新成功,但异步线程因OOM崩溃,缓存就永远停留在脏状态。我们曾用Arthas在线诊断,发现某次发布后,线程池队列积压了3000+个未执行的remove任务,导致整整2小时内的权限变更全部失效。补救措施是加了ScheduledExecutorService定时扫描失败日志并重试,但这又引入了新的复杂度。结论是:异步清理是过渡方案,必须搭配失败监控和重试机制,否则就是埋雷。

3.3 方案三:事件驱动(解耦核心业务与缓存逻辑)

真正的生产级方案,是把“权限变更”抽象为一个领域事件,让缓存清理成为该事件的订阅者。这需要三步改造:

  1. 定义事件:UserPermissionChangedEvent
  2. 发布事件:在权限Service中,updateUserPermission()方法末尾applicationEventPublisher.publishEvent(new UserPermissionChangedEvent(user))
  3. 订阅事件:编写@EventListener监听器,执行缓存清理
@Component public class PermissionCacheEvictor { @EventListener public void onUserPermissionChanged(UserPermissionChangedEvent event) { String username = event.getUser().getUsername(); // 使用RedisTemplate精准删除 redisTemplate.delete("shiro:authc:" + username); // 同时记录审计日志 auditLogService.log("PERMISSION_CACHE_EVICTED", username); } }

这个方案的优势是彻底解耦:权限Service不用关心缓存技术细节,监听器可以独立部署、灰度发布、甚至迁移到消息队列(如Kafka)。我们在某银行核心系统中,将监听器升级为Kafka消费者,实现了跨数据中心的缓存同步——上海机房改权限,北京机房500ms内完成缓存失效。但要注意:事件必须是可靠的。我们最初用Spring内置的ApplicationEvent,结果在应用重启时丢失了未消费事件。后来改用Kafka,配置enable.auto.commit=false,并在消费成功后手动提交offset,才真正做到了“至少一次”投递。

3.4 方案四:缓存预热+TTL兜底(终极稳定性保障)

即使事件驱动再可靠,也无法100%避免极端情况(如Kafka集群全挂)。因此,我们必须加入“双保险”:在精准刷新之外,为主动缓存设置合理的TTL(Time-To-Live),并辅以按需预热。具体做法:

  • 将AuthorizationCache的TTL设为5分钟(redisTemplate.expire(key, 5, TimeUnit.MINUTES))
  • 在用户首次访问受保护资源时,如果缓存未命中,不仅从DB加载权限,还顺手将AuthorizationInfo写入缓存,并设置TTL
  • 对高频访问的超级管理员,启动时预热其缓存:“开机即巅峰”
// 预热示例:启动时加载TOP 100管理员 @PostConstruct public void warmUpAdminCache() { List<User> topAdmins = userService.findTopAdmins(100); topAdmins.forEach(user -> { SimpleAuthorizationInfo info = buildAuthInfoFromDb(user); authorizationCache.put(user.getUsername(), info); redisTemplate.expire("shiro:authc:" + user.getUsername(), 5, TimeUnit.MINUTES); }); }

压测数据显示,加入5分钟TTL后,即使Kafka完全不可用,权限变更的最大延迟也从“无限期”缩短为5分钟,且这5分钟内,用户只会经历一次慢查询(从DB加载),而非持续性错误。这才是生产环境该有的容错水位线。

注意:TTL值不是越短越好。设为30秒会导致缓存频繁失效,DB压力陡增;设为24小时则失去实时性意义。我们通过分析APM监控中的auth_cache_hit_rate指标,发现当TTL=5分钟时,命中率稳定在92%-95%,DB负载增加不到3%,是最佳平衡点。

4. 从代码到上线:一个可复用的权限刷新SDK设计与实操

前面讲了原理和策略,现在给你一套可以直接“抄作业”的工程化方案。这不是零散的代码片段,而是一个经过三个项目验证的、开箱即用的shiro-permission-refresh-starterSDK。它的设计哲学是:最小侵入、最大兼容、开箱即用。你不需要改一行Shiro配置,也不需要重写Realm,只需添加一个依赖,再加几行配置,就能获得企业级的权限刷新能力。

4.1 SDK核心模块与依赖关系

这个SDK采用分层架构,每一层都解决一个明确问题:

  • core模块:定义PermissionRefreshService接口和DefaultPermissionRefreshService实现,封装了remove、evictByPattern、warmUp等核心方法。它不依赖任何具体缓存技术,只面向AuthorizationCache编程。
  • redis-spring-boot-starter模块:Spring Boot自动装配模块。它会自动检测classpath中是否存在spring-boot-starter-data-redis,如果存在,则创建RedisPermissionRefreshServiceBean,该Bean使用RedisTemplate进行精准key操作,并集成Lettuce连接池健康检查。
  • event-spring-boot-starter模块:事件驱动模块。它提供@EnablePermissionEvent注解,开启事件监听,并内置KafkaPermissionEventListener和RabbitMQPermissionEventListener,支持主流消息中间件。

整个SDK的Maven依赖树非常干净,没有传递性冲突。你只需要在pom.xml中添加:

<dependency> <groupId>com.example</groupId> <artifactId>shiro-permission-refresh-starter</artifactId> <version>1.2.0</version> </dependency> <!-- 如果用Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- 如果用Kafka --> <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> </dependency>

4.2 三步接入:从零配置到全功能启用

第一步:基础配置(application.yml)

shiro: permission-refresh: enabled: true # 启用SDK strategy: redis # 可选:redis / kafka / rabbitmq / none ttl-minutes: 5 # 缓存TTL,单位分钟 warm-up: enabled: true admin-usernames: ["admin", "super_admin"] # 启动时预热的用户名列表

第二步:在权限变更处注入服务并调用

@Service public class UserServiceImpl implements UserService { @Autowired private PermissionRefreshService permissionRefreshService; @Override @Transactional public void updateUserRole(String username, String newRoleId) { // 1. 更新数据库 userRoleMapper.updateUserRole(username, newRoleId); // 2. 刷新该用户的授权缓存(一行代码!) permissionRefreshService.evictByUsername(username); // 3. 如果需要广播给其他节点,SDK会自动发送Kafka事件 } }

看到没?你原来的业务代码只增加了一行permissionRefreshService.evictByUsername(username)。SDK内部会根据strategy配置,自动选择Redis删除、Kafka广播或本地清除。如果未来你想从Redis切换到Kafka,只需改一行配置,无需动任何业务代码。

第三步:高级定制(可选)如果你的系统有特殊需求,比如“只刷新某个角色的缓存”,SDK提供了evictByRole(String roleName)方法:

// 当禁用某个角色时,批量刷新所有拥有该角色的用户 List<String> usernames = roleService.findUsersByRole("ROLE_DEPRECATED"); usernames.forEach(permissionRefreshService::evictByUsername);

或者,你想在缓存失效时收到通知,可以实现PermissionRefreshListener接口:

@Component public class MyPermissionRefreshListener implements PermissionRefreshListener { @Override public void onCacheEvicted(String username, EvictionType type) { if (type == EvictionType.EXPLICIT) { // 记录审计日志 auditLog.info("User {} cache evicted explicitly", username); } } }

4.3 上线前必做的五项验证清单

再好的SDK,上线前也必须经过严格验证。这是我总结的、每次发布前必做的五项检查,缺一不可:

  1. 缓存key验证:用Redis CLI连接生产Redis,执行KEYS "shiro:authc:*",确认key格式是否为shiro:authc:username,而非shiro:authc:[username]。如果格式不对,说明CacheManager配置有误,需修正keySerializer。
  2. TTL验证:对一个测试用户执行TTL "shiro:authc:testuser",确认返回值是预期的300(5分钟=300秒)。如果不是,检查shiro.permission-refresh.ttl-minutes配置是否被profile覆盖。
  3. 事件投递验证:在Kafka消费者端开启DEBUG日志,触发一次权限变更,确认日志中出现Received PermissionChangeEvent for user:testuser。如果没有,检查@EnablePermissionEvent是否被正确扫描。
  4. 并发压力验证:用JMeter模拟1000并发请求,连续执行100次权限变更,监控redisTemplate.delete的失败率(应为0%)和平均耗时(应<5ms)。如果失败率>0.1%,说明Redis连接池配置过小,需增大lettuce.pool.max-active。
  5. 降级验证:手动停掉Kafka集群,再执行权限变更,确认用户5分钟内(TTL到期)能自动获取新权限,且期间只出现一次慢查询(DB加载),无报错。

提示:我们把这个验证清单做成了自动化脚本,集成到CI/CD流水线中。每次构建时,脚本会自动启动一个嵌入式Redis和Kafka,运行上述5项测试,全部通过才允许打包。这让我们在过去18个月的237次发布中,权限相关故障率为0。

5. 踩坑实录:那些文档里不会写的、血泪换来的12条经验

理论和方案都讲完了,现在分享一些只有在深夜排查线上Bug时,才会刻进DNA里的经验。这些不是教科书知识,而是我亲手踩过、修复过、复盘过的真实教训。每一条,都对应着一个曾经让团队加班到凌晨三点的P0事故。

5.1 关于Realm的“懒加载”陷阱

很多开发者为了性能,在doGetAuthorizationInfo中写这样的代码:

@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = (String) principals.getPrimaryPrincipal(); // 错误:只查角色,不查权限 Set<String> roles = roleService.findRolesByUsername(username); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles); return info; // 没有调用 setStringPermissions() }

他们认为“角色有了,权限自然就有了”,因为Shiro的ModularRealmAuthorizer会自动根据角色查权限。但问题在于:ModularRealmAuthorizer的权限查询,是另一个独立的缓存(authorizationCache),它和角色缓存是分开的!所以,你清除了角色缓存,权限缓存还在。用户能看到新角色,但isPermitted("order:delete")依然返回false。正确做法是:在doGetAuthorizationInfo中,必须显式加载并设置所有权限:

Set<String> permissions = permissionService.findPermissionsByUser(username); info.setStringPermissions(permissions); // 必须这行!

5.2 关于Subject的“线程绑定”误区

Shiro的Subject是与当前线程绑定的(通过ThreadLocal)。这意味着,你在异步线程中调用SecurityUtils.getSubject(),拿到的可能是另一个用户的Subject,甚至是null。我见过最离谱的Bug是:一个异步任务里写了subject.checkRole("ADMIN"),结果它检查的不是当前任务所属用户,而是上一个HTTP请求的用户!解决方案只有两个:要么在异步任务开始时,把PrincipalCollection作为参数传入;要么用Subject.Builder重建Subject:

// 正确:传入principal CompletableFuture.runAsync(() -> { // ... do something permissionRefreshService.evictByUsername(username); // 用username,不用subject }); // 或者,重建Subject(不推荐,开销大) CompletableFuture.runAsync(() -> { Subject subject = new Subject.Builder().principals(new SimplePrincipalCollection(username, "myRealm")).buildSubject(); subject.checkRole("ADMIN"); // 这样才安全 });

5.3 关于缓存穿透的防御

当恶意用户用不存在的用户名(如username="aaaaa...")疯狂请求/admin/dashboard时,Shiro会每次都去查DB,因为缓存里没有。这就是缓存穿透。我们的解决方案是:在doGetAuthorizationInfo中,对查不到的用户,也写入一个空的SimpleAuthorizationInfo到缓存,并设置较短TTL(如30秒):

if (user == null) { // 写入空缓存,防止穿透 SimpleAuthorizationInfo emptyInfo = new SimpleAuthorizationInfo(); authorizationCache.put(username, emptyInfo); redisTemplate.expire("shiro:authc:" + username, 30, TimeUnit.SECONDS); return emptyInfo; }

这招让我们在一次针对登录接口的渗透测试中,成功抵御了10万QPS的穿透攻击,DB CPU始终保持在40%以下。

5.4 其他11条经验速查表

序号经验描述为什么重要如何验证
4SecurityManager必须是Spring管理的Bean,不能new DefaultWebSecurityManager()否则@Autowired的PermissionRefreshService会注入失败,导致null指针检查SecurityManager的@Bean方法是否被@Configuration类管理
5自定义CacheManager时,必须重写getCache(String cacheName)方法,返回AuthorizationCache实例Shiro会调用此方法获取授权缓存,如果返回null,所有缓存操作静默失败在getCache方法中加日志,确认是否被调用
6@RequiresPermissions注解的权限字符串,必须与setStringPermissions()中传入的完全一致(大小写、冒号、下划线)Shiro的权限匹配是严格字符串匹配,"order:delete"≠"order.delete"用subject.isPermitted("order:delete")在Debug模式下单步验证
7在logout()后,必须调用subject.getSession().stop(),否则Session缓存不会释放Session里可能存有AuthorizationInfo的引用,导致内存泄漏用VisualVM监控老年代内存,看是否有SimpleAuthorizationInfo对象堆积
8如果用了ShiroFilterFactoryBean,其filterChainDefinitionMap必须在SecurityManager之后初始化否则ShiroFilter会拿不到SecurityManager,导致所有请求404检查Spring Boot启动日志,确认ShiroFilter的postProcessBeforeInitialization日志在SecurityManager之后
9AuthorizationCache的put操作,必须在doGetAuthorizationInfo返回前完成否则put会被CacheManager的put方法拦截,但value是null在put方法中加断点,确认value不为null
10对于JWT Token认证,AuthorizationInfo必须存储在Token Payload中,不能只依赖缓存否则Token过期后,用户无法续期,必须重新登录用JWT.io解析Token,确认permissions字段存在
11@RequiresUser注解的校验,不走AuthorizationCache,而是走AuthenticationCache所以权限刷新不影响@RequiresUser,但会影响@RequiresPermissions分别用subject.isAuthenticated()和subject.isPermitted()测试
12在单元测试中,必须用@WithMockUser或SecurityMockMvcRequestPostProcessors.user()模拟用户否则SecurityUtils.getSubject()返回null,测试永远失败运行一个最简单的@Test,确认subject != null
13Shiro的AuthorizationCache和AuthenticationCache是两个独立缓存,必须分别管理清除授权缓存,不会影响登录状态缓存分别执行cacheManager.getCache("shiroAuthenticationCache")和getCache("shiroAuthorizationCache")
14生产环境必须关闭Shiro的debug日志,因为AuthorizationInfo.toString()会打印所有权限字符串,可能泄露敏感信息日志文件里会出现["sys:user:add", "sys:user:delete"],被运维或安全团队视为高危检查logback-spring.xml,确认org.apache.shiro的日志级别为INFO

最后分享一个小技巧:在application.properties中加一行logging.level.org.apache.shiro=INFO,然后在doGetAuthorizationInfo方法里加一句log.debug("Loaded auth info for user: {}", username);。上线后,你就能在日志中清晰看到每个用户的权限加载时间、是否命中缓存、加载了哪些权限。这比任何APM工具都直观,也是我们定位“为什么用户A没权限”的第一手证据。毕竟,再复杂的系统,真相永远藏在日志里,而不是在猜想中。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 1:02:41

M4C-Hat NPU扩展板实战:Pulsar2工具链与边缘推理性能测试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:02:34

Netty和MQTT不是二选一:物联网设备接入协议选型实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:01:53

单台卖二十多亿还要排队两年,这家荷兰工厂却在欧洲一台卖不动

单台卖二十多亿还要排队两年&#xff0c;这家荷兰工厂却在欧洲一台卖不动 如果你去问英伟达或者AMD的工程师&#xff0c;眼下算力最强的处理器是怎么造出来的&#xff0c;他们会告诉你&#xff0c;图纸再漂亮&#xff0c;最终都得送到台积电或三星的无尘车间里。但如果你走进那…

作者头像 李华
网站建设 2026/10/14 1:00:01

058、晶振频偏超出容限后的连接维持问题

一个差点让整批设备返工的凌晨现场 去年冬天&#xff0c;一个做无线传感采集的项目进入小批量试产阶段。设备装在现场运行了大约两周&#xff0c;陆续有十几台出现同一个症状&#xff1a;白天通信正常&#xff0c;到了后半夜或者环境温度降到零下之后&#xff0c;设备会从网关上…

作者头像 李华
网站建设 2026/10/14 0:58:38

无需环境配置,Codex 一键安装包,零基础搭建本地 AI 工作流

&#x1f50d;前言 本文介绍 Codex Harness 一键安装包的部署方案&#xff0c;无需解压&#xff0c;也无需繁琐的环境配置&#xff0c;新手也能快速上手。该方案同时支持 Windows 与 macOS 双平台&#xff0c;跟随向导几步即可完成安装、登录与模型密钥配置。文中完整演示了技…

作者头像 李华
网站建设 2026/10/14 0:58:31

python大作业.zip全拆解:爬虫+数据清洗+可视化完整项目实战

简介&#xff1a;一份围绕Python爬虫实战的课程大作业压缩包&#xff0c;面向已掌握基础语法、需要完成综合编程练习或希望入门网络数据抓取的学生&#xff0c;内容覆盖从HTTP/HTTPS协议理解、requests请求发送&#xff0c;到BeautifulSoup页面解析、正则匹配、数据存储与异常处…

作者头像 李华