多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

从菜单表到接口权限:RBAC权限设计完整落地指南

从菜单表到接口权限:RBAC权限设计完整落地指南 1. 先从一张菜单表说起权限设计的第一个误区我做后台管理系统有些年头了几乎每次接手一个新项目都要面对同一个问题——权限这块到底怎么设计。而最近被问到最多的一种情况是“菜单表已经建好了角色也关联上了用户登录后该看到哪些菜单也没问题那接口权限怎么配”这个问题看似简单但背后隐藏着一个很常见的认知误区很多人以为菜单权限就是权限的全部。菜单能看、能点就觉得功能是受控的。但实际上菜单权限只是前端展示层的控制真正的安全底线在后端接口。换句话说用户看不到某个菜单不代表他不能直接调接口拿数据。只要他知道了接口地址用工具或者浏览器控制台模拟请求你的数据照样可能暴露。所以配置接口权限这件事本质上是给后端接口加上“通行证”机制让每个请求在到达业务代码之前先经过一道身份与权限的检验。只有通过检验请求才能继续往下走。那“已知菜单表”这个前提意味着什么意味着你的系统已经有了基础的角色-菜单关系数据。这是一笔可以充分利用的资产因为接口权限的设计完全可以顺着菜单结构往下铺而不是推倒重来。在展开具体设计之前先明确几个核心概念菜单Menu前端导航条目属于展示层资源。接口API后端暴露的访问端点属于数据层资源。角色Role权限的载体一组权限的集合。权限Permission对某个资源的操作许可可以细分为菜单权限和接口权限。菜单和接口的关系很微妙。一个菜单页面上可能对应多个接口操作比如列表查询、新增、编辑、删除。一个接口也可能被多个菜单共用。设计接口权限时要先梳理清楚这个对应关系。我见过不少项目菜单表设计得很漂亮层级清晰、图标齐全但问起接口权限怎么控制要么是空白的要么是只校验了登录态根本没有细粒度控制。这种情况下系统上线后往往隐患很大。所以这篇内容我们就围绕“已知菜单表”这个场景把接口权限的配置方案完整梳理一遍。从表结构设计、权限模型选型到拦截器实现、常见坑点一步步来最后你完全可以照着落地。2. 菜单权限和接口权限的边界为什么必须分开管很多人会问既然角色已经关联了菜单那能不能在菜单表上加一个字段标注这个菜单对应的接口路径然后直接通过菜单权限来控制接口听起来很美好但实际做起来会踩到几个坑。2.1 接口和菜单不是一一对应的一个典型的列表页面前端要调用的接口可能有查询分页数据的接口查询筛选条件的字典接口导出数据的接口查看详情的接口这些接口在菜单表里并没有对应的节点它们是页面内部的“隐式资源”。如果把接口权限挂在菜单节点上你会发现要么菜单表被撑爆要么接口权限颗粒度太粗无法单独控制“可看列表但不可导出”这类需求。反过来一个接口可能被多个菜单复用。比如“上传文件”接口用户头像、商品图片、附件文档都在用。你把这个接口挂到哪个菜单下都会出问题因为它本质上是公共能力。2.2 前端控制和后端控制的责任不同菜单权限解决的是“用户能看到什么”接口权限解决的是“用户能调什么”。两者必须配合但绝不能混淆。前端控制的作用是提升用户体验让用户看不到他用不了的功能避免误操作。但前端控制是不可信的安全手段因为所有前端代码都暴露在浏览器里用户可以绕过界面直接构造请求。后端控制才是安全底线。每一次请求进来都要校验身份校验角色权限校验接口权限。只有层层通过才允许访问业务逻辑。2.3 权限模型选型RBAC还是ABAC配置接口权限之前先选好权限模型。大多数后台系统适合用RBAC基于角色的访问控制因为角色数量有限管理成本低理解起来也直观。RBAC的核心是“用户-角色-权限”三张表再加上用户与角色、角色与权限的关系表。放到接口权限的场景里“权限”就对应到具体的接口资源。如果你的系统权限规则比较复杂比如要根据用户所属部门、数据层级、时间范围来动态判断是否允许访问这时候可以考虑ABAC基于属性的访问控制。但ABAC的配置复杂度高不适合作为接口权限的默认选择。对于大多数项目我推荐的做法是核心采用RBAC特殊场景用ABAC做补充。先跑通主流程再针对个别接口单独写策略不要一上来就把体系搞得很重。2.4 菜单表和接口权限表的关系设计现在回到“已知菜单表”这个前提。菜单表通常是这样一张表字段含义id菜单IDparent_id父级菜单IDname菜单名称path前端路由路径component前端组件路径perm_code权限标识如 sys:user:listtype菜单类型目录/菜单/按钮sort排序号这里有个关键字段perm_code。如果你在建菜单表时已经给每个菜单和按钮定义了权限标识那接口权限就可以直接复用这套标识体系。具体做法给每个接口设置一个RequiresPermission(sys:user:list)这样的标识然后在后端校验时检查当前用户是否拥有这个权限标识。这样菜单权限和接口权限就统一到了同一套“权限码”体系里只是控制的位置不同——菜单控制的是前端渲染接口控制的是后端调用。如果你的菜单表里还没有perm_code字段那需要补上。这是接口权限配置中最基础也最容易被忽略的一步。3. 设计接口权限表五张表还是三张表明确了模型选型之后接下来落地表结构。这一节直接给出设计思路和建表SQL你可以根据自己项目的实际情况调整。3.1 基础表结构RBAC五张核心表完整的RBAC模型需要以下这些表sys_user用户表sys_role角色表sys_menu菜单表含按钮和权限标识sys_user_role用户角色关联表sys_role_menu角色权限关联表接口权限不用单独建一张“接口表”而是复用sys_menu中typeF按钮的记录来承载。每一条按钮类型的菜单记录就对应一个接口的权限码。为什么这样设计因为大多数后台系统的权限码天然支持树形结构按钮挂在菜单下面用户拥有某个菜单权限通常意味着可以操作该菜单下的基础按钮。虽然不一定所有按钮都开放给所有能看菜单的角色但通过sys_role_menu可以精确地给每个角色分配按钮权限灵活性已经足够。3.2 如果菜单表没有按钮节点如果你的菜单表只维护了目录和菜单没有按钮这一层那就需要扩展。最省事的做法是在sys_menu表里增加记录type设为Fparent_id指向对应的菜单IDperm_code写上权限标识。举个例子用户管理菜单下面可以加这几个按钮权限菜单名称typeperm_code用户查询Fsystem:user:query用户新增Fsystem:user:add用户编辑Fsystem:user:edit用户删除Fsystem:user:delete用户导出Fsystem:user:export这样一来角色-菜单关联表里就可以为不同角色分配不同的按钮权限。接口校验的时候直接判断当前用户是否具有对应的perm_code即可。3.3 匿名接口和登录接口特殊处理哪些接口需要纳入权限校验哪些不需要要提前规划好。通常以下接口可以放行登录、登出、验证码用户注册刷新令牌静态资源图片、文件预览系统监控的health检查这些接口如果也要求权限验证会造成逻辑死循环——用户还没登录系统却要求登录。所以配置过滤器或拦截器时要维护一个白名单放行这些公共接口。3.4 建表SQL参考下面给出一份可以直接用的建表SQL主流的MySQL方言改改就能用-- 菜单表含按钮权限 CREATE TABLE sys_menu ( id bigint NOT NULL AUTO_INCREMENT, parent_id bigint NOT NULL DEFAULT 0 COMMENT 父菜单ID0表示根节点, name varchar(64) NOT NULL COMMENT 菜单名称, path varchar(255) DEFAULT NULL COMMENT 前端路由路径, perm_code varchar(128) DEFAULT NULL COMMENT 权限标识如 system:user:list, type char(1) NOT NULL COMMENT 类型M目录C菜单F按钮, sort int NOT NULL DEFAULT 0 COMMENT 排序, status tinyint NOT NULL DEFAULT 1 COMMENT 状态1启用0禁用, PRIMARY KEY (id), KEY idx_parent_id (parent_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT菜单权限表; -- 角色表 CREATE TABLE sys_role ( id bigint NOT NULL AUTO_INCREMENT, name varchar(64) NOT NULL COMMENT 角色名称, code varchar(64) NOT NULL COMMENT 角色编码, status tinyint NOT NULL DEFAULT 1 COMMENT 状态1启用0禁用, PRIMARY KEY (id), UNIQUE KEY uk_code (code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT角色表; -- 用户表精简字段 CREATE TABLE sys_user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(64) NOT NULL COMMENT 用户名, password varchar(128) NOT NULL COMMENT 密码, status tinyint NOT NULL DEFAULT 1 COMMENT 状态1启用0禁用, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 用户-角色关联表 CREATE TABLE sys_user_role ( user_id bigint NOT NULL, role_id bigint NOT NULL, PRIMARY KEY (user_id, role_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户角色关联表; -- 角色-菜单关联表含按钮权限 CREATE TABLE sys_role_menu ( role_id bigint NOT NULL, menu_id bigint NOT NULL, PRIMARY KEY (role_id, menu_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT角色菜单关联表;这套结构和大多数开源后台管理框架如若依、Vue Admin等的权限设计是一致的。如果你用过这类框架上手会非常快如果没用过照着这个结构来也足够支撑绝大多数业务场景。表结构设计好之后下一步是编写代码来让这套模型真正跑起来。4. 接口权限校验的三种落地方式表结构就绪之后核心问题是代码层面怎么拦截请求并校验接口权限。这里介绍三种由浅入深的方式你可以根据项目的技术栈和改造成本来选择。4.1 方式一拦截器数据库实时查询最直接的方式是写一个HandlerInterceptor在请求进入Controller之前取到当前登录用户的角色和权限集合判断当前请求路径是否在授权范围内。核心代码如下Component public class PermissionInterceptor implements HandlerInterceptor { Autowired private UserService userService; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 如果是预检请求直接放行 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } // 1.获取当前登录用户从Token或Session中解析 Long userId JwtUtil.getUserIdFromRequest(request); if (userId null) { throw new UnauthorizedException(未登录或登录已过期); } // 2.查询当前请求路径对应的权限标识 String requestPath request.getRequestURI(); String method request.getMethod(); String permCode permissionService.getPermCodeByPath(method, requestPath); if (permCode null) { // 该接口未配置权限标识默认需要登录即可访问 return true; } // 3.检查用户是否拥有该权限标识 boolean hasPermission userService.hasPermission(userId, permCode); if (!hasPermission) { throw new ForbiddenException(无权限访问该接口); } return true; } }这个方案的理解成本最低但它有一个明显的问题每次请求都要查一次数据库高并发场景下会增加数据库压力。而且“请求路径-权限标识”的映射关系维护在数据库里需要额外一张表。适合的场景内部管理系统的并发量不高、权限规则相对简单、改造时间有限。如果你只是想先把权限体系跑起来这个方案足够。4.2 方式二注解驱动切面校验更优雅的方案是用注解。定义一个RequiresPermission注解标注在Controller类或方法上然后用AOP切面在方法调用前统一的做出拦截判断。注解定义Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface RequiresPermission { String value(); // 权限标识如 system:user:add }Controller中使用示例RestController RequestMapping(/api/system/user) public class UserController { GetMapping(/list) RequiresPermission(system:user:query) public Result pageList() { // 查询分页数据 } PostMapping(/add) RequiresPermission(system:user:add) public Result add(RequestBody User user) { // 新增用户 } DeleteMapping(/{id}) RequiresPermission(system:user:delete) public Result delete(PathVariable Long id) { // 删除用户 } PostMapping(/export) RequiresPermission(system:user:export) public Result export() { // 导出Excel } }切面校验逻辑Aspect Component public class PermissionAspect { Around(annotation(requiresPermission)) public Object checkPermission(ProceedingJoinPoint joinPoint, RequiresPermission requiresPermission) throws Throwable { // 获取当前登录用户 LoginUser loginUser SecurityUtils.getLoginUser(); if (loginUser null) { throw new UnauthorizedException(未登录或登录已过期); } // 校验权限 SetString perms loginUser.getPermissions(); if (!perms.contains(requiresPermission.value())) { throw new ForbiddenException(无权限访问该接口); } return joinPoint.proceed(); } }这个方案把权限标识和接口绑定在了一起可读性很强代码里一眼就能看出接口需要什么权限。而且权限集合在用户登录时就加载到内存中后续请求直接走内存判断性能比方式一好不少。使用这个方案时有个关键点登录时要把用户的所有权限标识查出来放到登录用户对象里。一般可以通过用户ID查到角色ID列表再通过角色ID查到菜单权限标识列表最后合并去重。这样每次请求校验时就不需要查库了。4.3 方式三网关统一鉴权如果系统是微服务架构多个服务之间需要统一鉴权那拦截器和注解方案就不太适用了。这时候需要在网关层做统一的权限校验。网关层拿到请求后解析Token获取用户信息然后调用权限服务判断该用户是否有权访问目标接口。这个方案的优点是权限逻辑集中管理各个业务服务不需要关心权限细节缺点是每次请求都要经过一次远程调用或缓存查询对网关的性能要求比较高。如果你当前项目是单体应用方式二已经足够了。除非确定要拆分微服务否则不必一步到位采用网关方案。4.4 方案选型建议对比维度拦截器查库注解AOP网关统一鉴权实现复杂度低中高性能依赖数据库查询内存判断依赖缓存或远程调用可读性一般高中适用场景单体低并发单体/服务化微服务架构权限标识管理数据库映射表注解按钮记录权限中心我一般建议单体项目直接选方式二理由很实际代码即文档每个接口需要什么权限一目了然性能好不每次查库重构成本低权限变更只改注解即可。后续系统升级成微服务架构再做网关层适配也不迟。5. 配置步骤实操从菜单表到接口权限全流程现在进入全文的核心实操部分。假设你的菜单表里已经有了菜单和按钮数据角色也分配好了这里一步步演示如何把接口权限配置并启用。5.1 第一步梳理菜单表数据补齐按钮权限记录打开数据库查询你的菜单表看看现有数据。SELECT id, parent_id, name, type, perm_code, path FROM sys_menu ORDER BY parent_id, sort;你需要做的事找出所有的目录和菜单节点type为M或C。为每个需要控制的接口操作在菜单表下添加上级按钮记录。举个例子如果用户管理的菜单ID是5那么执行以下SQL就可以为它添加按钮权限INSERT INTO sys_menu (parent_id, name, perm_code, type, sort) VALUES (5, 用户查询, system:user:query, F, 1), (5, 用户新增, system:user:add, F, 2), (5, 用户编辑, system:user:edit, F, 3), (5, 用户删除, system:user:delete, F, 4), (5, 用户导出, system:user:export, F, 5);这里有个易错点perm_code的命名一定要规范。我推荐统一的命名规则模块:子模块:操作比如system:user:add全部小写用冒号分隔。这样在代码里搜索、维护、授权的时候都很方便。5.2 第二步确认角色-按钮权限的分配数据按钮权限记录添加好之后需要给角色分配这些按钮权限。如果你的系统已经有一个“超级管理员”角色通常它应该拥有所有权限你可以写SQL批量插入-- 假设管理员角色ID为1把所有按钮权限分配给该角色 INSERT INTO sys_role_menu (role_id, menu_id) SELECT 1, id FROM sys_menu WHERE type F;对于普通角色需要在后台管理界面里勾选分配或者在SQL里按需插入。这一步容易踩的坑是忘记给角色分配按钮权限导致接口全部403。开发环境排查问题时可以先确认用户关联的角色再查角色关联的按钮权限。5.3 第三步登录时加载权限标识到用户上下文这是整个方案里最核心的代码逻辑。用户登录成功后需要把该用户所有去重过的按钮权限标识加载出来放入登录用户对象。public SetString getPermissionCodes(Long userId) { // 1.查询用户的角色ID列表 ListLong roleIds userRoleMapper.selectRoleIdsByUserId(userId); if (roleIds.isEmpty()) { return Collections.emptySet(); } // 2.通过角色ID查询菜单权限标识 ListString permCodes roleMenuMapper.selectPermCodesByRoleIds(roleIds); // 3.过滤掉空值去重收集 return permCodes.stream() .filter(StringUtils::hasText) .collect(Collectors.toSet()); }SQL可以参考这个SELECT DISTINCT m.perm_code FROM sys_role_menu rm JOIN sys_menu m ON rm.menu_id m.id WHERE rm.role_id IN (1, 2, 3) AND m.type F AND m.perm_code IS NOT NULL AND m.status 1;登录接口成功之后把查到的权限标识集合设置到登录用户对象里LoginUser loginUser new LoginUser(); loginUser.setUserId(user.getId()); loginUser.setUsername(user.getUsername()); loginUser.setPermissions(permCodes);之后的每个请求都可以直接从登录用户对象中拿到权限集合做内存判断。5.4 第四步接口上标注权限注解这一步就是把写好的RequiresPermission注解标注到Controller方法上。注意一个细节是标注在Controller的方法上不是Service方法上。为什么因为权限校验属于外部访问控制的范畴放在Controller层可以让权限逻辑和业务逻辑分离而且注解的意图也更清晰。如果你放在Service层万一有内部方法调用可能也会被权限拦截引起不必要的麻烦。标注了几个示例GetMapping(/{id}) RequiresPermission(system:user:query) public Result getInfo(PathVariable Long id) { ... } PutMapping(/{id}) RequiresPermission(system:user:edit) public Result update(PathVariable Long id, RequestBody User user) { ... } DeleteMapping(/batch) RequiresPermission(system:user:delete) public Result deleteBatch(RequestBody ListLong ids) { ... }批量删除和单条删除如果都是删除操作共享同一个权限码即可不用为每个接口单独建权限标识。5.5 第五步注册拦截器或切面让其生效以Spring Boot项目为例注解切面的配置方式在4.2节已经给出。如果项目里已经引入了spring-boot-starter-aop依赖那么只需要写一个配置类启用AOP即可过程不再展开。注册拦截器的配置Configuration public class WebMvcConfig implements WebMvcConfigurer { Autowired private PermissionInterceptor permissionInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(permissionInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login, /api/auth/captcha, /api/common/upload); } }addPathPatterns指定拦截哪些路径excludePathPatterns放行公共接口。5.6 第六步验证是否生效配置完成后至少要做三轮验证使用管理员账号登录访问受控接口确认能正常请求。创建一个测试角色只分配“查询”按钮权限不分配“新增”权限使用该角色登录访问新增接口确认返回403。直接退出登录访问受控接口确认提示未登录。第3轮验证很容易被忽略但它恰恰是检验“登录态校验”是否生效的关键一步。很多接口权限配置好了但登录态校验有漏洞导致未登录用户能直接调用接口这是绝对不能放行的。6. 常见问题与排查技巧实录实际配置接口权限的过程中会遇到各种奇奇怪怪的问题。这里把我在项目中遇到最多的问题整理一下给出一份排查手册。6.1 接口返回403但用户明明分配了权限这是最让人头疼的问题排查思路按顺序来第一步确认权限标识是否匹配。检查Controller注解里的权限标识和数据库里sys_menu表的perm_code是否一模一样。注意大小写、引号、空格system:user:add和system:user:Add是不同的。第二步确认角色是否分配了按钮权限。执行SQLSELECT rm.* FROM sys_role_menu rm JOIN sys_menu m ON rm.menu_id m.id WHERE m.perm_code system:user:add;如果查询结果为空说明角色没有分配这个按钮权限。第三步确认用户-Role关联关系。查用户角色关联表确认当前用户确实关联了有权限的角色。SELECT ur.user_id, ur.role_id FROM sys_user_role ur WHERE ur.user_id 10001;第四步确认登录加载逻辑。在代码里打日志把用户登录时加载出来的权限标识集合打印出来看是否包含所需的权限码。这一步能快速定位到“加载逻辑缺漏”还是“数据库数据缺失”。我在实际排查中超过80%的问题出在第三步和第四步用户关联的角色没错角色也分配了权限但登录逻辑里只查了部分角色或者查询SQL里漏掉了typeF的条件导致按钮权限没被加载。6.2 接口返回401但登录接口明明调用成功了这通常是Token解析的问题。检查以下几点Token有没有正确传到后端前端请求头是不是Authorization: Bearer tokenToken过期时间是不是太短开发环境把过期时间设置得长一点比如24小时。拦截器里Token解析逻辑是否兼容刷新后的Token6.3 预检请求被拦截导致前端无法调用前端发POST、PUT、DELETE请求时会先发一个OPTIONS预检请求。如果拦截器没有放行OPTIONS请求会出现一个诡异的现象请求头里能看到请求已经发出但后端一直在报405或403。在拦截器的preHandle方法里加上这段代码if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; }同时在后端配置跨域时要让Access-Control-Allow-Methods包含OPTIONS。6.4 某个接口不需要权限控制但被拦截了一个典型场景文件预览接口或导出下载接口URL里带了动态参数比如/api/file/download/12345每次路径都不同。解决方案有两种一种是使用Ant风格的路径匹配在excludePathPatterns里配/api/file/download/**白名单。另一种是在接口上加Anonymous注解在拦截器里检测到该注解则放行。第二种方案更灵活因为白名单写在代码里跟接口定义在一起可维护性好。但需要自己实现注解解析逻辑稍微多一点代码量。6.5 权限加载到内存后权限变更不生效用注解AOP的方案权限标识集合是在登录时加载的。如果用户在系统运行过程中被改了角色或权限已经登录的用户不会立即感知到变化。这个问题的常见处理方式强制用户重新登录。权限变更后清理该用户的缓存。用Redis存储权限集合每次请求都从Redis拉取牺牲一点性能换取实时性。根据项目的安全等级要求选择策略。一般后台管理系统用“强制重新登录”就足够了简单可靠。7. 越权测试清单配置完不等于安全了接口权限配好之后不能只看“能访问”和“不能访问”这两种情况。真正的安全性测试要覆盖一系列边界场景。7.1 横向越权测试用户A能否通过修改请求参数比如userId访问到用户B的数据权限控制往往能挡住“没有功能的访问”但拦不住“有功能但数据越权”。比如用户B的详情接口是受控的但用户A把自己的ID换成B的ID如果后端没有校验数据归属依然能获取到数据。这类问题不属于接口权限的范畴而是业务层的越权校验但它和接口权限强相关。测试列表修改路径中的ID参数尝试访问其他人的数据。在列表接口添加不合理的过滤条件尝试绕过列表范围限制。修改请求体中的归属字段比如userId、deptId。7.2 垂直越权测试用户A是普通角色尝试调用管理员才能使用的接口。测试方法很简单拿到普通用户的Token直接访问管理员接口看是否返回403。常见问题有些开发者只在前端隐藏了管理按钮后端的Controller方法没有加权限注解导致普通用户直接拼URL即可调用。这就是接口权限缺失的典型场景。7.3 HTTP方法越权有些系统把删除接口设计成GET请求或者允许同一个URL用不同HTTP方法访问不同业务逻辑。这种设计容易引起混乱。测试时关注用GET请求访问只允许POST的接口。用POST请求访问只允许GET的接口。使用PUT和DELETE方法的权限是否一致。7.4 深度覆盖检查在真实项目里经常会遇到Controller里某个方法忘了加权限注解但接口路径又在拦截范围之内的情况。这种情况默认会“只要登录就能访问”如果这个接口本身就不应该让所有登录用户访问就会产生漏洞。所以配置接口权限的过程中最需要的是“穷举接口清单”的意识。项目上线前把所有接口路径和方法列出来逐个对照权限标注查漏补缺。8. 接口权限的演进方向从静态权限到动态治理如果你所在的团队打算长期维护这个系统在基础的接口权限跑通之后可以考虑一些演进方向。8.1 接口权限和数据权限的分离接口权限解决的是“能不能调用这个接口”的问题数据权限解决的是“调用接口后能看哪些数据”的问题。两者要区分开。例如销售角色可以调用订单查询接口但只能看到自己负责的订单经理可以看到整个团队的订单。这就是数据权限的范畴通常需要在SQL层面附加过滤条件是另一套独立的设计。8.2 权限标识的自动化审计当项目越来越大接口越来越多靠人工在注解上维护权限标识会逐渐变得难以管理。可以考虑引入简单的审计机制在启动时扫描所有Controller方法上的权限注解和数据库里的按钮权限记录做比对找出“有注解无记录”和“无注解无白名单”的接口输出到日志或报表里定期Review。这个思路不复杂但很实用能帮助团队保持权限配置和代码的一致性。8.3 权限配置的热更新前面提到过权限变更需要重新登录才能生效。如果你希望权限变更能即时生效可以把权限集合放到Redis里设置过期时间比如5分钟每次请求时加载缓存5分钟内自动刷新。这样既保证了实时性又不会每次请求都查一次数据库。实现方式登录时把权限标识集合写入Rediskey为userId:permissionsvalue为JSON数组权限变更时主动删除对应key拦截器里先查Redis没有再查数据库查完回填Redis。这个方案比全内存方案灵活很多改造成本也不高。8.4 和开放平台对接时的接口权限模型如果你的系统需要对外开放API比如第三方系统通过OAuth2或API Key调用你的接口那权限模型需要调整。此时不再是“用户-角色-权限”而是“应用-API权限”的模型可以考虑引入API Key、独立的应用权限表和独立的API网关层。这个问题离主题稍远但如果你所在的系统有对外开放的规划建议提前在权限模型上留出扩展余地比如权限标识统一以模块分层避免未来重构。从整体看接口权限的配置并不是一个孤立的开发任务它和菜单表、角色表、用户表、前端路由、后端拦截机制构成了一个完整的权限闭环。把“已知菜单表”作为切入点一步步往下打通最后收获的不只是接口安全更是一套清晰的权限治理思路。
返回列表