做后台管理系统的人,十有八九会被权限模块折磨过。这次要说的这个项目,就是用SpringBoot和Shiro做了套细粒度动态权限管理系统,核心目标是把权限配置从代码里搬出来,塞进数据库,做到URL级的接口拦截、按钮级的前端联动,以及权限变更后不重启就能生效。
我先把这项目的“解痛”过程讲清楚,再一步步拆表结构、Shiro集成、动态Filter链、缓存刷新和几个折腾了很久的坑。文章里涉及到的代码和配置都是从实际项目里摘出来的,做过一点脱敏整理,可以直接参考着改。
1. 项目概述与整体设计思路
1.1 细粒度动态权限到底解决什么问题
传统做法里,权限控制基本是这么玩儿的:在ShiroFilterFactoryBean里写死一堆路径规则,比如/admin/** = roles["admin"],新加一个接口就改一次配置,重新编译、重启服务。这种方式在小项目里勉强能用,一旦上了规模就暴露问题:
- 权限配置散落在代码里。产品提了个“给运营加个临时权限”的需求,你都得去翻ShiroConfig,改完还得发版,等半天才能生效,这体验太差了。
- 到不了按钮级。普通用户虽然进不了用户管理页面,但前端的“新增用户”按钮还是能看见,点进去发现被接口拦截才报错。体验不好是一回事,前端这边还得对每个按钮做一遍判断,逻辑散得到处都是。
- 权限变更不够“动态”。权限改了,老用户session里还是旧的角色信息,除非重新登录,否则改等于白改。
我这个项目要做的,就是一套围绕“配置化”和“即时生效”设计的权限系统。细粒度指的不只是能进哪个菜单,而是能操作哪个按钮、能调哪个接口;动态则指权限数据全部来自数据库,后台改完持久化之后,通过缓存刷新机制让授权信息在较短时间内生效。
1.2 技术选型:SpringBoot + Shiro 是怎么定下来的
选SpringBoot没什么悬念,它的自动配置极大简化了项目搭建。不过这里有一个搜索频率很高的问题:SpringBoot版本太高会不会有坑?确实有。SpringBoot 2.6之后的路径匹配策略默认变成了PathPatternParser,而Shiro那一套还在用AntPathMatcher,两者不匹配就会导致你配置的Filter链根本不生效。这个我在后面问题排查部分会专门讲,先记住结论:项目里把spring.mvc.pathmatch.matching-strategy显式配成ant_path_matcher,或者用兼容版本。
Shiro和Spring Security的选择,每次都能吵半天。我的看法是:如果项目就是中小型后台、核心诉求是快速完成认证+授权,Shiro的学习成本和配置复杂度都友好得多。Spring Security功能更全、社区更大,但它那套过滤器链和配置方式,团队不熟悉的话前期成本会明显偏高。Shiro的Realm机制、注解式授权和Session管理,做动态权限改造时改造成本也小。如果团队本身就是Security熟练工,那用Security没毛病;但就这个项目而言,SpringBoot+Shiro是性价比更高的选择。
1.3 整体架构与一次完整请求的流转过程
整个系统大致分三层:
- 表现层:前端页面和接口,登录成功后拿到用户权限码集合,用于渲染菜单和按钮。
- 权限核心层:Shiro负责认证和授权,自定义Realm处理登录和权限查询,动态Filter负责URL级权限校验。
- 数据层:MySQL存用户、角色、菜单、权限点等基础数据,Redis或本地缓存承担权限数据的读缓存。
一次请求的流转大致是这样:
请求进来 -> Shiro过滤器链 -> 登录认证过滤(确认身份) -> 动态URL权限过滤器(匹配当前请求所需权限码) -> 进入Controller -> 方法级注解二次校验(可选) -> 返回结果这里有一个容易忽略的点:Shiro里的Filter链是顺序匹配的,匹配到第一个合适的规则之后,后面的规则就不会再管了。这个特性在配置动态权限时尤其重要,后面“路径顺序坑”会展开讲。
项目目录结构也顺便理一下,权限这块集中在几个包里:
com.demo.permission ├── config # ShiroConfig、RedisConfig等 ├── controller # 登录、用户、角色、菜单管理等 ├── service # 业务逻辑,权限变更、缓存刷新 ├── mapper # MyBatis-Plus的Mapper层 ├── entity # 用户、角色、菜单、权限实体 ├── filter # 自定义动态授权过滤器 ├── realm # UserRealm,认证+授权数据源 └── common # 统一返回、异常处理、工具类2. 权限模型与数据库表设计
2.1 六张核心表的结构思路
权限系统的基础是数据模型。这套系统用的是经典的RBAC模型:用户关联角色,角色关联权限。一共设计了六张核心表:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| sys_user | 用户表 | id, username, password, status |
| sys_role | 角色表 | id, role_code, role_name, status |
| sys_menu | 菜单/资源表 | id, parent_id, menu_name, url, perm_code, type |
| sys_user_role | 用户角色关联表 | user_id, role_id |
| sys_role_menu | 角色权限关联表 | role_id, menu_id |
| sys_perm_api | 接口权限表 | id, url, method, perm_code |
sys_menu属于复合表,它既承载了左侧菜单树的结构信息(parent_id层级关系),也承载了操作按钮的权限点信息(type字段区分目录、菜单、按钮)。sys_perm_api单独拆出来,是因为一个接口可能对应多个权限点,方便做URL和权限码的映射,这个映射表就是动态Filter链的数据来源。
注意一点:接口权限和页面按钮权限其实是两种维度。接口权限决定了这个请求能不能调到后端;按钮权限只决定前端这个按钮显不显示。所以我把按钮权限点在sys_menu里用perm_code记录,把接口和权限的对应关系放在sys_perm_api里,这样职责清晰。
2.2 权限标识符设计:Shiro通配符的实战用法
Shiro的权限判断支持通配符,比如sys:user:add这种三段式写法。冒号分隔的每一段可以是具体值,也可以用*表示全部。具体的权限检查规则是:如果用户拥有sys:user:*,那么sys:user:add、sys:user:edit这些子权限他都有。听起来方便,实际用的时候有个坑:sys:user和sys:user:*在Shiro里不是等同的,你给用户授权了sys:user,他调用subject.isPermitted("sys:user:add")还是false。所以设计权限码的时候要统一风格,要么全部用完整三段,要么规定清楚“父权限不含子权限”这一事实,免得授权时理解不一致。
我的习惯是这么设计权限码:
sys:user:add——新增用户sys:user:edit——编辑用户sys:role:assign——给角色分配权限dashboard:view——查看首页数据
权限码本身是字符串,不参与父子继承判断。真正控制继承逻辑的是数据模型:角色关联了哪些权限码,用户就拥有哪些权限码。不搞模糊的*泛化授权,全部精确到操作,这样排查问题的时候一清二楚。
2.3 用户、角色、权限的关联逻辑与授权策略
用户和角色的关联,我采用的是“用户可能有多个角色”的策略。用户在真实业务中往往不止一个身份,比如一个用户既是普通运营,又是某个业务线的管理员,如果只允许单角色,就得去创建一堆“运营兼管理员”的组合角色,角色数量直接爆炸。多角色方案下,一个用户拥有的权限码就是所有角色权限码的并集。
授权策略上不要直接把用户和权限点绑定,而是让角色作为中间层。角色可以复用,权限变更只需要改角色关联的菜单/权限点,不需要挨个用户去操作。用户量大的时候这个优势会非常明显。
另外一个实操细节:删除角色或者修改权限点的时候,一定要先检查是否有用户关联这个角色。我在项目里加了一个约束:分配了用户的角色不允许直接删除,必须先解除用户关联,防止历史数据直接变成孤儿数据。
3. Shiro核心集成与动态Filter链实现
3.1 Shiro四大核心组件先理清楚
动手写代码之前,四个东西必须弄明白:Subject、SecurityManager、Realm、SessionManager。
- Subject:当前用户的操作主体,实际上是一个安全视图,你可以通过它获取用户身份、判断权限。
- SecurityManager:Shiro内部的总调度器,所有安全操作最终都汇聚到它这里。
- Realm:数据源桥接层。Shiro不认识你的用户表,你得在Realm里告诉它怎么查询用户、怎么加载用户的角色和权限。
- SessionManager:负责Session的创建、维护和销毁。
这套系统的核心逻辑集中在Realm里。自定义Realm继承AuthorizingRealm,重写两个方法:doGetAuthenticationInfo负责登录时校验账号密码,doGetAuthorizationInfo负责给当前用户装配角色和权限码。权限码在登录成功后的第一次授权时加载。
3.2 SpringBoot中注册Shiro配置类
SpringBoot的优势在于自动配置,我们自己需要做的,是把Shiro的几个核心对象声明成Bean,让Spring容器管理它们。
@Configuration public class ShiroConfig { @Bean public UserRealm userRealm() { UserRealm realm = new UserRealm(); // 缓存授权信息,避免每次请求都重新查数据库 realm.setCachingEnabled(true); realm.setAuthorizationCachingEnabled(true); realm.setAuthorizationCacheName("authorizationCache"); return realm; } @Bean public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(userRealm()); return securityManager; } @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(securityManager()); factoryBean.setLoginUrl("/login"); factoryBean.setUnauthorizedUrl("/403"); // 关键:注册自定义动态权限过滤器 factoryBean.getFilters().put("dynamicPerms", dynamicAccessFilter()); // 关键:从数据库加载URL和权限规则的映射 factoryBean.setFilterChainDefinitionMap(loadFilterChainDefinitions()); return factoryBean; } }这里面有SpringBoot自动装配发挥重要作用的点:ShiroFilterFactoryBean被声明成Bean之后,Spring容器会在启动时自动把它注册到Servlet容器中,拦截所有请求。不需要XML配置,不需要手动FilterRegistrationBean,这个机制就是SpringBoot“约定优先于配置”的体现。
3.3 从数据库加载动态Filter链的关键代码
普通的Shiro项目里,filterChainDefinitionMap是这么写的:
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/logout", "logout"); filterChainDefinitionMap.put("/static/**", "anon"); filterChainDefinitionMap.put("/**", "user");静态配置的问题就不用我多说了。动态方案的核心是:每次服务启动或者权限刷新时,从sys_perm_api表里查出所有需要权限控制的URL,拼装成Shiro的filter规则字符串。
public Map<String, String> loadFilterChainDefinitions() { Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); // 不需要登录就能访问的路径 filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/logout", "logout"); filterChainDefinitionMap.put("/static/**", "anon"); filterChainDefinitionMap.put("/error", "anon"); // 从数据库加载权限URL规则 List<SysPermApi> apiPerms = permApiService.listAll(); for (SysPermApi item : apiPerms) { String path = item.getUrl(); String method = item.getMethod(); String permCode = item.getPermCode(); // 自定义过滤器:根据URL识别出需要的权限码,再判断当前用户是否有该权限 filterChainDefinitionMap.put(path, "dynamicPerms[" + method + ":" + permCode + "]"); } // 兜底规则:其余请求必须登录 filterChainDefinitionMap.put("/**", "user"); return filterChainDefinitionMap; }这里要解释一下dynamicPerms[...]这种形式。Shiro的filter规则其实支持参数传递,factoryBean.getFilters().put("dynamicPerms", filter)注册一个过滤器之后,规则里写dynamicPerms[参数]就能把参数传给这个过滤器实例。我在自定义过滤器里获得这些参数后,再去判断当前用户是否满足对应权限。这就实现了“配置在库里,规则动态加载”。
3.4 动态URL匹配的路径顺序坑
这是个非常容易踩的坑。Shiro的路径匹配是按filterChainDefinitionMap的插入顺序从上往下匹配的,一旦某个规则先匹配到了,后面的规则全部失效。用LinkedHashMap就是为了维持插入顺序,但很多人会忽略这一点。
举个实际出问题的案例。我把/sys/user/**这条规则放在了/**的下面,结果访问用户管理接口的时候,先匹配到了/**,Shiro直接要求登录,根本走不到具体的URL权限规则。排查了半天才发现是顺序问题。
正确的顺序安排是:
- 放行规则(anon、logout):登录页、静态资源、验证码接口等。
- 具体业务URL权限规则:越具体的路径越靠前。
- 兜底规则(
/**= user):放最后。
还有另一个相关的小问题:如果某个URL在sys_perm_api表里配置了权限规则,但前端实际请求路径和表里的url字段有一个字符的偏差,这条规则就匹配不上了,落到了兜底的/** = user上,结果只是要求了登录,没做权限校验。看起来接口“白名单化”了,实际上权限完全失效。排查这类问题的办法很简单,在自定义Filter里打印一下当前请求地址和匹配到的规则,看一眼就知道了。
4. 细粒度权限的落地实现
4.1 URL、方法注解、前端按钮三层方案
现在可以聊细粒度权限的落地了。我把它分了三层来实现:
第一层是URL级拦截,由自定义动态权限Filter完成。这一层决定了用户能不能访问某个接口,是权限控制的主防线。
第二层是方法级注解,在Service的敏感操作上加Shiro的@RequiresPermissions(value = {"sys:user:delete"}, logical = Logical.OR)注解。为什么有了URL拦截还要方法注解?因为同一个URL可能被多个方法复用,或者有些内部的批量操作不是直接走Controller入口的。方法注解相当于第二道门,防止有人绕过第一道门直接调用内部方法。
第三层是前端按钮级控制,登录后把用户的所有权限码返回给前端,前端根据权限码决定按钮和菜单的显隐,给用户的操作体验做一个提醒,告诉他哪些功能他不该点。注意,第三层只是体验优化,安全校验必须靠第一层和第二层。
这三者的关系可以用一个表格概括:
| 层级 | 实现方式 | 控制粒度 | 安全性 | 主要作用 |
|---|---|---|---|---|
| URL级 | 自定义AccessControlFilter | 接口 | 高 | 主防线,拦截所有请求 |
| 方法级 | @RequiresPermissions注解 | 操作方法 | 高 | 二次校验,保护内部细节 |
| 前端级 | 权限码Set做渲染判断 | 按钮/菜单 | 低 | 体验优化,不让用户瞎点 |
4.2 自定义动态授权过滤器详解
自定义过滤器的关键代码在这里:
public class DynamicAccessFilter extends AccessControlFilter { private static final Logger log = LoggerFactory.getLogger(DynamicAccessFilter.class); @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { HttpServletRequest httpRequest = (HttpServletRequest) request; // mappedValue就是filter规则里的参数,形如"GET:sys:user:add" if (mappedValue == null) { return true; } String[] args = mappedValue.toString().split(":"); if (args.length != 3) { return true; } String method = args[0]; String permCode = args[1] + ":" + args[2]; // 简单校验请求方式,防止POST请求用了GET的权限 if (!method.equalsIgnoreCase(httpRequest.getMethod())) { return false; } Subject subject = getSubject(request, response); if (subject == null || !subject.isAuthenticated()) { return false; } // 如果用户没有这个权限码,则返回false,进入onAccessDenied处理 return subject.isPermitted(permCode); } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { // 未登录跳登录页,已登录但没权限则跳403 Subject subject = getSubject(request, response); if (subject == null || !subject.isAuthenticated()) { WebUtils.issueRedirect(request, response, "/login"); } else { WebUtils.issueRedirect(request, response, "/403"); } return false; } }核心逻辑都在isAccessAllowed里,就是“当前请求需要的权限码,和当前用户的权限码集合有没有交集”。有了Shiro的缓存机制,subject.isPermitted走的是授权缓存,不会每次都查库,所以性能上没有明显压力。
这里有个细节容易搞混:Shiro的isPermitted判断的是当前Subject是否拥有指定权限,而过滤器拦截的是“这个URL需要什么权限”。所以要在sys_perm_api表里配置好“URL对应哪个权限码”,才能让这两个概念对上。
4.3 按钮权限的前端联动做法
这套系统如果配合前端页面,按钮权限的实现其实很简单。登录成功之后,后端把当前用户的所有权限码返回给前端:
public LoginResult doLogin(String username, String password) { // 登录验证逻辑... Subject subject = SecurityUtils.getSubject(); Set<String> permCodes = new HashSet<>(); for (String roleCode : userService.getUserRoleCodes(username)) { permCodes.addAll(userService.getPermCodesByRoleCode(roleCode)); } // 回传给前端,前端保存后用于渲染判断 }前端拿到权限码之后,封装一个hasPerm方法:
function hasPerm(permCode) { const userPerms = store.getters.permCodes; // 全局状态里保存的权限码集合 return userPerms.includes(permCode); }然后模板里就可以这么写:
<el-button v-if="hasPerm('sys:user:add')" type="primary">新增用户</el-button> <el-button v-else type="primary" disabled>无权限</el-button>菜单和路由的过滤也是同样的思路,权限码集合和菜单表返回的url逐条比对,生成当前用户可见的菜单树。不要在前端把逻辑搞得太复杂,就是一个Set的去重和判存,真正的新增、删除操作,后端接口该拦还是会拦的。
5. 动态权限刷新与缓存优化
5.1 权限数据缓存设计
权限数据的特点是读多写少,用户访问每个接口都要做权限校验,如果每次都去数据库查,数据库压力会很大。所以缓存是必须的。
我在这套系统里做了两层缓存:
- 权限配置缓存:URL与权限码的映射表,也就是sys_perm_api查出来的数据,用Map存在本地缓存里(也可以放到Redis)。这个Map在每次权限配置变更时会主动刷新。
- 用户授权缓存:Shiro的AuthorizationCache。用户登录后第一次做权限判断时,Realm从数据库查出该用户的全部权限码,放进缓存。之后同一用户的权限判断直接走缓存。
Shiro的AuthorizationCache默认可以用内存缓存,如果项目已经用了Redis,建议把CacheManager切到Redis,好处是服务重启后授权缓存还在,用户不会因为缓存丢失被打回未授权状态。这里需要注意序列化方式,Shiro默认的序列化在Redis里可能存储成奇怪的二进制,建议配置JSON序列化器和适当的TTL。
5.2 权限版本号与即时生效机制
动态权限最大的价值是“配置改了马上生效”,但Shiro的授权缓存会把旧的权限码保存下来。如果某个用户被取消了某个角色,他的AuthorizationCache里还有旧权限,除非重新登录,否则他依然能访问被撤销的接口。这个冲突是很多权限系统做动态化时翻车最多的地方。
我的方案是引入权限版本号。
系统里维护一个全局的版本号字段,每次管理员修改了任何角色和权限的关系,就把这个版本号加1。用户的授权信息存入缓存时,在缓存key里带上当前版本号;每次请求做权限判断时,先对比当前全局版本号和缓存key里的版本号,如果不一致,说明权限数据已经更新,就清掉该用户的授权缓存,让它重新从数据库加载最新权限。
实现思路很清晰,关键代码就这部分:
public void refreshPermissionCache() { Long newVersion = permissionVersionService.incrementAndGet(); // 清除所有用户的授权缓存 Cache<Object, Object> cache = cacheManager.getCache("authorizationCache"); if (cache != null) { cache.clear(); } // 同时刷新URL权限映射缓存 urlPermsCache.clear(); log.info("权限缓存已刷新,版本号:{}", newVersion); }这里有一个取舍:如果清除所有用户的授权缓存,在高并发场景下会出现短暂的大量缓存重建,数据库压力会瞬间上去。如果想要更平滑,可以把权限变更的影响范围缩小到单个用户,但实现复杂度会明显提高。对于中小型后台系统,全量清缓存是完全够用的。
5.3 定时任务兜底刷新
权限版本号机制解决了权限变更生效的问题,但还有个边界情况:如果缓存数据因为某种原因不一致了(比如修改权限之后接口刷新失败),用户看到的还是旧权限。为了兜底,我加了SpringBoot的定时任务,每天凌晨清理一次授权缓存,强制所有用户重新从数据库加载权限,算是给动态权限机制补了一层保险。
@Component @Slf4j public class PermissionCacheTask { @Resource private CacheManager cacheManager; @Scheduled(cron = "0 0 3 * * ?") public void clearAuthorizationCache() { Cache<Object, Object> cache = cacheManager.getCache("authorizationCache"); if (cache != null) { cache.clear(); log.info("定时任务:授权缓存已全部清理"); } } }这个定时任务不需要太频繁,一天一次就够了,因为正常流程中权限变更已经用了版本号机制去即时生效。定时清理只是处理意外情况,比如权限刷新的程序挂掉了、漏掉了一部分用户的缓存更新。
6. 常见问题与排查技巧实录
6.1 SpringBoot高版本与Shiro的路径匹配机制冲突
这是我会把“SpringBoot版本太高”相关热词拎出来说的原因。某次我在SpringBoot 2.7.x的项目里集成Shiro,配置好之后发现所有Filter规则都没有生效,接口可以随意访问。查了很久才发现,SpringBoot 2.6开始默认使用PathPatternParser匹配路径,而Shiro的URL规则是基于AntPathMatcher的。两边都认为自己匹配到了,但实际上规则压根没进入Shiro的过滤器链。
解决方案是把MVC的路径匹配策略改回Ant:
spring: mvc: pathmatch: matching-strategy: ant_path_matcher如果项目已经用到了PathPatternParser的一些高级特性,那就要谨慎切换。更稳妥的方案是把SpringBoot版本降到与Shiro兼容的版本,或者升级Shiro到1.10以上。我的建议是:不要盲目追求最新版本,SpringBoot和Shiro这种框架类组件,选一个经过验证的稳定组合,比什么都强。
6.2 rememberMe导致的权限更新不生效
Shiro的rememberMe功能延迟生效问题,在权限系统里很容易被忽略。用户登录时勾选了“记住我”,他的主体标识会序列化到Cookie里。之后每次请求,Shiro可以从Cookie中恢复用户身份,但恢复出来的授权信息可能是旧的。管理员修改了这个用户的角色权限之后,因为rememberMe的存在,用户下一次请求瞬间又带着旧权限回来了。
这里我的处理方式是:rememberMe只负责“记住我是谁”,不负责“记住我能干什么”。在Realm的doGetAuthorizationInfo里永远不做缓存绕过,同时把权限版本号机制放到过滤器最前面,一旦检测到版本号变化就直接踢掉该用户的记住我状态,强制重新登录。
如果你不希望用户因为权限变更被强制下线,那就在后端判断一下:如果用户当前权限因为版本号变化被刷新了,就重定向到登录页即可。这个体验上有点粗暴,但安全上没话说。
6.3 全部路径被拦截成404的排查过程
有一次测试反馈,说系统突然有一批页面打不开了,登录之后跳转到一个没有任何内容的空白页。我先看了后端日志,发现请求根本没有进入Controller,再看了网络请求状态,全部是401或404。
排查下来发现是filterChainDefinitionMap的加载逻辑出了问题:数据库里sys_perm_api有一批废物数据,某个URL写错了前缀,导致所有请求都没匹配到具体规则,整体掉进了默认的/** = user分支。理论上这条兜底规则对应的登录检查应该放行已经登录的人,但Shiro的登录检查判断的是当前请求是否携带有效身份,如果rememberMe的逻辑再出一丁点问题,就全拦住了。
这种问题的排查思路是:先把Filter链和实际配置打印出来,对照数据库数据逐条看。我在自定义Filter里加了个日志级别为DEBUG的打印,专门输出当前请求URI、匹配到的规则、当前用户身份。排查效率能提高很多。
6.4 数据权限的边界
“细粒度动态权限”会被很多人误以为可以控制行级数据权限,比如“运营只能看自己创建的订单”。这个需求其实不在本系统的范围内。我的实践原则是:按钮级和URL级权限寿险控制操作入口,数据级权限要单独设计。
如果硬把数据权限塞进Shiro的权限码体系里,很快会失控。比如你想表达“用户A能看到订单列表中属于他部门的记录”,靠权限码是做不到的,必须在SQL层做数据过滤,考虑MyBatis的拦截器或专门的租户隔离方案。这个项目到按钮级就足够了,再往下做要单独设计,别混在一起写。
6.5 问题排查速查表
| 现象 | 可能原因 | 排查/解决方案 |
|---|---|---|
| 配置的Filter规则全不生效 | SpringBoot高版本的PathPatternParser与AntPathMatcher冲突 | 配置matching-strategy为ant_path_matcher或升级Shiro |
| 权限改了,老用户还能访问旧功能 | Shiro授权缓存或者rememberMe保留了旧数据 | 用权限版本号机制清理AuthorizationCache |
| 请求命中不了具体的URL规则 | 路径顺序放错,或数据库url字段与请求不匹配 | 调整LinkedHashMap的插入顺序,具体路径在前 |
| 页面按钮权限和接口权限不一致 | 前端权限码集合没有连同版本号一起更新 | 编写权限码与按钮的对应关系时,出了单据要全查一遍 |
| 高并发时权限校验变慢 | 授权缓存被全量清空,大量请求同时重建缓存 | 缓存清理接口加锁,或改为按用户纬度刷新 |
还有一个值得养成的习惯:权限变更操作要记录审计日志。谁改了哪个角色的权限、加了哪个URL规则、清空过缓存,都要在sys_perm_log表里留一条流水。不然线上出了“权限神秘失效”的问题,连排查的依据都没有,只能靠猜。
写在最后的一点实际体会
这套系统做完上线之后,回过头看,最大的收获不是“用Shiro做了个权限管理”这么简单,而是把权限问题的处理思路从“改代码”变成了“改数据”。产品经理自己就能去配置一个临时角色,不用再等一个发版周期,这种运营效率的提升才是动态权限真正值钱的地方。
不过我也得说句实话:动态化不是银弹。权限全部扒拉到数据库之后,出现了新的问题,比如换了个开发看权限规则,他直接去数据库改了一行,连Shiro的Filter链怎么组成都没理解,结果把URL匹配顺序搞乱。后来我补充了两层约束:一是重要权限配置只在管理后台操作,数据库直接改作为兜底;二是权限表加触发器或API层的校验逻辑,不允许URL和权限配置出现“孤儿数据”。
另外再提一个小建议:如果要在现有系统上做类似改造,控制好权限模块的边界,别一上来就想着把数据权限、字段权限全部塞进去,先把URL和按钮控制做扎实,跑通一版再迭代,比一开始就设计一个巨复杂的模型要靠谱得多。
技术选型上,Shiro并不是最时髦的方案,但它的轻量和对动态权限改造的友好度,让这个项目落地得很顺畅。如果是一个新项目,团队也熟悉Spring Security,那选它也没问题;但如果你是想在现有SpringBoot项目里快速加一套可靠、不重、可动态配置的权限机制,SpringBoot+Shiro这个组合依然值得一试。