2026/10/12 8:28:59

基于RBAC的通用权限系统设计:风汐通用管理系统实践

基于RBAC的通用权限系统设计:风汐通用管理系统实践 去年有段时间我同时维护着三个内部业务系统越做越觉得不对劲每个系统的登录逻辑大同小异菜单权限、按钮控制、角色分配这些东西几乎每开一个新项目都要重新来一遍代码复制过去之后还要改表改字段改接口真正业务功能还没开始写光权限底座就折腾掉两三天。后来我下定决心把所有项目里的权限需求全部抽出来重新设计一套独立的通用权限管理系统也就是现在的“风汐通用管理系统”。这套系统核心采用经典的 RBAC基于角色的访问控制模型把用户、角色、菜单、权限点这四类核心数据全部抽象成可配置的基础模块前端动态路由、后端接口鉴权、数据范围控制、操作日志这些能力尽量沉淀为通用件让新项目接入时只需要做初始化配置不需要再怎么写权限代码。这篇文章我想把“风汐”在设计上和技术实现上的核心思路完整记录下来包括数据库模型怎么设计、认证流程怎么走、按钮级权限和数据权限分别怎么落地还有我在实际开发过程中踩过的一些坑和排查经验。适合正在开发权限模块的后端同学、想独立搭建后台管理系统的全栈工程师以及准备从零设计 RBAC 权限体系、但不希望走太多弯路的人参考。1. 项目背景与核心需求拆解1.1 为什么一定要做一套独立的通用权限系统先说结论通用权限系统真正的价值不是“省掉登录页”而是把权限体系里最容易被各项目改乱的部分从业务系统里彻底剥离出来。我之前维护的几个系统权限实现路径完全不同。有的是自己写拦截器判断 session 里的角色名有的用 Shiro 配一堆 filterChain最新那个项目用的 Spring Security 但注解用法五花八门。表面上看功能都跑得通但实际上每一个系统的权限判断都有各自的隐藏逻辑有人在 Controller 里硬编码了 admin 判断有人在前端菜单里写死了路径还有一个项目的删除按钮只在前端做了隐藏后端接口居然没有任何权限校验。这种系统稍微长大一点权限就是一笔糊涂账改需求的时候谁都不敢碰。继续往下分析我整理出了这类系统中常见的权限需求清单用户登录后能看到哪些菜单访问哪些页面页面里的新增、编辑、删除按钮哪些人能点哪些人看不到不同角色查询数据时能看到哪些范围比如只能看本部门的数据还是能看全公司超管和普通用户的权限边界例如是否需要绕过所有限制权限变更后需要多快生效是用户重新登录生效还是改完立刻生效。这些需求真实且高频。如果每个项目都从零实现等于每个项目都要重新思考一遍“用户和角色怎么关联、菜单和权限怎么绑定、前端怎么拿路由、后端怎么校验接口”本质上是在浪费时间。如果单独抽一套系统把这些能力做成默认自带、可配置、可扩展那后续接项目就变成了“接入登录态、注册路由、配置菜单、声明数据权限”可以省掉大量重复设计。1.2 技术定位与普适性设计“通用”这两个字说起来容易做起来非常难。我给自己定的原则是业务系统不该碰的东西风汐绝对不碰业务系统必须依赖的东西风汐要提供完整能力。具体落到设计上我做了几个关键决定权限模型采用 RBAC0支持向 RBAC1 扩展。角色不搞复杂的层级继承因为绝大多数内部管理系统的真实场景中扁平角色已经够用角色多了反而增加管理成本。菜单和权限点统一建模。菜单目录、页面菜单、按钮权限全部放在同一张表里用类型字段区分通过父子关系形成树。这样做的好处是前端菜单树和后端权限标识来源统一不用维护两套数据。认证方式选 JWT Redis 双轨制。JWT 做无状态认证Redis 存用户的权限标识集合、在线会话和控制失效时间。这样既有 JWT 的便捷性又能解决“权限改了还没生效”的痛点。核心功能全部做成可配置、可插拔。比如数据权限支持“全部数据”“本部门及以下”“仅本部门”“仅本人”四种范围不做死业务规则。不绑定具体技术栈的前端。后端只输出菜单 JSON 结构前端拿到之后自行构建动态路由。也就是说将来换一套前端框架不影响后端权限体系。这个组合方案不是我拍脑袋定的而是经过了几轮取舍。最初我也想过直接用 ABAC基于属性的访问控制把部门、岗位、职级、数据归属全部做成动态属性理论上更强大但落地后发现问题很多规则配置界面复杂、业务人员看不懂、调试权限要翻一堆策略对多数管理系统来说属于过度设计。RBAC 在可维护性和表达力之间是最均衡的选择。1.3 系统边界与功能范围风汐通用管理系统对外提供的能力包括用户管理新增、禁用、重置密码、分配角色角色管理创建角色、分配菜单权限、配置数据范围菜单管理维护目录、菜单、按钮三级结构配置路由地址和权限标识部门管理维护组织架构树供数据权限和用户归属使用数据字典维护常用的状态值、类型值方便业务系统下拉框使用操作日志记录关键操作包括操作人、操作类型、请求路径、请求参数和结果在线用户与强制下线用于查看当前登录会话、踢人下线。这套边界下来系统会保持一种很舒服的克制感。权限系统就是权限系统不去越界做 CMS 的内容管理、也不强行塞审批流这样它才能在不同业务系统之间反复复用。2. 技术选型与总体架构设计2.1 后端技术栈选型与理由先直接列我选定的技术栈Java 17 Spring Boot 2.7.x Spring SecurityMyBatis-Plus 3.5.x代码生成和分页查询靠它省不少事Redis 6.x用于缓存权限与登录会话MySQL 8.x存储业务数据Knife4j 接口文档方便前端联调Maven 多模块工程将 framework公共能力、system权限模块、business-demo示例业务分层隔离可能有人会问权限判断用 Shiro 不是更轻量吗为什么用 Spring Security我的真实理由是风汐要面对未来各种新项目接入而新项目大概率会用 Spring Boot/Spring Cloud 这一套体系Spring Security 是生态内默认的安全标准和 Spring 的过滤器链、注解机制、事件机制结合得最自然。Shiro 虽然轻但遇到复杂过滤链、分布式会话、和 Spring Security 混用的情况时容易出问题。一套通用组件要尽量和主流技术栈同频不能为了轻量牺牲长期兼容性。Spring Security 本身学习曲线偏陡所以我在项目中做了一个重要的简化处理不开启它那套复杂的 HttpBasic 和表单登录只使用它的过滤器链机制加上自定义的 JWT 认证过滤器把用户信息从 Redis 加载后填入 SecurityContext。业务接口的权限校验用自定义注解 AOP 来实现尽可能让团队里的人看得懂、改得动。2.2 认证流程与登录态设计风汐的登录认证链路如下用户提交账号、密码和验证码后端校验验证码然后用 BCrypt 校验密码校验通过后生成 JWT token并把它写入 RedisRedis 的 key 是用户 ID 加一个随机会话标识value 里保存用户基本信息、角色编码列表、权限标识集合和 token 过期时间前端把 token 存到本地在请求拦截器里放到 Authorization 请求头后端的 JWT 认证过滤器解析 token判断 Redis 里对应会话是否还存在如果存在则放行。为什么要 JWT 和 Redis 各存一份因为 JWT 本身是自包含的很难单独把它强制失效。如果权限变更后还想让旧 token 立刻失效没有 Redis 辅助很不好做。把会话写入 Redis 之后只需要在权限变更时删掉对应用户的 Redis key用户下一次请求就会收到“登录已失效”的提示需要重新登录或者重新拉取权限实现了改完权限立刻生效的效果。关于 token 过期时间我默认设置 30 分钟Redis 里也设置同样时长的过期时间。用户每次主动请求时会滑动续期如果 Redis 里的值过期了JWT 就算没过期也会被拒绝。这种做法兼顾了安全性和体验。2.3 前后端交互与动态菜单方案前端我选的 Vue 3 Vite Pinia Vue Router Element Plus。权限控制上最关键的是动态路由生成后端返回当前用户可访问的菜单树包含路由路径、组件路径、菜单标题和图标前端在 Pinia 中保存这份菜单数据用户登录后前端先跳转到一个空白加载页在这个页面里把后端返回的菜单树转换成路由表用 router.addRoute 逐个注册注册完之后再跳转到目标页面路由守卫会在每一次跳转前判断如果尚未拉取路由则强制走一次“拉取菜单并注册路由”的流程。动态菜单有一个需要特别提防的点直接访问一个没有注册过的 URL 时如果路由表里没有匹配项Vue Router 会落到通配符路由一般会跳 404 页面。但系统里的菜单是后端配置的合法的页面路径不应该出现在前端源码里所以这个机制是安全的。为了不让“刷新页面路由丢失”成为问题我还在每次刷新后从后端重新拉取菜单。代价是多一次请求但换来了权限数据的强一致性这个取舍我认为是值得的。3. 数据库模型设计与 RBAC 核心表结构3.1 核心表设计风汐的权限模型基于经典的 RBAC 设计核心表一共五张用户表、角色表、菜单权限表、用户角色关联表、角色菜单关联表。再加上部门表、操作日志表、数据字典表整个权限域的表结构大致如下。表名用途关键字段sys_user用户表id、dept_id、username、password、nickname、statussys_role角色表id、role_name、role_key、role_sort、statussys_menu菜单权限表id、parent_id、menu_name、menu_type、perms、path、componentsys_user_role用户-角色关联表user_id、role_idsys_role_menu角色-菜单关联表role_id、menu_idsys_dept部门表id、parent_id、dept_name、ancestorssys_oper_log操作日志表id、user_id、module、oper_type、request_method、request_url、request_param、status其中 sys_menu 是权限模型里最关键的一张表。menu_type 字段决定了记录是目录还是菜单还是按钮。目录和菜单用来生成前端动态路由按钮记录则不生成路由只作为权限标识参与后端接口鉴权。例如一个按钮记录可以是INSERT INTO sys_menu (parent_id, menu_name, menu_type, perms, path, component) VALUES (101, 用户新增, F, system:user:add, NULL, NULL);perms 字段的命名我建议统一使用“模块:实体:动作”三段式风格例如 system:user:add、system:user:edit、system:role:delete。这种命名方式在菜单权限树里筛选时非常直观代码里也容易读明白。3.2 多对多关联的合理性用户和角色、角色和菜单都是多对多关系这在 RBAC 里是比较标准的做法。有些系统喜欢把角色表里直接加一个权限 ID 数组字段用逗号分隔存多个权限 ID这种设计在查询时会很痛苦数据库层面无法做高效的关联查询和去重而且权限变更时需要做字符串拆分拼接。宁可多建两张中间表也要保持关系结构清晰。用户和角色的关联表 sys_user_role我的设计是在唯一索引上设置 user_id role_id避免重复绑定。角色和菜单的关联表 sys_role_menu 则使用 role_id menu_id 做唯一索引。后续给角色分配权限时直接删除旧的关联记录再批量插入新的处于一个事务内即可。批量操作时千万要注意“先删后插”的先后顺序。先 delete 再 insert期间如果有用户的请求正在加载权限可能会短暂出现“查不到权限”的情况。所以我在这套系统里把角色权限变更的逻辑统一加上事务并在变更完成后发布一个 Redis 缓存删除事件把受影响的用户会话缓存一次性清掉尽量避免脏读取。3.3 数据范围与部门表设计RBAC 只解决了“功能权限”问题但现实中系统往往还需要限制“数据可见范围”比如“销售一部的主管只能看到自己部门和下级部门的数据”。这种情况单靠 RBAC 的菜单权限是覆盖不了的。风汐的做法是给角色表增加一个 data_scope 字段配合部门表实现数据权限控制。我在角色表里定义的数据范围data_scope 值含义SQL 拼接效果1全部数据不拼接额外条件2本部门及以下数据WHERE dept_id IN (本部门及所有子部门)3本部门数据WHERE dept_id 当前用户部门4仅本人数据WHERE create_by 当前用户ID5自定义数据还需要额外配置角色与部门的关联表部门表 sys_dept 里的 ancestors 字段可以很方便地查出所有子部门它的值是父级链路的 ID 拼接例如当前部门 ID 为 102ancestors 可能是 0,100,101,102。查询子部门时可以直接用 LIKE 匹配 ancestors不过大量数据时建议做缓存避免频繁扫表。数据权限的实现会在 SQL 层动态拼接条件这个我后面在核心模块实现部分详细展开。4. 核心模块实现与关键代码拆解4.1 基于 JWT 的认证过滤器实现风汐后端的认证环节没有直接用 Spring Security 的登录表单而是自己写了一个 JwtAuthenticationTokenFilter它在 Spring Security 过滤器链的最前面执行。核心逻辑大概是这样Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // 1. 从请求头获取 token String token request.getHeader(Authorization); if (StringUtils.isNotBlank(token) token.startsWith(Bearer )) { token token.substring(7); // 2. 解析 token获取用户 ID 和会话标识 Long userId JwtUtils.parseUserId(token); String sessionKey RedisKeyUtils.buildSessionKey(userId); // 3. 从 Redis 中读取会话对象 LoginUser loginUser redisService.getObject(sessionKey); if (loginUser ! null) { // 4. 把用户信息放入 SecurityContext后续业务可以直接获取 UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } chain.doFilter(request, response); }这里有一个非常关键的细节如果 Redis 里的 session 不存在JwtAuthenticationTokenFilter 不会直接拒绝请求而是让请求继续往下走等到真正访问需要认证的接口时由 Spring Security 的 AccessDeniedHandler 统一返回 401。这样做可以让后端很优雅地区分“未认证”和“已认证但无权限”两种情况避免把校验逻辑全部堆在过滤器里。4.2 自定义注解 RequiresPermission 实现按钮级权限Spring Security 自带的 PreAuthorize 也可以用但它需要写 SpEL 表达式比如 PreAuthorize(hasAuthority(system:user:add))表达式写错时排查成本比较高而且团队里不熟悉 SpEL 的人容易写规范不一致。风汐最终选择了自定义注解加 AOP 的方式用法更直白。自定义注解定义Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequiresPermission { String value(); // 权限标识例如 system:user:add }然后在切面里做校验Aspect Component public class PermissionAspect { Before(annotation(requiresPermission)) public void checkPermission(JoinPoint joinPoint, RequiresPermission requiresPermission) { // 获取当前登录用户 LoginUser loginUser SecurityUtils.getLoginUser(); // 判断是否为超级管理员 if (loginUser.getUser().getIsAdmin()) { return; } // 判断用户权限集合中是否包含所需权限 SetString perms loginUser.getPermissions(); if (!perms.contains(requiresPermission.value())) { throw new ServiceException(403, 无访问权限请联系管理员); } } }实际上风汐还包括一个 RequiresDataScope 注解方式类似但是会在切面中修改 MyBatis-Plus 的查询参数。按钮级权限接口的写法则变成RequiresPermission(system:user:add) PostMapping(/user) public R? addUser(RequestBody SysUser user) { return userService.addUser(user); }这个做法的用户体验非常好代码里一眼就能看出接口需要什么权限切面统一处理所有校验逻辑新来的同事也不用去学 SpEL 语法。4.3 菜单树与权限标识缓存方案系统登录成功后后端会把用户的所有权限标识集合加载到 Redis避免每次请求都去数据库查询角色和菜单关联。这个权限集合是经过合并去重的对应多个角色的权限并集。具体的数据存储会设计成两个部分第一个部分菜单树数据序列化成 JSON 后按用户维度缓存key 可设计为login_user:menus:{userId}用于前端动态路由第二个部分权限标识集合存储所有按钮权限编码key 为login_user:perms:{userId}用于后端接口鉴权。权限变更时具体怎么清缓存我总结了一套规则变更动作需要清除的缓存给角色分配菜单权限删除所有拥有该角色的用户的菜单缓存与权限缓存修改用户绑定的角色删除该用户的两个缓存新增或删除菜单权限点删除所有用户的菜单缓存与权限缓存禁用用户账号删除该用户缓存并强制其会话下线缓存失效时如果正好有请求在读可能会产生短暂的权限脏数据。这个问题在真实场景中很难完全避免所以我做了一层兜底删除缓存的同时把该用户的 Redis 会话也删掉强制重新登录。这样虽然体验上要重新登录一次但保证权限状态绝对准确。对内部管理系统来说这个可控的“麻烦”是可以接受的。4.4 数据权限的 SQL 拼接方案数据权限是最容易写乱的部分也是“通用”二字最难hold住的部分。简单的做法是每个业务查询自己写死部门条件但这会让业务代码严重耦合权限逻辑换一个权限策略就要改代码。风汐的思路是在 service 查询方法上加 RequiresDataScope 注解切面在执行方法前通过 ThreadLocal 向当前线程写入数据权限上下文业务方法在实际查询时调用 DataScopeHelper 来拼接 SQL 条件。举个例子业务代码是这样的RequiresDataScope public PageResultSysUser listUser(UserQuery query) { LambdaQueryWrapperSysUser wrapper Wrappers.lambdaQuery(); wrapper.like(StringUtils.isNotBlank(query.getUsername()), SysUser::getUsername, query.getUsername()); // 关键把数据范围写进 wrapper DataScopeHelper.applyDeptFilter(wrapper, SysUser::getDeptId, SysUser::getCreateBy); return userMapper.selectPage(new Page(query.getPageNum(), query.getPageSize()), wrapper); }DataScopeHelper 内部会根据角色表里的 data_scope 值决定给 wrapper 增加什么条件。例如数据范围为“本部门及以下”时会先查出当前部门及所有子部门的 ID 集合然后追加wrapper.in(SysUser::getDeptId, deptIds)。如果是“仅本人”则追加wrapper.eq(SysUser::getCreateBy, currentUserId)。这里有一个非常重要的经验数据权限条件千万不能用字符串拼接AND dept_id IN (...)直接怼进 SQL那样很容易出现注入问题更重要的是无法和 MyBatis-Plus 的 QueryWrapper 一起工作。统一走 wrapper 条件让框架去处理参数绑定安全又省心。4.5 日志模块与异步记录操作日志这块我的设计不是用拦截器全局无脑记录而是用自定义注解 Logmodule、operType去声明“哪些操作需要记录、属于什么模块”。切面里统一做请求参数解析、执行结果记录、异常信息记录然后丢进异步队列落库。为什么要把日志写成异步原因很现实日志记录如果和业务逻辑同步执行每次写库都会增加接口耗时而日志本身不能反过来拖垮业务。我用了 Spring 的 Async 异步任务把日志插入和业务返回值解耦。异步线程池单独配置一个容量足够的池子避免占用 Tomcat 主线程。5. 实操过程中的高频问题与排查经验5.1 权限变更后用户还在用旧权限的问题这是我在刚上线时遇到最多的问题。改完角色权限用户没重新登录前端还是能看见旧菜单后端旧权限接口还能调通。原因很简单登录时已经把权限数据写入了 Redis后端接口鉴权用的是 Redis 里的集合权限变更后没有及时清理缓存。解决办法我在前面也提过就是“按用户清缓存”。给角色分配菜单权限时不能只更新数据库必须同步删除该角色关联的所有用户的login_user:menus:{userId}和login_user:perms:{userId}。为了快速查出哪些用户拥有该角色我在 sys_role_menu 变更后先查 user_role 关联表再逐个删缓存。有人可能会说“那为什么不统一设置权限缓存五分钟过期”只能说这是一种规避手段但不是正确方案。内部系统对权限时效的期望是“尽量立刻生效”五分钟过期在体验上仍然会被投拆。强制重新登录虽然粗暴但至少逻辑清晰。5.2 动态路由刷新后 404 和路由残留问题用户登录系统后第一次跳转页面的动态路由注册流程已经讲过。实践中我发现两个典型坑。第一个坑是刷新页面后路由丢失。Vue Router 的路由表是内存里的数据刷新浏览器后会重置。如果不处理用户直接刷新一个二级页面就会出现白屏或者 404。解决方式是在路由守卫里增加一个全局状态判断没有菜单数据时先掉接口拉取菜单、动态注册路由然后再放行当前导航。第二个坑是账号切换之后路由残留。用户 A 登出后切换到用户 B如果只清空了 Pinia 里的菜单数据却没有移除之前 addRoute 注册的动态路由那么 B 可能会访问到 A 的页面。处理方法是在登出和登录切换时把动态路由记录保存到一个数组里然后遍历router.removeRoute全部移除再重新注册新路由。5.3 MyBatis-Plus 树形查询 N1 问题菜单树和部门树在数据量不大时很多人会图省事写递归查询一个节点一次 SQL这样数据量一旦变大接口会很慢。风汐里我改成了“一次查全量内存建树”的方案。具体做法是先把整张表的数据全部查出来放进 List然后用一个 HashMap 以 ID 为 key 建立索引再遍历 List把每个节点挂到父节点上。这样不需要在数据库层面递归一次查询搞定。虽然菜单表通常只有几百条记录但部门树在大型组织架构里可能有几千条内存建树的优势更明显。5.4 数据权限与自定义查询的冲突业务系统里经营报表、统计图表这类复杂查询比较多很多时候不是简单 wrapper 能表达的条件。我在风汐里做了一个妥协DataScopeHelper 只针对标准 CRUD 查询提供能力对于复杂报表业务层可以用DataScopeHelper.getCurrentScope()自己获取当前用户的数据范围然后在 SQL 里手动拼接参数。这种方式不能彻底避免重复代码但至少权限规则和业务逻辑的边界是清晰的。我在实际操作中还发现一个容易犯的错使用了 RequiresDataScope 注解但方法里没有调用 DataScopeHelper结果数据权限完全不生效而且不报错。排查这类问题最有效的办法是看日志中最终执行的 SQL比较有没有拼接出dept_id或其他条件。5.5 超管账号是否绕过权限校验超管是否走 RBAC 校验是很多权限系统要定的“政治问题”。我的方案是超管账号在数据库使用 is_admin 标记在后端鉴权时直接放行所有接口但在菜单数据上超管依然返回全量菜单。注意这里有一个容易踩坑的点如果超管直接放行接口但前端没有对应按钮权限或者菜单权限超管想访问某个页面时会无法进入因为后端返回的菜单不包含该页面路由。所以超管在风汐里其实不受接口权限约束但菜单还是走同样的树结构逻辑。为了保证超管能看全部菜单我的实现是管理员登录后加载全部菜单不走角色菜单关联表。这样做的好处是超管只是一个“万能钥匙”普通用户还是严格走 RBAC权限模型本身没有被破坏。5.6 会话并发与强制下线实践在线用户管理和强制下线最核心的技术点就是操作 Redis 会话。风汐里的会话 key 是login_tokens:{userId}:{sessionId}一个用户可能同时有多条会话记录。强制下线就是删除对应的 Redis 会话 key。用户下一次请求时JWT 过滤器里从 Redis 读不到 session就会被视为未认证直接返回 401前端收到 401 后统一跳转登录页。多端登录控制也是这个结构的扩展能力。比如要求“同一用户最多只能有一个在线会话”则在登录时把所有login_tokens:{userId}:*全部删除再写入新的会话。这个需求在权限系统里经常被提实现成本非常低。6. 常见问题排查速查表与避坑建议现象可能原因排查方式与解决建议权限接口返回 403用户权限缓存未刷新查看用户权限集合是否包含所需权限更新角色菜单后清缓存前端能登录但菜单空白动态路由未注册成功查看后端返回的菜单 JSON 结构检查 component 路径是否匹配刷新页面后跳到 404动态路由未持久化在路由守卫里加入先拉取菜单再放行的逻辑切账号后出现上个账号的页面动态路由残留登出时遍历 removeRoute 移除所有动态路由用户被禁用后仍能访问会话缓存未删除禁用时立刻删除对应用户的 Redis session key修改部门树后数据权限错乱部门缓存未刷新部门变更后清除部门相关的缓存键日志丢数据异步线程池拒绝配置合理的线程池队列容量增加拒绝策略报警数据权限不生效没有调用 DataScopeHelper查看 SQL 日志确认是否拼接条件另外再补几个经验性的建议权限标识命名一定要早规范。不要在项目里既有 system:user:add 又有 userAdd 这种风格后期维护会崩溃。菜单表删除前要检查子公司是否还有引用物理删除容易造成角色权限数据悬空风汐里我采用的是逻辑删除菜单标记为停用。权限表达式不要允许前端传什么就匹配什么后端必须以数据库配置为准。前端的按钮显隐只是体验安全边界永远在后端。接口设计上不要把“按钮权限”和“数据权限”混在一个注解里分开声明会让业务代码可读性高很多。7. 最后再分享一点我的真实体会做风汐这套系统前后大概花了三周时间真正写完核心权限模块只用了大半周剩下大量时间都是在做“让权限不生效的测试”和“混乱需求下的边界梳理”。我的体会是权限系统的难点从来不在某个具体技术而是你能不能把“哪些资源需要保护、保护到多细的粒度、变更后如何生效”这三件事想清楚。如果你也想做类似的东西我给一个务实建议不要上来就追最新框架先把数据库表设计好尤其是 sys_menu 的 perms、menu_type、parent_id 这三列一定要想明白这是整个权限系统的基石。表设计稳定了后面加缓存、加注解、加数据权限都只是工作量的问题。风汐目前已在我这边多个内部项目里运行稳定。后续我计划把它扩展出更细粒度的数据规则配置界面以及把菜单权限与 ABAC 做一点轻量融合让复杂的多维授权场景也能用配置解决。这个方向还在迭代等有更多落地经验再和大家细聊。