news 2026/9/8 7:16:32

RBAC 只能管按钮,真正难的是数据权限:从 ShiyuAdmin 拆解 Data Scope 设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RBAC 只能管按钮,真正难的是数据权限:从 ShiyuAdmin 拆解 Data Scope 设计

很多后台管理系统做到 RBAC,就觉得权限系统结束了。

比如给用户分配一个角色:

张三 ↓ 部门管理员 ↓ system:user:list

于是张三可以进入:

系统管理 → 用户管理

接口也能正常访问:

GET /api/v1/system/users

看起来权限没问题。

但真正麻烦的问题来了:

张三到底应该看到哪些用户?

是全公司的 10000 个用户?

还是自己部门的 30 个人?

还是自己部门以及下面所有子部门?

甚至只能看到自己?

这时候普通 RBAC 就不够用了。

因为 RBAC 解决的是:

你能不能调用这个接口?

而数据权限解决的是:

调用接口以后,你到底能看到哪些数据?

这两个问题完全不是一回事。

我最近在整理 ShiyuAdmin:

https://github.com/Rodert/ShiyuAdmin

里面就专门拆了一层:

Data Scope

今天我们从代码层面把这个东西讲透。


一、接口权限和数据权限有什么区别?

先看一个最简单的例子。

假设系统里有三个部门:

总公司 ├── 技术部 │ ├── Java 组 │ └── Go 组 │ └── 市场部

系统里有这些用户:

王总 总公司 张三 技术部 李四 Java组 王五 Go组 赵六 市场部

现在张三拥有:

system:user:list

意味着:

张三可以访问用户列表接口

但并不意味着:

张三可以看全公司所有用户

真正返回什么数据,还应该继续判断:

张三的数据范围是什么?

例如:

dept

那么张三只能看:

技术部

如果是:

deptAndChild

那么可以看:

技术部 Java组 Go组

如果是:

self

那就只能看:

张三自己

所以完整权限模型应该是两层:

第一层:功能权限 system:user:list system:user:create system:user:update system:user:delete ↓ 第二层:数据权限 all dept deptAndChild custom self

这才更接近企业后台真实的权限系统。


二、把 DataScope 放到角色上

一种比较常见的设计,是把数据权限范围放到角色上。

例如:

typeRolestruct{IDint64RoleCodestringRoleNamestringRoleKeystringDataScopestringStatusint}

那么不同角色可以配置:

超级管理员 DataScope = all

部门管理员:

DataScope = deptAndChild

普通员工:

DataScope = self

自定义区域负责人:

DataScope = custom

这样角色不只是决定:

能干什么

还决定:

能看到什么

三、常见的 5 种数据权限

我比较推荐后台系统至少支持下面几种。

1. all:全部数据

例如:

超级管理员 老板 系统管理员

可以看到所有用户。

逻辑:

ifdataScope=="all"{return&UserDataScope{All:true,}}

最终 SQL 基本没有额外限制:

SELECT*FROMsys_users;

2. dept:当前部门

比如张三属于:

技术部

那么:

DataScope = dept

最终查询:

SELECT*FROMsys_usersWHEREdept_code='TECH';

只返回技术部直属人员。


3. deptAndChild:部门及子部门

这个就有意思了。

例如组织结构:

技术部 TECH ├── Java组 JAVA │ ├── Java一组 JAVA_01 │ └── Java二组 JAVA_02 │ └── Go组 GO

张三属于:

TECH

如果权限是:

deptAndChild

那么最终应该允许:

TECH JAVA JAVA_01 JAVA_02 GO

SQL:

SELECT*FROMsys_usersWHEREdept_codeIN('TECH','JAVA','JAVA_01','JAVA_02','GO');

问题来了。

怎么找到所有子部门?


四、部门树怎么展开?

ShiyuAdmin 当前的思路比较直观。

先把全部部门查出来:

depts,err:=deptRepo.List(ctx)iferr!=nil{returnerr}

然后构造:

ParentCode → Children

映射。

代码可以写成:

childrenByParent:=make(map[string][]*Dept)for_,dept:=rangedepts{ifdept==nil{continue}childrenByParent[dept.ParentCode]=append(childrenByParent[dept.ParentCode],dept,)}

假设数据:

TECH JAVA parent=TECH GO parent=TECH JAVA_01 parent=JAVA JAVA_02 parent=JAVA

最终 Map 大概是:

TECH ├── JAVA └── GO JAVA ├── JAVA_01 └── JAVA_02

然后递归遍历。

varwalkfunc(parentCodestring)walk=func(parentCodestring){children:=childrenByParent[parentCode]for_,child:=rangechildren{ifchild==nil{continue}deptCodes[child.DeptCode]=struct{}{}walk(child.DeptCode)}}

最后调用:

walk("TECH")

就能拿到:

TECH JAVA JAVA_01 JAVA_02 GO

五、为什么用 map[string]struct{}?

这里顺带讲一个 Go 里面非常常见的小技巧。

我们需要保存:

部门编码集合

同时又要防止重复。

很多人第一反应:

[]string

例如:

vardeptCodes[]string

但是这样判断重复就比较麻烦。

更适合使用:

map[string]struct{}

例如:

deptCodes:=map[string]struct{}{}

添加:

deptCodes["TECH"]=struct{}{}deptCodes["JAVA"]=struct{}{}deptCodes["GO"]=struct{}{}

判断:

if_,exists:=deptCodes["JAVA"];exists{// 已存在}

最后再转:

funcmapKeys(valuesmap[string]struct{},)[]string{result:=make([]string,0,len(values))forkey:=rangevalues{result=append(result,key)}returnresult}

这是 Go 中实现:

Set

最常用的方式之一。


六、第四种:custom 自定义数据权限

企业后台经常还有一种需求:

华北负责人

但是组织架构可能并不是:

华北 ├── 北京 ├── 天津 └── 河北

甚至几个部门完全不在同一棵子树下面。

这时候就不能简单使用:

deptAndChild

因此可以增加:

custom

然后引入:

角色部门关联表

例如:

sys_role_depts

数据:

role_code dept_code north_admin beijing north_admin tianjin north_admin hebei

实体:

typeRoleDeptstruct{IDint64RoleCodestringDeptCodestring}

查询:

depts,err:=roleDeptRepo.GetRoleDepts(ctx,role.RoleCode,)

然后加入集合:

for_,dept:=rangedepts{ifdept==nil{continue}deptCodes[dept.DeptCode]=struct{}{}}

这样后台管理员可以自己勾选:

☑ 北京 ☑ 天津 ☑ 河北 ☐ 上海 ☐ 深圳

形成自定义数据权限。


七、第五种:self 仅本人

这是最严格的一种。

例如普通员工只能看到:

自己的数据

最终查询:

SELECT*FROMsys_usersWHEREuser_code='USER001';

在代码里,可以表示成:

UserDataScope{UserCode:"USER001",}

八、真正麻烦的问题:一个用户有多个角色怎么办?

这才是数据权限最值得讨论的地方。

假设张三同时有两个角色:

Role A DataScope = dept

以及:

Role B DataScope = custom

Role A 允许:

技术部

Role B 允许:

市场部

那么最终张三应该看到什么?

一般有两种模型。

一种是:

交集

也就是:

A ∩ B

权限越多,数据反而越少。

这种比较少见。

另一种是:

并集

也就是:

A ∪ B

张三最终能看到:

技术部 + 市场部

后台 RBAC 一般更适合使用:

并集模型

也就是:

用户拥有的多个角色权限累加

九、ShiyuAdmin 怎么合并多个 DataScope?

可以定义一个统一结构:

typeUserDataScopestruct{AllboolUserCodestringDeptCodes[]string}

然后遍历用户的所有角色。

roles,err:=userRoleRepo.GetUserRoles(ctx,userCode,)

初始化:

deptCodes:=map[string]struct{}{}includeSelf:=false

遍历:

for_,role:=rangeroles{ifrole==nil||role.Status!=1{continue}switchrole.DataScope{case"all":return&UserDataScope{All:true,},nilcase"dept":deptCodes[user.DeptCode]=struct{}{}case"deptAndChild":addDeptAndChildren(ctx,deptCodes,user.DeptCode,)case"custom":addRoleDepts(ctx,deptCodes,role.RoleCode,)default:includeSelf=true}}

注意这里一个非常关键的地方:

case"all":return

为什么直接返回?

因为:

all ∪ 任意集合 = all

既然其中一个角色已经拥有全部数据:

后面其他角色就没有必要继续计算

十、最终把权限模型变成 SQL 条件

前面做了一大堆事情,最后目的其实只有一个:

拼出正确的 WHERE 条件

例如最终算出来:

scope:=&UserDataScope{UserCode:"USER001",DeptCodes:[]string{"TECH","JAVA","GO",},}

Repository 层再应用它。

例如:

funcapplyUserScope(query*gorm.DB,userCodestring,deptCodes[]string,)*gorm.DB{switch{caseuserCode!=""&&len(deptCodes)>0:returnquery.Where("(user_code = ? OR dept_code IN ?)",userCode,deptCodes,)caseuserCode!="":returnquery.Where("user_code = ?",userCode,)caselen(deptCodes)>0:returnquery.Where("dept_code IN ?",deptCodes,)default:returnquery.Where("1 = 0")}}

这里面最值得注意的,其实是最后一句:

returnquery.Where("1 = 0")

十一、为什么没有权限时要WHERE 1 = 0

这是整套设计里我很喜欢的一个细节。

假设由于某个 Bug:

UserCode = "" DeptCodes = []

很多程序员可能会这么写:

returnquery

结果是什么意思?

意味着:

SELECT*FROMsys_users;

也就是:

权限计算异常,反而看到了全部数据。

这在权限系统里非常危险。

正确思路应该是:

我不知道你有什么权限 ↓ 默认不给权限

所以:

WHERE1=0

最终:

SELECT*FROMsys_usersWHERE1=0;

结果:

0 条数据

这就是安全领域一个非常重要的原则:

Fail Closed

出现异常时:

默认拒绝

而不是:

默认放行

十二、Fail Open 和 Fail Closed

两者区别非常简单。

Fail Open

权限服务异常:

权限判断失败 ↓ 算了,让他进去

这是:

Fail Open

优点:

业务可用性高

问题:

可能产生安全事故

Fail Closed

权限判断失败:

权限无法确定 ↓ 拒绝访问

这是:

Fail Closed

权限系统一般更应该选择:

Fail Closed

尤其:

后台管理 支付 财务 用户数据 订单数据 敏感操作

宁可少返回一些数据,也不要意外泄露全部数据。


十三、Controller 层不要自己拼部门 SQL

还有一个很重要的代码设计。

很多项目最后会写成:

funcListUser(c*gin.Context){ifrole=="admin"{}elseifrole=="dept_admin"{}elseifrole=="user"{}}

然后:

db.Where(...)

这种代码一多,后面基本没法维护。

更合理的分层应该是:

Controller ↓ DataScopeService ↓ UserDataScope ↓ Service ↓ Repository ↓ SQL

Controller 只负责:

我要查询用户列表

DataScopeService 负责:

这个用户能看哪些范围

Repository 负责:

把范围变成 SQL

职责完全分开。


十四、完整请求链路

最终一次:

GET /api/v1/system/users

可以经历:

HTTP Request ↓ JWT Middleware ↓ 获得 UserCode ↓ RBAC Permission Middleware ↓ 检查 system:user:list ↓ DataScopeService ↓ 读取 User ↓ 读取 User Roles ↓ 解析 Role.DataScope ↓ 计算部门集合 ↓ UserDataScope ↓ UserService ↓ UserRepository ↓ WHERE 条件 ↓ Database ↓ 只返回有权限的数据

这里可以非常清楚地看出来:

RBAC

和:

DataScope

其实解决的是完全不同的事情。


十五、如果部门有 10 万个怎么办?

再往生产环境考虑一步。

现在:

deptRepo.List(ctx)

把全部部门加载出来,再在内存构造:

Parent → Children

对于普通后台:

几十 几百 几千个部门

完全够用。

但如果是大型组织:

10 万部门节点

每次算数据权限都把整张部门表加载出来,就不合适了。

这时候可以继续优化。

比如:

方案一:Redis 缓存部门树

结构:

dept:children:TECH

保存:

JAVA GO AI

还可以使用:

Closure Table

例如专门建:

sys_dept_closure

保存:

ancestor descendant depth TECH TECH 0 TECH JAVA 1 TECH JAVA_01 2 TECH JAVA_02 2

查询技术部全部子部门:

SELECTdescendantFROMsys_dept_closureWHEREancestor='TECH';

不用递归。

这种方案非常适合:

层级复杂 查询特别频繁 组织架构非常大

的系统。


十六、也可以使用路径字段

还有一种常见方案:

Materialized Path

给部门保存:

path

例如:

技术部 /TECH/

Java:

/TECH/JAVA/

Java 一组:

/TECH/JAVA/JAVA_01/

查询所有技术部子部门:

SELECT*FROMsys_deptsWHEREpathLIKE'/TECH/%';

实现简单。

但是移动部门节点时:

整个子树的 path 都需要更新

所以各种树结构方案都有取舍。


十七、数据权限不仅能控制用户表

DataScope 真正有价值的地方在于:

它应该是一套通用能力。

例如用户:

WHEREdept_codeIN(...)

订单:

WHEREcreator_dept_codeIN(...)

客户:

WHEREowner_dept_codeIN(...)

操作日志:

WHEREoperator_dept_codeIN(...)

工单:

WHEREhandler_dept_codeIN(...)

最终:

DataScopeService

可以成为整个后台系统统一的数据安全边界。


十八、再复杂一点:业务归属不一定等于用户部门

这里还有一个真实项目经常遇到的问题。

例如销售系统。

张三属于:

北京销售部

但是他负责的客户可能包括:

北京 天津 河北

这时候:

用户 DeptCode

已经无法直接决定:

客户数据范围

需要进一步抽象:

组织权限 业务权限 资源权限

甚至最后会演变成:

RBAC + ABAC

ABAC:

Attribute-Based Access Control

也就是:

基于属性的访问控制

例如:

用户地区 = 华北 AND 订单地区 = 华北 AND 订单金额 < 100000

才能访问。

这也是权限系统从:

简单后台

逐渐演化成:

企业权限平台

的过程。


最后

很多人学习后台权限,只停留在:

User ↓ Role ↓ Menu

但真正到了项目里,RBAC 只是第一步。

完整的问题其实是:

这个人能不能进入页面? ↓ 这个人能不能调用接口? ↓ 这个人能不能点击按钮? ↓ 这个人调用接口以后能看到哪些数据? ↓ 这些数据里哪些又允许修改和删除?

所以一个稍微完整一点的后台权限体系,应该至少包括:

JWT + RBAC + Permission + Department + Data Scope + Repository SQL Filter

而且一定记住一个原则:

权限算不清楚的时候, 宁可返回 0 条数据, 也不要默认返回全部数据。

也就是:

Fail Closed

这类细节,才是真实后台系统和普通 CRUD Demo 之间的区别。

项目代码:

https://github.com/Rodert/ShiyuAdmin

如果你正在学 Go + Gin + Gorm,这个数据权限模块其实很适合自己完整实现一遍。

因为你会同时碰到:

RBAC 多角色合并 部门树 递归 Set 去重 Gorm 动态查询 Repository 分层 数据安全 Fail Closed

这些才是真正值得练的后端基本功。

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

小米首页静态复刻:HTML+CSS+JS布局与Spring Boot部署实战

简介&#xff1a;这份静态页面项目以小米官网首页为蓝本&#xff0c;面向初学HTML与CSS的前端爱好者&#xff0c;帮助练习页面结构搭建、样式设计与常见布局实现。压缩包共38个文件&#xff0c;包含2个HTML入口页面、8个CSS样式文件、多张JPG/PNG/SVG图片以及字体文件等&#x…

作者头像 李华
网站建设 2026/9/8 7:15:03

C#实现IIS监控插件:实时检查站点与应用程序池状态

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

作者头像 李华
网站建设 2026/9/8 7:14:40

PLC编程与通讯实战:从梯形图到Modbus TCP、Profinet的底层逻辑

前几天后台的热搜词几乎都被PLC相关的词占满了&#xff0c;从西门子、三菱到台达、汇川、信捷&#xff0c;从“PLC编程入门”到“Profinet通讯”“Modbus TCP服务器”“LabVIEW监控”……说实话&#xff0c;这个老物件在工控圈的生命力比很多人想象中旺盛得多。这几年我一直在现…

作者头像 李华
网站建设 2026/9/8 7:14:39

3D-ResNets-PyTorch实战:视频动作识别原理与迁移学习全指南

简介&#xff1a;这是面向计算机视觉和视频理解研究者的三维ResNet动作识别实现&#xff0c;源自CVPR 2018论文&#xff0c;核心解决视频中人类行为分类与时空特征提取问题&#xff0c;适合刚接触视频理解的研究生以及需要算法落地的工程师。代码基于PyTorch重构&#xff0c;支…

作者头像 李华
网站建设 2026/9/8 7:14:27

DeepSeek技术路线图解析:开源AGI与国产芯片机遇

这次我们来深入解读梁文锋在DeepSeek投资者交流会上的核心观点。这场3小时44分的交流不仅揭示了DeepSeek的技术路线图&#xff0c;更重要的是为国产芯片和开源生态指明了发展方向。从会议内容看&#xff0c;DeepSeek展现出了难得的克制——不盲目追求参数规模&#xff0c;而是聚…

作者头像 李华
网站建设 2026/9/8 7:10:15

Java并发编程全解析:从三性到锁、线程池与面试实战

最近在面试别人时碰到一个挺典型的场景&#xff1a;简历上写着"精通并发编程"&#xff0c;结果聊到volatile和synchronized的区别&#xff0c;答了"一个修饰变量一个修饰方法"就卡住了。再追问一句"那synchronized在JDK 1.6之后到底优化了什么"&…

作者头像 李华