首页 / 资讯中心 / 文章详情

Java权限模型实战:从RBAC到数据权限与Spring Boot落地

Java权限模型实战:从RBAC到数据权限与Spring Boot落地 ★ FEATURED ARTICLE
最近又在技术群里看到有人问“Java项目里的权限到底怎么做”底下回复五花八门有说直接上Spring Security的有说抄一套若依的也有说用Sa-Token更省事。说实话权限模型这个东西我在Java后端摸爬滚打了五六年从最早自己写过滤器硬编码判断到后来老老实实按RBAC设计表结构再到现在对比各种快速开发平台的权限模块中间踩过的坑能写满一页A4纸。所以这篇笔记想把这件事完整梳理一遍从基础RBAC的原理到数据权限、行级权限这些衍生问题再到Java生态里Spring Boot项目中的落地套路以及当我们不想从零写权限时快速开发平台到底该怎么选。这篇文章适合谁看我觉得三类人最需要第一刚接触Java后台开发、被“权限”两个字吓到的新人第二项目里权限越写越乱、想系统重构的老手第三正在做技术选型、纠结自己写还是用开源平台的团队负责人或者架构师。下面我尽量用大白话讲清楚不堆八股文概念把每个关键选择背后的理由也一并交代。1. RBAC为什么能成为Java权限系统的默认答案1.1 权限系统到底在解决哪三个问题一上来先别急着看代码先把“权限”拆清楚。我习惯把权限系统的问题拆成三个层次你是谁认证、你能做什么授权、你能对哪些数据做数据范围。这三者的关系有点像公司门禁。你是谁决定你能不能进大楼你的职位角色决定你能进几楼几号办公室而进了办公室之后桌上哪份文件你能翻开看还得看你的岗位职责是什么。认证环节通常由Spring Security、Sa-Token这类框架解决授权和数据模型则需要我们自己设计数据范围更是要结合具体业务单独定义。很多新人只盯着第二个问题把用户表、角色表、菜单表设计完就觉得大功告成。结果一上线就被业务方追着问“为什么A部门的经理能看到B部门的订单数据”这就是典型的第三个问题没考虑。所以你看标题里说的“权限模型”核心落点其实在授权和数据范围这两块认证反而是相对现成的部分。1.2 角色这个中间层RBAC模型的核心设计RBAC的全称是Role-Based Access Control基于角色的访问控制。它的核心思路一句话就能说完用户不直接关联权限而是通过角色间接获得权限。为什么中间要插一个角色可以试想一下系统里有10个用户、3个权限点。如果直接给用户分配权限10乘以3就是30条关联记录。当用户数量到1000、权限点到200时直接管理的复杂度会爆炸而且每个人都可能配出不一样的权限组合审计和调整都无从下手。引入角色之后所有同类用户共享一个角色管理员只需要维护“角色—权限”关系给用户分配角色即可。公司来了个新员工拉进“运营”角色菜单权限自动就有了员工转岗换个角色权限自动收回。这个模型跟现实组织的管理逻辑完全吻合所以业务方理解成本极低这也是Java项目普遍选它的根本原因。RBAC在学术上还分RBAC0、RBAC1、RBAC2等层级。RBAC0是基础模型就是用户-角色-权限三元组RBAC1加了角色继承比如“运营总监”自动继承“运营专员”的所有权限RBAC2加了职责分离约束比如同一个人不能同时拥有“制单”和“审批”两个互斥角色。实际项目中绝大多数需求用RBAC0加一点RBAC1就足够了别一上来就把模型搞得太重。1.3 一套可落地的RBAC表结构长什么样理论说完看落地。网上流传的“五张表”指的就是用户表、角色表、菜单权限表、用户-角色关联表、角色-菜单关联表。下面这套SQL是我在不同项目里反复调整后比较顺手的版本CREATE TABLE sys_user ( id BIGINT PRIMARY KEY COMMENT 用户ID, username VARCHAR(50) NOT NULL COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT 密码BCrypt加密, dept_id BIGINT COMMENT 所属部门, status TINYINT DEFAULT 1 COMMENT 状态1正常 0停用, create_time DATETIME COMMENT 创建时间 ); CREATE TABLE sys_role ( id BIGINT PRIMARY KEY COMMENT 角色ID, role_code VARCHAR(50) NOT NULL COMMENT 角色编码, role_name VARCHAR(50) NOT NULL COMMENT 角色名称, data_scope TINYINT DEFAULT 1 COMMENT 数据范围1全部 2本部门及以下 3本部门 4本人 5自定义, create_time DATETIME COMMENT 创建时间 ); CREATE TABLE sys_menu ( id BIGINT PRIMARY KEY COMMENT 菜单ID, parent_id BIGINT DEFAULT 0 COMMENT 父菜单ID, menu_name VARCHAR(50) COMMENT 菜单名称, perms VARCHAR(100) COMMENT 权限标识如 system:user:list, menu_type TINYINT COMMENT 类型1目录 2菜单 3按钮 ); CREATE TABLE sys_user_role ( user_id BIGINT COMMENT 用户ID, role_id BIGINT COMMENT 角色ID, PRIMARY KEY (user_id, role_id) ); CREATE TABLE sys_role_menu ( role_id BIGINT COMMENT 角色ID, menu_id BIGINT COMMENT 菜单ID, PRIMARY KEY (role_id, menu_id) );注意几个细节。第一perms字段建议直接用“模块:功能:操作”的字符串格式比如system:user:add代表“用户管理-新增用户”比单纯存个菜单ID更好扩展后端注解校验时直接比对字符串即可。第二data_scope字段放角色表比放用户表更合理因为同一个人的不同角色可能有不同数据范围归角色管才不会乱。第三菜单表里混着目录、菜单、按钮三类数据靠menu_type区分查询时按类型过滤就好。提示这张表只是基础。真正生产环境还要加部门表、操作日志表、角色-部门自定义数据范围表但核心主线永远是这五张。2. 从基础RBAC到复杂权限角色继承、数据权限与行级控制2.1 用户组与角色继承组织架构复杂化后的第一步基础RBAC用久了第一个发现的问题是角色会爆炸。公司组织稍微复杂点比如华东区销售、华东区销售经理、华南区销售、华南区销售经理如果每个岗位都新建角色权限一改就得同时改好几个角色重复配置特别多。这时候有两个常见解法用户组和角色继承。用户组解决的是“一批人批量获得一堆角色”的问题。比如“华东区销售组”里挂了50个账号管理员给销售组分配角色时组内所有用户自动生效不用一个一个人点。用户组和角色的区别在于角色表达的是“能力范围”用户组表达的是“人员集合”两者可以叠加使用。角色继承解决的是“权限共性”问题对应RBAC1。设计上可以让子角色持有父角色ID比如“销售经理”继承“销售专员”的所有权限再额外加上审批、查看报表的权限。查询当前用户权限时递归收集父角色权限即可。不过递归深度我建议控制在两层最多三层三层以上的权限级联会让排错变成噩梦甚至出现“为什么这个人有这个权限”这种扯不清的情况。2.2 数据权限RBAC管不到“行”这一层如果说角色权限回答的是“你能不能点这个菜单”数据权限回答的就是“你点进去之后能看到哪些数据”。前者是接口和按钮级别后者是SQL查询的数据行级别也就是网上经常提到的“行级权限”。最典型的业务场景就是部门分级管理。比如一份订单表销售专员只能看自己名下的订单销售经理能看整个部门的订单运营总监能看全公司的订单。菜单权限大家都有但查询结果的SQL条件完全不一样。通常做法是在角色表上加一个data_scope字段用数字字典表示范围data_scope含义SQL追加条件示例1全部数据不追加条件2本部门及以下部门dept_id IN (当前部门及子部门)3本部门dept_id 当前部门4本人create_by 当前用户ID5自定义按用户自定义规则生成条件片段这个字段的设计逻辑很直白数据权限通常跟着角色走换一个角色数据范围就变一个层次。把data_scope放在角色表而不是用户表也正好呼应了前面说的“角色表达能力范围”这件事。2.3 字段级权限与按钮级权限的实现思路除了行级控制还有两个更细的维度容易被忽略。按钮级权限说白了就是菜单表里menu_type3那类按钮记录。前端拿到用户权限列表后决定“新增”“删除”按钮是否渲染后端接口再校验一次权限标识防止有人绕过前端直接调接口。前端隐藏只是体验优化后端校验才是安全底线。这个点我在后面踩坑部分还会重点讲。字段级权限解决的是“同一个列表A角色能看到手机号B角色看不到”这类问题。实现上通常在注解里声明哪些字段需要脱敏或过滤比如用户查询接口标注FieldPermission({mobile: encrypt})后端在处理响应前自动按配置裁剪或加密字段。字段级权限对性能和代码侵入都有影响一般只在用户信息、合同信息等敏感场景使用不建议全系统铺开。这两个维度补上之后一套相对完整的权限模型才是接口能不能调接口/按钮权限、数据能看到哪几行行级数据权限、数据里哪些列能看到字段权限。3. Spring Boot MyBatis下落地RBAC的完整套路3.1 认证授权框架选型Spring Security还是Sa-Token在Java生态里落地RBAC绕不开认证授权框架的选择。我接触最多的是Spring Security和Sa-TokenShiro现在新项目已经很少有人选了简单提一下即可。维度Spring SecuritySa-TokenShiro学习曲线陡峭概念多平缓上手快平缓功能完备度极高OAuth2/SAML等都能扩展高登录、踢人、单点都有基础功能齐全社区活跃度极高Spring官方维护高国内社区活跃偏低二次开发难度过滤器链理解门槛高注解拦截器直观一般典型场景中大型项目、标准化企业架构中小项目、敏捷交付老项目维护我的经验是如果是全新项目、团队没人熟悉Spring Security又想快速出活Sa-Token非常友好它的注解式鉴权和登录集成几乎零成本如果团队有Spring Security基础或者项目后续要接OAuth2、客户端资源服务等复杂场景直接上Spring Security更稳。选型的核心不是谁更强大而是团队能不能在两三周内真正驾驭它。3.2 接口权限校验注解 切面的实际写法无论选哪个框架接口级别的权限校验思路是通用的先拿到当前登录用户的权限标识集合再判断目标接口要求的权限标识是否在集合里。用Spring Security时可以直接用PreAuthorizePreAuthorize(hasAuthority(system:user:add)) public void addUser(UserDTO dto) { // 新增用户逻辑 }如果不想绑定框架也可以自定义注解加AOP切面Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequiresPermission { String value(); }Aspect Component public class PermissionAspect { Before(annotation(requiresPermission)) public void check(JoinPoint joinPoint, RequiresPermission requiresPermission) { String required requiresPermission.value(); // 从当前登录上下文获取权限集合 SetString perms SecurityUtils.getPermissions(); if (!perms.contains(required) !perms.contains(*)) { throw new AccessDeniedException(无访问权限: required); } } }代码本身不复杂但有个关键点容易漏权限集合的获取时机。如果每次请求都查一次数据库接口一多数据库压力会陡增。正确做法是登录成功时把权限集合写入Rediskey为用户ID管理员调整角色权限时主动删除对应用户的缓存key。后续请求从Redis读权限既保证实时性又不会反复压数据库。3.3 MyBatis拦截器实现数据权限过滤行级数据权限在MyBatis架构下最优雅的落地方式是拦截器。原理很简单在执行查询SQL之前根据当前用户的数据权限范围动态往SQL里拼一个WHERE条件。核心思路可以写成一个自定义拦截器拦截StatementHandler的prepare方法Intercepts({ Signature(type StatementHandler.class, method prepare, args {Connection.class, Integer.class}) }) public class DataScopeInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { // 1. 从上下文中获取当前用户的dataScope与部门ID // 2. 根据dataScope生成条件片段如 dept_id IN (...) // 3. 通过BoundSql重新封装SQL注意使用?占位参数不要直接拼接字符串 // 4. 放行 return invocation.proceed(); } }具体实现时有两个坑。第一SQL要正确注入到原有查询的WHERE位置最简单的方式是用一个自定义注解标记需要数据权限的Mapper方法拦截器只处理带注解的方法避免所有查询都被动态改动减少误伤。第二条件片段里的部门ID集合要提前查出来作为参数传入而不是在SQL里写子查询否则大表场景下性能堪忧。如果项目用的是MyBatis Plus也可以参考内置的DataScope注解方案。RuoYi等平台里面已经有现成封装直接拿来改成本地版本比自己从零写拦截器稳得多。注意数据权限是所有查询的公共底线动手改拦截器之前先把现有SQL的拼接位置梳理清楚最好写几个核心场景的集成测试用例。4. 快速开发平台的权限模块选型从若依到芋道4.1 主流开源平台的权限模块差异对比标题里提到的“快速开发平台”一般指集成了用户、角色、菜单、数据权限、代码生成器、后台管理界面的一整套脚手架。Java生态里这类平台非常多我列几个有代表性的重点看权限模块的差异。平台权限模型特点技术栈适合场景RuoYi若依RBAC 数据权限部门自定义Spring Boot MyBatis Vue2中小后台、管理系统起步RuoYi-Vue-Plus增强版支持多租户、数据权限更细Spring Boot 3 Sa-Token Vue3需要多租户、快速交付的中小项目JeecgBootRBAC 数据权限 在线报表Spring Boot MyBatis Plus中后台报表类项目yudao芋道源码RBAC 数据权限 多租户 工作流集成Spring Boot MyBatis Plus Vue3企业级ToB/ToG项目JeeSiteRBAC 数据权限 组织架构树Spring Boot MyBatis传统企业管理软件这里要特别提醒不要只看star数量要看权限模型是否贴近你的业务。比如RuoYi经典版的数据权限是经典的四模式简单够用RuoYi-Vue-Plus在多租户和行级过滤上更细致JeecgBoot强在报表和工作流联动芋道源码对多组织、多租户和企业工作流的支持更深入适合定制化要求高的项目。拿一个简单平台去硬扛复杂权限需求后面改起来会非常难受。4.2 选型之前先想清楚你的权限复杂度我的习惯是选型前先回答三个问题三个问题想清楚答案基本就出来了。第一项目是多租户还是单租户如果是SaaS产品租户隔离要做到哪一层租户ID绑定在数据行上还是只在登录用户层面隔离RuoYi经典版默认没有完整的多租户能力RuoYi-Vue-Plus和芋道则内置了这一点差异非常大。第二数据权限的角色分级有多细如果只是“管理员、普通用户”两档随便一个平台的RBAC都够用如果是五层组织架构加跨部门共享那就必须仔细研究平台的data_scope是固定枚举还是可扩展的注解逻辑。第三权限变更频率高不高高频变化的权限模型对缓存刷新、操作审计要求更高需要平台有完整的缓存失效方案和操作日志低频的话简单的Session权限缓存就够了。很多团队选型时只看界面好不好看、代码生成器强不强把权限模型这个最核心的根基忽略了这是我在不少项目里见过的最贵的一次学费。4.3 自研权限模块的时间成本估算聊完平台肯定有人问那我还不如自己写这个问题的答案是看你的核心业务是什么。我按一个标准后台管理模块来估算自研成本。表结构设计和基础RBAC编写大约2到3天登录认证接入2天菜单权限和接口校验1到2天数据权限拦截器实现2到3天前端权限路由和按钮控制2到3天操作日志、异常处理、缓存刷新这些加固工作3到5天。也就是说一个比较完整的权限模块一个人全职做保守估计需要三到四周而且这是没有踩大坑、一次设计到位的情况。三到四周什么概念对于大部分业务团队这个时间足够用快速开发平台把整个管理后台搭出来再把权限模块的核心需求定制完。当然如果团队本身就打算沉淀一套统一的脚手架给多个项目复用而且有专人长期维护那自研完全值得。关键是自己写完之后后面每个项目的权限需求变化都要有人持续接住这个隐性成本往往被低估。4.4 我推荐的选型思路最后说说我个人的推荐思路仅代表经验不绝对。如果是三五个人、两三个月内要交付的运营管理后台直接选RuoYi-Vue-Plus这类开箱即用的平台前端路由权限、后端数据权限、代码生成器都是现成的把业务表建好就能开工。如果是面向企业客户、权限和流程都很复杂的系统优先考虑芋道这类企业级平台或者从RuoYi-Vue版本fork出来做深度二次开发把数据权限和多租户提前设计进去。如果团队希望完全掌控代码也不想被开源平台的代码风格束缚那就按第三章的套路自研一个轻量版前期控制好范围不要把数据权限做成一个无限扩展的规则引擎。无论选哪条路权限模型的设计都不建议直接照抄网上的代码因为每个业务的“数据边界”完全不一样抄了表结构等于把别人的边界假设也抄进来了。5. 我在权限模型落地过程中踩过的坑5.1 角色变更后权限缓存不刷新有一次开发环境里测试人员反馈给一个账号加了角色刷新页面还是看不到新菜单。查数据库关联关系都正常最后定位问题出在缓存。我们在登录时把用户权限集合写入了Redis但管理员在后台调整角色权限时只改了数据库表没有同步删除对应账号的缓存key。所有用户重新登录之前权限就一直停留在旧状态。解决思路有两个层面一是角色权限变更时主动清理涉及该角色的所有用户缓存可以维护一张“角色与用户”的关系缓存索引二是给权限缓存加一个相对短的过期时间比如30分钟作为兜底。生产环境两个方案都建议上一个是实时性一个是安全性。权限这种数据宁可慢一点刷新也不能让旧权限长期残留。5.2 数据权限SQL拼接带来的注入风险数据权限拦截器最容易出安全事故的地方是SQL拼接。我见过一个项目为了防止跨部门查看数据手写了数据权限过滤但过滤条件是直接把前端传来的部门ID拼进SQLString sql SELECT * FROM orders WHERE dept_id IN ( request.getParameter(deptIds) );前端伪造一个deptIds参数传个1); DROP TABLE orders;--整个表都保不住。数据权限过滤属于系统级安全控制必须使用参数绑定让MyBatis通过#{}处理而不是${}直接拼。另外自定义数据权限的SQL片段也要做白名单校验只允许固定的表字段和操作符不能开放任意表达式。每次看到有人把安全控制代码写成可注入的字符串拼接我都想强调一遍权限模块是攻击者的重点目标写的时候就要当它在真实对抗环境里跑。5.3 前端隐藏按钮不等于后端安全这个坑看起来简单但我确实见过不止一次。前端根据权限列表判断是否显示“导出”按钮没权限的人看不到按钮于是大家觉得功能安全了。直到有一天运营部门反馈有人通过浏览器开发者工具构造了一个POST请求绕过前端界面直接把全量订单导出成了Excel。原因就是导出接口只做了菜单路由权限校验没做按钮级接口权限校验。按钮消失只影响用户体验和交互接口是否可调必须由后端说了算。正确姿势是前端按钮按权限渲染后端接口同时用RequiresPermission(order:export)标注并校验两个层面缺一不可。前端权限是为了让界面干净后端权限是为了让数据安全。5.4 第一版就上ABAC过度设计的代价最后一个坑有点反常识权限模型设计得越“高级”项目死得越快。有个朋友的项目一开始就想做一套万能权限系统直接引用了ABAC基于属性的访问控制把用户属性、资源属性、环境条件全部建模规则引擎都搭好了。结果上线后业务方提的权限需求其实很简单团队却花了大量时间在配规则和排查规则冲突上一个“为什么这个人的权限变了”的排错周期能拖一整天。我的建议是第一版从标准RBAC开始权限表只有用户、角色、菜单、关联关系架构上留好扩展点。当真正出现“同一个用户在同一角色下、因上下文不同权限不同”的需求时再局部引入ABAC或策略决策点。绝大多数业务系统在很长一段时间里RBAC加数据权限已经足够支撑。别为了技术的爽感把简单问题做成复杂系统。如果你正在做权限模块我的实操建议是先把基础RBAC的表结构理清楚再根据业务把数据权限范围定义好框架上用团队最熟悉的那一个能选快速开发平台就别从零造。权限模型真正难的不是代码而是把业务边界问清楚。想明白每一行数据在谁手里、谁批准、谁只能看不能改表结构自然就出来了。这套思路我用了几年项目基本都能平稳落地。也欢迎有不同经验的同行交流毕竟权限设计的本质是对业务规则理解的深度而不是对框架的堆砌。
阅读完成 · 觉得有帮助?
咨询建站