浅析 Web 权限控制机制:从 RBAC、ABAC 到数据权限与字段权限

序言

  在 Web 应用开发中,身份认证和权限控制是两个非常基础、但又容易混淆的技术环节。

  例如,在一个企业管理系统中,用户登录成功后,可能需要查看订单、处理审批、管理部门成员等业务数据。此时,服务器不仅需要知道当前请求来自哪个用户,还需要进一步判断:这个用户是否可以访问对应的功能?是否可以查看这条订单?是否可以看到订单中的金额、利润等敏感字段?

  这些问题都属于权限控制的范畴。

  在实际业务中,权限控制通常不只是简单地判断用户是否拥有某个菜单或按钮。随着系统规模扩大,权限控制会逐渐细化为多个层次:

  • 功能权限:用户是否可以访问某个菜单、接口或执行某项操作
  • 数据权限:用户可以访问哪些业务数据,例如全部数据、本部门数据或本人数据
  • 字段权限:用户可以查看、修改或导出一条数据中的哪些字段

  围绕这些需求,常见的权限控制模型主要包括 RBAC(Role-Based Access Control,基于角色的访问控制)和 ABAC(Attribute-Based Access Control,基于属性的访问控制)。

  RBAC 通过用户、角色和权限之间的关系完成授权,适合大多数企业管理系统;ABAC 则根据用户、资源、操作和环境等属性动态判断是否允许访问,能够处理更加复杂的业务规则。

  需要注意的是,RBAC 和 ABAC 解决的是授权决策采用什么依据的问题,而数据权限和字段权限描述的是权限控制的粒度。它们并不是互相替代的关系,实际系统中往往需要组合使用。

  本文将从权限控制的基本概念出发,逐步介绍 RBAC、ABAC 的设计思想,再进一步讨论数据权限、字段权限以及它们在 Java 后端系统中的落地方式。

权限控制的基本概念

身份认证与权限控制

  首先,需要区分两个容易混淆的概念:身份认证(Authentication)和授权(Authorization)。

  身份认证解决的是“你是谁”的问题。

  例如,用户提交用户名和密码,服务器验证成功后,确认当前用户是张三。后续请求可以通过 Session 或 Token 识别张三的身份。

  授权解决的是“你能做什么”的问题。

  例如,张三登录系统后,系统还需要判断:

  • 张三是否可以访问订单管理菜单?
  • 张三是否可以执行订单删除操作?
  • 张三是否可以查看其他部门的订单?
  • 张三是否可以看到订单中的成本价和利润?
  • 张三是否可以导出包含客户手机号的 Excel 文件?

  因此,身份认证成功并不代表用户可以访问系统中的所有资源。认证只是确认用户身份,授权才是根据用户拥有的权限决定其可以执行哪些操作。

权限控制的三个层次

  从实际业务来看,权限控制可以按照控制对象划分为三个层次:功能权限、数据权限、字段权限。

功能权限

  功能权限主要解决用户能否使用某项功能的问题。

  例如,在订单管理模块中,可以将权限拆分为:

  • order:list:查询订单列表
  • order:detail:查看订单详情
  • order:create:创建订单
  • order:update:修改订单
  • order:delete:删除订单
  • order:export:导出订单

  如果张三只有订单查询权限,那么即使他能够正常登录系统,也不能执行订单删除操作。

数据权限

  数据权限主要解决用户能够访问哪些记录的问题。

  例如,两个用户都拥有订单查询权限,但他们能够查询到的订单范围可能不同:

  • 普通员工只能查看自己创建的订单
  • 部门负责人可以查看本部门所有员工的订单
  • 区域负责人可以查看所负责区域内的订单
  • 系统管理员可以查看全部订单

  这里的差异并不是功能权限,而是同一个查询功能所能访问的数据范围不同。

字段权限

  字段权限主要解决用户能够访问一条记录中的哪些字段的问题。

  例如,一条订单记录可能包含客户名称、客户手机号、订单金额、采购成本和利润等字段。普通员工可以查看客户名称和订单金额,但不能查看采购成本和利润;财务人员则可以查看这些财务字段。

  由此可以看出,数据权限控制的是“能看到哪些记录”,字段权限控制的是“能看到记录中的哪些信息”。

权限控制的整体流程

  在一个完整的 Web 请求中,身份认证和授权通常按照下面的顺序执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
客户端发起请求
|
v
身份认证:识别当前用户
|
v
构建用户上下文
|
v
功能权限校验:能否执行当前操作?
|
v
数据权限校验:允许访问哪些记录?
|
v
业务处理与数据查询
|
v
字段权限处理:哪些字段可以返回或修改?
|
v
返回响应

  当然,这只是一种便于理解的逻辑划分,并不意味着所有系统都必须严格按照同一个技术组件顺序实现。例如,数据权限条件可能在 SQL 查询阶段就已经生效,字段权限也可能在查询投影阶段完成。

  核心在于:每一个访问入口都必须在服务端完成必要的权限校验,不能仅依赖前端隐藏菜单、按钮或字段。

RBAC:基于角色的访问控制

为什么需要 RBAC?

  假设现在有一个企业管理系统,系统中有 1000 名员工,每名员工都需要使用不同的功能。

  如果直接给用户分配权限,那么管理员就需要逐个用户配置权限。例如,给张三配置订单查询、订单创建权限,给李四配置订单查询、订单审核权限,订单导出权限。

  随着用户数量增加,权限配置和维护会变得非常复杂。

  由于企业中的用户通常具有明确的岗位或职责,例如普通员工、部门负责人、财务人员和系统管理员。同一岗位的用户往往需要使用相同的一组功能。

  因此,我们可以引入一个中间层:角色。

  将一组权限分配给角色,再将角色分配给用户。这样,用户就可以通过角色间接获得权限,这便是 RBAC 的基本思想。

1
2
3
+------+  用户拥有角色    +------+  角色拥有权限    +------+       +----------------+
| 用户 | -------------> | 角色 | -------------> | 权限 | ----> | 系统资源或操作 |
+------+ +------+ +------+ +----------------+

  例如:

1
2
3
4
张三 -------- 普通员工 -------- 订单查询、订单创建
李四 -------- 部门负责人 ------ 订单查询、订单审核
王五 -------- 财务人员 -------- 订单查询、财务报表
赵六 -------- 系统管理员 ------ 用户管理、角色管理、权限管理

  当某个岗位的权限发生变化时,管理员只需要调整对应角色的权限,而不需要逐个修改该角色下的用户。

RBAC 的核心对象

  一个基础的 RBAC 模型通常包含用户(User)、角色(Role)和权限(Permission)三个核心对象。实际系统还会将权限关联到菜单、接口、按钮等资源。

User:用户

  用户是系统中的具体使用者,例如员工张三、部门负责人李四等。

  用户本身通常不直接维护一整套权限,而是通过关联角色获得权限。

Role:角色

  角色代表一组职责或岗位,例如普通员工、部门经理、财务人员和系统管理员。

  角色的作用是对权限进行分组,让权限分配和维护更加方便。

Permission:权限

  权限表示系统允许执行的某项操作,通常可以对应菜单、按钮、接口或业务操作。例如:

权限标识 说明
user:list 查询用户
user:create 创建用户
user:update 修改用户
order:list 查询订单
order:export 导出订单

  在设计时,权限一般定义稳定且唯一的标识,而不是直接使用菜单名称作为权限判断依据。菜单名称可能因为产品调整而变化,但权限标识通常应保持稳定。

Resource:资源

  资源是权限所保护的对象,可以是菜单、页面、按钮、API 接口或业务资源。

  在一些系统中,Resource 和 Permission 会合并设计。例如,直接将 order:delete 作为一条权限记录;在另一些系统中,则会分别维护资源和操作,再组合形成权限。

  两种方式都可以,关键是明确系统中“资源”和“可执行操作”的边界,避免模型过度复杂。

RBAC 的关系与表结构设计

  在基础 RBAC 模型中,用户与角色通常是多对多关系,一个用户可以拥有多个角色,一个角色也可以分配给多个用户。

  角色与权限同样是多对多关系,一个角色可以拥有多个权限,一条权限也可以分配给多个角色。

1
2
3
4
5
6
7
8
+----------+  1:N  +---------------+  N:1  +----------+
| sys_user | ----> | sys_user_role | ----> | sys_role |
+----------+ +---------------+ +----------+
| 1:N
v
+----------------+ N:1 +---------------------+
| sys_permission | <---- | sys_role_permission |
+----------------+ +---------------------+

  常见的表结构可以设计为:

用户表:sys_user

字段 说明
id 用户 ID
username 用户名
dept_id 所属部门
status 用户状态

角色表:sys_role

字段 说明
id 角色 ID
role_name 角色名称
role_code 角色编码

权限表:sys_permission

字段 说明
id 权限 ID
permission_name 权限名称
permission_code 权限标识
resource_type 资源类型
resource_path 资源路径

用户角色关联表:sys_user_role

字段 说明
user_id 用户 ID
role_id 角色 ID

角色权限关联表:sys_role_permission

字段 说明
role_id 角色 ID
permission_id 权限 ID

  以上是一个基础模型。实际项目还可能需要增加租户 ID、数据范围、角色状态、权限类型、创建人和审计字段等信息。

RBAC 的授权流程

  以用户访问订单删除接口为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
请求:DELETE /orders/1001
|
v
身份认证:获取当前用户 ID
|
v
查询用户拥有的角色
|
v
查询角色关联的权限
|
v
判断是否包含 order:delete
|
+--+--+
| |
有 无
| |
v v
执行业务 返回 403

  在 Java 项目中,可以将权限校验统一交给 Spring Security、Sa-Token 等安全框架处理,一般也可以通过自定义注解和拦截器实现。

  例如,使用权限注解表达接口要求:

1
2
3
4
5
6
@PreAuthorize("hasAuthority('order:delete')")
@DeleteMapping("/orders/{id}")
public Result<Void> delete(@PathVariable Long id) {
orderService.delete(id);
return Result.success();
}

  这里的注解只是声明接口所需的权限,真正的权限数据仍然需要由系统根据当前用户的角色或授权关系加载。

扩展模型:RBAC0、RBAC1、RBAC2 和 RBAC3

  在介绍 RBAC 时,经常会看到 RBAC0、RBAC1、RBAC2 和 RBAC3 这几个概念。它们描述的是 RBAC 模型在基础角色关系之上的扩展。

RBAC0:基础模型

  RBAC0 是最基础的 RBAC 模型,包含用户、角色和权限三个核心对象,以及用户与角色、角色与权限之间的关联关系。

  大多数常见后台管理系统中的用户、角色、菜单权限设计,都可以看作 RBAC0 的一种工程实现。

RBAC1:角色继承

  RBAC1 在基础模型上增加了角色继承关系。

  例如,系统中存在普通员工、部门经理和部门总监三个角色。部门经理除了拥有自己的权限外,还可能继承普通员工的权限;部门总监则可以继承部门经理的权限。

1
2
3
4
5
6
7
部门总监
|
v
部门经理
|
v
普通员工

  角色继承可以减少重复配置,但也会增加权限关系的复杂度。实际使用时,需要明确继承方向,并避免循环继承。

RBAC2:角色约束

  RBAC2 在基础模型上增加了约束条件,用于限制用户与角色、角色与权限之间的分配关系。

  例如:

  • 互斥角色:同一个用户不能同时拥有某些互相冲突的角色,例如付款申请人和付款审批人
  • 基数约束:某个角色最多只能分配给指定数量的用户
  • 先决条件约束:用户必须先拥有某个角色,才能获得另一个角色

  这些约束能够将组织管理中的职责分离要求纳入权限模型。

RBAC3:继承与约束的组合

  RBAC3 可以理解为同时具备 RBAC1 的角色继承和 RBAC2 的约束能力。

  需要注意,RBAC0~RBAC3 是对模型能力的分类,并不意味着实际项目必须完整实现所有模型。对于大多数后台管理系统,基础 RBAC 通常已经能够满足功能权限管理需求。

RBAC 的优缺点与适用场景

  RBAC 的优点主要包括:

  • 结构清晰:用户、角色、权限之间的关系明确,容易理解和维护
  • 便于批量授权:可以通过角色统一管理一组权限
  • 适合组织岗位管理:企业中的岗位职责通常相对稳定,容易映射为角色
  • 易于与安全框架集成:Spring Security、Sa-Token 等框架都可以支持基于权限标识的访问控制

  但 RBAC 也存在一定局限:

  • 角色数量可能膨胀:当授权规则越来越细时,可能需要创建大量组合角色
  • 难以表达动态业务条件:例如只有订单所属部门与当前用户部门相同,并且订单处于待审核状态时,才允许审核
  • 角色不等于数据范围:拥有订单查询角色,并不意味着用户应该看到全部订单
  • 环境因素难以直接表达:例如仅允许在工作时间或指定网络环境下执行某项操作

  因此,RBAC 非常适合解决相对稳定的功能授权问题,但当授权规则依赖用户、资源和环境的动态属性时,就需要考虑 ABAC。

ABAC:基于属性的访问控制

为什么需要 ABAC?

  先看一个业务场景:某企业系统中有一个订单审核功能,部门负责人可以审核本部门员工提交的订单,但需要同时满足以下条件:

  • 当前用户是部门负责人
  • 当前订单属于该用户负责的部门
  • 当前订单状态为待审核
  • 当前时间处于允许审核的时间范围内

  如果只使用 RBAC,我们可能会给部门负责人分配 order:audit 权限。

  但这个权限只能说明用户具备订单审核功能,并不能单独表达“只能审核本部门、处于待审核状态、且在指定时间内的订单”。

  如果继续通过角色细分,例如创建“部门 A 审核员”“部门 B 审核员”“夜间审核员”等角色,角色数量很容易膨胀,而且订单状态变化、部门调整等情况也会让角色维护变得复杂。

  这时,可以将用户、订单、操作和环境中的属性作为授权判断依据,根据规则动态决定是否允许访问。这便是 ABAC 的基本思想。

什么是 ABAC?

  ABAC(Attribute-Based Access Control,基于属性的访问控制)是一种根据属性和策略进行授权决策的访问控制模型。

  与 RBAC 主要通过角色关联权限不同,ABAC 会综合考虑多个维度的属性,再根据预先定义的策略判断当前请求是否允许执行。

  ABAC 通常包含四类属性:

  • Subject:主体属性,即发起请求的用户或服务
  • Object:客体属性,即被访问的资源
  • Action:操作属性,即当前请求要执行的动作
  • Environment:环境属性,即请求发生时的环境信息

  可以简单理解为:谁,在什么环境下,对什么资源,执行什么操作。

ABAC 的四类属性

Subject:主体属性

  主体通常是当前登录用户,也可以是某个服务账号。

  常见属性包括:

属性 示例
userId 1001
role 部门负责人
deptId 200
tenantId tenant-a
jobLevel P6
region 华东区域

  这些属性可以来自用户表、角色关系、组织架构、租户信息或身份认证上下文。

Object:客体属性

  客体是被访问的资源,例如订单、合同、客户或审批单。

  常见属性包括:

属性 示例
orderId 90001
deptId 200
tenantId tenant-a
status PENDING
amount 50000
ownerId 1002

  需要注意,资源属性通常应来自服务端可信的数据源,而不能直接相信客户端提交的参数。例如,用户不能通过修改请求参数中的 deptId,就把订单所属部门伪造成自己所在的部门。

Action:操作属性

  Action 表示用户当前要执行的操作,例如:

  • 查询订单
  • 创建订单
  • 修改订单
  • 删除订单
  • 导出订单

  在设计时,可以将操作抽象成稳定的动作标识,例如 order:read、order:update 和 order:audit。

Environment:环境属性

  Environment 表示请求发生时的上下文信息,例如:

  • 当前时间
  • 请求来源 IP
  • 设备类型
  • 网络区域
  • 是否处于工作时间
  • 风险等级

  例如,企业系统可以规定:只有在公司内网,并且处于工作时间时,才允许访问某些高敏感数据。

ABAC 的授权流程

  ABAC 的核心不是简单查询用户拥有哪个角色,而是获取当前请求涉及的属性,并根据策略完成判断。

ABAC 授权流程

  继续使用订单审核场景,策略可以描述为:

1
2
3
4
5
允许审核订单,当且仅当:
当前用户是部门负责人
AND 当前用户部门 ID = 订单部门 ID
AND 订单状态 = 待审核
AND 当前时间处于允许审核时间范围

  这样,系统不需要为每个部门、每种订单状态创建不同角色,而是根据实际属性动态判断。

ABAC 的策略设计

  策略(Policy)是 ABAC 的核心。它将业务中的授权规则抽象成可以被系统执行的条件。

  一条策略通常需要明确以下内容:

  • 主体范围:哪些用户或用户属性适用
  • 资源范围:策略保护哪些资源
  • 操作范围:策略约束哪些动作
  • 条件表达式:需要满足哪些属性条件
  • 决策结果:满足条件时允许还是拒绝

  例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Policy: 部门负责人审核本部门订单

Subject:
role = DEPT_MANAGER

Object:
resourceType = ORDER

Action:
action = AUDIT

Condition:
subject.deptId == object.deptId
AND object.status == PENDING

Effect:
ALLOW

  实际系统还需要考虑多条策略同时匹配时的处理方式。常见的策略组合方式包括:

  • 默认拒绝:没有匹配到允许策略时,拒绝访问
  • 拒绝优先:只要有一条明确拒绝策略匹配,就拒绝访问
  • 允许优先:存在允许策略时即可允许,但需要谨慎使用
  • 首条匹配:按照策略优先级或顺序,采用第一条匹配策略

  对于企业系统,通常建议采用默认拒绝的思路,并明确处理允许策略与拒绝策略冲突的规则。不能因为策略未配置或策略引擎异常,就默认放行。

ABAC 的优缺点与适用场景

  ABAC 的优点主要包括:

  • 表达能力强:可以组合用户、资源、操作和环境属性
  • 适合动态规则:能够处理部门、资源状态、金额、时间等业务条件
  • 减少角色膨胀:不必为每一种属性组合创建独立角色
  • 策略可集中管理:可以将授权规则从业务代码中抽离,统一维护和审计

  但 ABAC 也会带来一些复杂度:

  • 属性来源复杂:需要明确属性从哪里获取、是否可信以及如何更新
  • 策略设计成本较高:规则越多,策略之间的关系越难理解
  • 调试难度较大:一次授权失败可能由多个属性或策略条件导致
  • 性能需要关注:如果每次请求都访问多个服务获取属性,授权链路可能变长
  • 策略治理要求高:需要版本管理、测试、审计和异常处理机制

  因此,ABAC 更适合授权规则复杂、业务属性丰富、需要动态判断的系统。对于权限关系简单、岗位职责稳定的后台管理系统,没有必要为了使用 ABAC 而引入复杂的策略引擎。

RBAC 与 ABAC 的对比及组合使用

RBAC VS ABAC

  RBAC 和 ABAC 并不是简单的高低级关系,它们主要区别在于授权决策的依据不同。

对比维度 RBAC ABAC
核心依据 用户拥有的角色及角色权限 主体、资源、操作和环境属性
授权方式 通过角色间接获得权限 根据策略动态计算
规则表达 相对固定 灵活,可组合多种条件
维护方式 主要维护用户、角色、权限关系 维护属性和策略
适用场景 岗位职责相对稳定的系统 业务条件复杂、动态授权场景
主要挑战 角色数量可能膨胀 策略和属性治理复杂

  例如,用户是否可以访问订单审核功能,可以先通过 RBAC 判断其是否拥有 order:audit 权限;而用户是否可以审核当前这张订单,则可以进一步通过 ABAC 判断部门、订单状态和环境条件。

RBAC 与 ABAC 如何组合?

  在实际系统中,通常没有必要在 RBAC 和 ABAC 之间二选一。更常见的方式是让两者承担不同层次的职责。

  例如,订单审核请求可以按照以下流程处理:

链路

  其中,RBAC 负责判断用户是否具备使用该功能的基础资格,ABAC 负责判断当前用户是否满足具体业务条件。

  这种组合方式能够兼顾 RBAC 的简单性和 ABAC 的灵活性。

不同业务场景如何选择?

  对于普通后台管理系统,如果主要需求是菜单、按钮和接口权限,采用 RBAC 通常就足够了。

  对于多租户 SaaS 系统,可以使用 RBAC 管理租户内的角色和功能权限,再结合租户属性、部门属性和数据范围控制访问边界。

  对于审批、金融、合同、订单等业务规则较复杂的系统,可以在 RBAC 的基础上引入 ABAC,处理资源状态、金额、组织关系、时间和风险等级等动态条件。

  选择模型时,应该先梳理业务授权规则,再决定是否需要引入更复杂的模型,而不是反过来为了使用某种模型而设计业务。

数据权限:控制用户可以访问哪些数据

什么是数据权限?

  前面介绍的 RBAC 和 ABAC,主要讨论系统应该依据什么信息做出授权决策。接下来,我们进一步讨论一个企业系统中非常常见的问题:数据权限。

  假设系统中有一个订单列表接口:

1
GET /orders

  普通员工、部门负责人和系统管理员都拥有 order:list 权限,因此他们都可以访问这个接口。

  但这并不意味着他们应该查询到相同的数据:

  • 普通员工只能查询自己创建的订单
  • 部门负责人可以查询本部门员工创建的订单
  • 区域负责人可以查询所负责区域的数据
  • 系统管理员可以查询全部订单

  这便是数据权限要解决的问题:在用户具备某项功能权限之后,进一步限制其可以访问的业务数据范围。

  从数据库查询角度来看,数据权限通常表现为对 SQL 查询条件的约束,也就是常说的行级数据访问控制(Row-Level Security)。

常见的数据权限范围

  企业管理系统中,比较常见的数据范围包括:

数据范围 含义 示例
全部数据 可以访问当前授权边界内的全部数据 系统管理员
本部门数据 只能访问当前用户所属部门的数据 部门负责人
本部门及下级部门 可以访问本部门及其下属部门的数据 区域或事业部负责人
本人数据 只能访问由当前用户负责或创建的数据 普通员工
自定义数据 可以访问指定部门或指定范围的数据 特殊业务角色

  这里的“全部数据”并不一定意味着可以访问整个系统的数据。在多租户系统中,它通常仍然受租户边界约束,只表示当前租户范围内的全部数据。

数据权限与 RBAC 的关系

  数据权限通常可以与角色关联。例如,给不同角色配置不同的数据范围:

  • 角色:普通员工 –> 数据范围:本人数据
  • 角色:部门负责人 –> 数据范围:本部门及下级部门
  • 角色:系统管理员 –> 数据范围:全部数据

  这样,用户通过角色获得功能权限的同时,也可以获得相应的数据范围。

  当然,角色和数据范围并不是天然绑定的。实际系统可以选择以下几种方式:

  • 角色绑定数据范围:每个角色配置一个或多个数据范围,简单直观,适合常见后台系统
  • 用户单独配置数据范围:当同一角色下不同用户的数据范围不同时,可以增加用户级别的授权配置
  • 通过 ABAC 策略计算数据范围:当数据范围依赖用户属性、资源属性或业务条件时,可以通过策略动态计算

  如果一个用户拥有多个角色,还需要明确多个角色的数据范围如何合并。例如,普通员工角色只能查看本人数据,部门负责人角色可以查看本部门数据,那么最终范围可能取两个角色授权范围的并集。

  不过,并集并不是所有业务都适用。如果系统存在明确的禁止规则、职责分离或敏感数据限制,就需要设计更明确的冲突处理机制。不能简单地认为角色越多,权限就一定越大。

数据权限落地方案

  在 Java 后端项目中,数据权限通常需要与业务查询结合。常见实现方式主要有以下几种。

方式一:在业务代码中手动控制

  最直接的方式是在 Service 层根据当前用户的数据范围构造查询条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
public List<Order> listOrders(Long userId, Long deptId, String scope) {
LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();

if ("SELF".equals(scope)) {
wrapper.eq(Order::getCreateBy, userId);
} else if ("DEPT".equals(scope)) {
wrapper.eq(Order::getDeptId, deptId);
} else if ("ALL".equals(scope)) {
// 不额外限制数据范围
}

return orderMapper.selectList(wrapper);
}

  这种方式容易理解,也便于针对业务做特殊处理。

  但如果系统中有几十个甚至几百个查询接口,每个接口都重复编写数据权限判断,就容易出现代码冗余和遗漏。尤其是新增接口时,开发人员可能忘记添加数据权限条件,造成越权访问。

方式二:通过 MyBatis 拦截器统一处理

  为了减少业务代码中的重复逻辑,可以考虑在 MyBatis 层统一处理数据权限。

  基本思路是:

  1. 用户请求进入系统后,获取当前用户的身份和组织属性
  2. 根据角色或策略计算当前用户的数据范围
  3. 在执行 SQL 前识别需要进行数据权限控制的查询
  4. 将数据范围转换为 SQL 条件
  5. 将条件追加到原始 SQL 中,再交给数据库执行

  例如,原始 SQL 为:

1
2
3
SELECT *
FROM orders
WHERE status = 'PENDING';

  如果当前用户只能查看本部门数据,则最终 SQL 可以变为:

1
2
3
4
SELECT *
FROM orders
WHERE status = 'PENDING'
AND dept_id = 200;

  如果用户只能查看本人数据,则可以增加:

1
AND create_by = 1001

  这种方式能够将通用的数据权限逻辑集中处理,减少业务代码重复。

  不过,SQL 拦截并不是没有成本。实际实现时需要关注:

  • SQL 解析:不能简单地通过字符串拼接处理所有 SQL,尤其是多表 JOIN、子查询、UNION 和复杂条件。
  • 表别名:同一张表可能在 SQL 中出现多次,需要准确识别受控表及其别名。
  • 条件优先级:追加条件时必须正确处理 AND、OR 和括号,避免改变原 SQL 语义。
  • 分页与统计:分页查询、COUNT 查询和聚合查询都需要保持数据范围一致。
  • 写操作:UPDATE、DELETE 不能只在列表查询时控制。更新和删除目标记录时,也必须验证记录是否处于用户授权范围内。
  • 绕过风险:不能让某些 Mapper 或自定义 SQL 因未经过拦截器而意外绕过数据权限。

  因此,拦截器适合承担统一的通用规则,但不能替代业务层对关键资源的授权校验。

方式三:通过 MyBatis-Plus 等框架扩展(常用)

  如果项目使用 MyBatis-Plus,可以基于其拦截器机制扩展数据权限处理逻辑,也可以参考项目中已有的数据权限组件。

  例如,部分后台管理框架会通过注解标记需要进行数据权限控制的方法,再由拦截器根据当前用户的角色数据范围生成 SQL 条件。

1
2
3
4
@DataPermission
public List<Order> listOrders(OrderQuery query) {
return orderMapper.selectList(buildWrapper(query));
}

  这里的注解仅用于说明设计思路,具体注解名称和实现方式取决于项目使用的框架。

  需要强调的是,数据权限组件必须有清晰的默认行为:对于明确要求数据权限的业务接口,如果无法计算出授权范围,应当拒绝访问或返回空结果,而不是不加限制地查询全部数据。

多租户系统中的数据权限

  在多租户 SaaS 系统中,还需要区分租户隔离和租户内部的数据权限。

  租户隔离解决的是不同租户之间的数据边界问题。

  例如,租户 A 的用户不能访问租户 B 的订单。常见做法是在业务表中增加 tenant_id,查询时自动增加租户条件:

1
2
3
SELECT *
FROM orders
WHERE tenant_id = 'tenant-a';

  租户内部的数据权限解决的是同一个租户中,不同用户可以访问哪些数据的问题。

  例如,租户 A 内部的普通员工只能查看本人订单,部门负责人可以查看本部门订单。

  因此,最终查询条件可能同时包含租户条件和数据范围条件:

1
2
3
4
SELECT *
FROM orders
WHERE tenant_id = 'tenant-a'
AND dept_id = 200;

  两者的职责不同,不能因为系统已经实现了 tenant_id 自动注入,就认为数据权限也已经完成。

  在实现时,建议将租户隔离作为基础安全边界,并确保数据权限条件不能覆盖、删除或绕过租户条件。对于跨租户管理等特殊能力,应当通过独立、明确且经过审计的授权入口实现。

字段权限:控制用户可以访问哪些字段

什么是字段权限?

  数据权限解决的是用户可以访问哪些记录,但在很多业务系统中,即使用户有权查看某条记录,也不代表他有权查看记录中的所有字段。

  例如,一条客户信息可能包含:

1
2
3
4
5
6
7
8
{
"id": 1001,
"name": "张三",
"phone": "13800000000",
"idCard": "110101199001011234",
"address": "北京市朝阳区",
"creditLimit": 500000
}

  普通客服可能只需要查看客户姓名和手机号;财务人员需要查看授信额度;其他岗位则不应该看到身份证号等敏感信息。

  如果所有用户都返回完整对象,即使前端页面隐藏了某些字段,用户仍然可以通过浏览器开发者工具或直接调用接口获取这些数据。

  因此,字段权限需要在服务端生效,控制哪些字段可以被查询、查看、修改或导出。

字段权限的常见类型

  字段权限并不只是“显示或隐藏”这么简单,实际业务中通常包含多个维度。

字段可见权限

  控制用户是否可以查看某个字段。

  例如,普通员工可以查看订单金额,但不能查看采购成本和利润。

字段脱敏权限

  用户可以查看字段,但只能看到经过脱敏处理的内容。

  例如:

1
2
3
4
5
原始手机号:13800000000
脱敏手机号:138****0000

原始身份证:110101199001011234
脱敏身份证:110***********1234

  脱敏与完全不可见不同。脱敏通常适用于业务仍然需要部分信息的场景。

字段编辑权限

  控制用户是否可以修改某个字段。

  例如,普通员工可以修改客户联系地址,但不能修改客户信用额度;财务人员可以维护信用额度。

  需要注意,编辑权限必须在服务端校验。即使前端将输入框设置为只读,用户仍然可以构造请求直接提交字段值。

字段查询与导出权限

  有些字段不允许出现在列表或详情中,但可能允许特定岗位通过专门的查询入口访问;也有一些字段可以在页面查看,却不允许导出。

  因此,字段权限最好明确区分页面展示、查询过滤、修改和导出等操作,而不是仅仅使用一个 visible 属性代表所有权限。

字段权限如何设计?

  字段权限可以围绕资源、字段、操作和授权主体进行设计。

  例如,订单模块包含以下字段:

字段标识 字段名称 敏感级别
orderNo 订单编号 普通
customerName 客户名称 普通
phone 客户手机号 敏感
amount 订单金额 敏感
cost 采购成本 高敏感
profit 订单利润 高敏感

  可以通过角色或策略配置字段访问规则:

角色 客户手机号 订单金额 采购成本 订单利润
普通员工 脱敏 可见 不可见 不可见
部门负责人 可见 可见 不可见 不可见
财务人员 可见 可见 可见 可见

  如果字段规则只与岗位角色有关,可以采用 RBAC 的方式配置;如果规则还依赖数据归属、业务状态或环境属性,则可以结合 ABAC 进行动态判断。

  例如,只有订单所属部门的财务负责人,才能查看该订单的采购成本和利润。

字段权限落地方案

  字段权限的实现位置可以有多个选择,实际项目需要结合数据访问方式、性能和安全要求决定。

方式一:在 DTO/VO 返回阶段过滤

  一种比较常见的方式是:业务查询完成后,将实体转换为 DTO 或 VO,再根据当前用户的字段权限决定哪些字段可以返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public OrderVO convert(Order order, FieldPermission permission) {
OrderVO vo = new OrderVO();
vo.setOrderNo(order.getOrderNo());
vo.setCustomerName(order.getCustomerName());
vo.setAmount(order.getAmount());

if (permission.canRead("cost")) {
vo.setCost(order.getCost());
}

if (permission.canRead("profit")) {
vo.setProfit(order.getProfit());
}

if (permission.canRead("phone")) {
vo.setPhone(order.getPhone());
} else if (permission.canReadMasked("phone")) {
vo.setPhone(maskPhone(order.getPhone()));
}

return vo;
}

  这种方式容易理解,也比较容易与现有 Java 项目结合。实体对象可以保留完整数据,而返回给不同用户的 VO 则根据权限进行处理。

  不过,必须确保所有返回入口都经过字段权限处理,包括列表、详情、导出、消息推送等。只在某一个详情接口中处理是不够的。

方式二:通过注解或统一转换器处理

  如果系统中有大量 DTO/VO,可以进一步封装字段权限处理组件。

  例如,通过注解标记字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class OrderVO {

private String orderNo;

@FieldPermission("order:read:amount")
private BigDecimal amount;

@FieldPermission("order:read:cost")
private BigDecimal cost;

@FieldPermission(value = "order:read:phone", mask = true)
private String phone;
}

  在对象转换或响应序列化阶段,统一读取字段上的权限标识,并根据当前用户的权限决定保留原值、脱敏还是置空。

  这种方案能够减少重复代码,但需要谨慎处理序列化行为、嵌套对象、集合、泛型以及不同接口的字段权限差异。

  此外,不能把“字段置空”作为唯一的安全策略。如果业务逻辑后续仍然能够读取该字段并执行敏感操作,就需要在业务层单独进行授权校验。

方式三:在 SQL 查询阶段控制字段

  另一种方式是在查询阶段就只读取用户有权访问的字段。

  例如,普通员工查询订单时,只查询:

1
2
3
4
5
6
SELECT
order_no,
customer_name,
amount
FROM orders
WHERE id = 1001;

  财务人员则可以查询:

1
2
3
4
5
6
7
8
SELECT
order_no,
customer_name,
amount,
cost,
profit
FROM orders
WHERE id = 1001;

  这种方式可以减少敏感数据从数据库传递到应用层的范围,也可能降低不必要的数据读取。

  但动态 SQL 投影会增加查询维护成本,尤其是多表关联、复杂报表和不同用户字段组合较多时。对于大多数业务系统,可以优先考虑在 DTO/VO 层统一处理;对于高敏感数据或数据量较大的场景,再考虑在查询层进一步收敛字段。

数据权限 VS 字段权限

  数据权限和字段权限都属于细粒度授权,但控制的对象不同。

对比维度 数据权限 字段权限
控制粒度 行级(记录) 列级(字段)
解决问题 用户可以访问哪些记录 用户可以访问记录中的哪些字段
常见实现 SQL WHERE 条件、数据范围策略 DTO/VO 过滤、脱敏、动态字段投影
典型场景 只能查看本部门订单 可以看订单,但不能看采购成本
重点风险 越权查询、越权修改、越权删除 敏感字段泄露、越权修改、导出泄露

  举个例子:用户张三拥有订单查询权限,数据权限规定他只能查看本部门订单;字段权限规定他只能查看订单编号、客户名称和订单金额。

  那么,系统最终返回的数据既要满足部门范围限制,也要满足字段访问限制。两种权限需要同时生效。

完整权限体系的设计与落地

以多租户企业管理系统为例

  前面分别介绍了 RBAC、ABAC、数据权限和字段权限。接下来,我们将它们放到一个完整的业务系统中,看看如何协同工作。

  假设现在有一个多租户企业管理平台,系统包含用户、部门、订单和审批等模块。

  不同用户的权限需求如下:

用户类型 功能权限 数据范围 字段范围
普通员工 查询、创建订单 本人订单 普通业务字段
部门负责人 查询、审核订单 本部门及下级部门 可查看部分敏感字段
财务人员 查询、导出订单 授权范围内的订单 可查看财务字段
租户管理员 租户内用户和业务管理 当前租户全部数据 根据业务职责配置
平台管理员 平台级管理 经明确授权的跨租户范围 按平台管理职责配置

  这里需要特别注意,平台管理员并不应该因为拥有平台管理角色,就自动获得所有租户业务数据的无限制访问能力。跨租户访问属于高风险能力,应当单独授权、记录审计日志,并尽量缩小使用范围。

整体权限控制架构

  一个相对完整的权限控制流程可以抽象为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
          客户端请求
|
v
身份认证
|
v
用户上下文
(userId / tenantId / deptId)
|
v
功能权限校验
(RBAC)
|
v
业务属性判断
(ABAC)
|
v
数据范围计算
(本人/部门/自定义)
|
v
数据库查询
(租户条件 + 数据条件)
|
v
字段权限处理
(可见/脱敏/编辑/导出)
|
v
返回响应

  在这个架构中,各个部分的职责相对明确:

  • 身份认证:确认当前请求对应哪个用户
  • RBAC:判断用户是否具备访问某个功能或执行某项操作的基础权限
  • ABAC:根据用户、资源、操作和环境属性判断是否满足动态业务条件
  • 数据权限:计算用户可以访问的记录范围,并将范围落实到查询和写操作
  • 字段权限:控制敏感字段的读取、展示、修改和导出

  需要说明的是,数据权限和字段权限既可以由 RBAC 配置,也可以由 ABAC 策略计算。它们不是独立于 RBAC/ABAC 的另一套授权模型,而是授权控制在不同数据粒度上的具体应用。

Java 技术栈中的实现思路

  对于常见的 Spring Boot 后端系统,可以按照职责拆分权限组件。

层次 可选技术或实现方式 主要职责
身份认证 Spring Security、Sa-Token、自定义过滤器 登录状态、Token、用户身份
功能权限 Spring Security、Sa-Token、自定义注解 菜单、接口、操作权限
ABAC 策略 自定义策略服务、表达式引擎或策略引擎 属性获取、策略匹配和决策
数据权限 MyBatis 拦截器、MyBatis-Plus 插件、业务查询条件 行级数据范围控制
字段权限 DTO/VO 转换器、字段权限组件、动态查询投影 字段过滤、脱敏和修改校验
租户隔离 租户上下文、ORM 拦截器、数据库隔离方案 租户边界控制
审计 操作日志、授权决策日志 记录敏感操作和授权结果

  这里列出的是常见的技术实现方向,并不是要求一个项目必须引入所有组件。对于规模较小的系统,可以自定义注解来实现 RBAC,再通过统一的数据权限组件实现行级控制;当业务规则变得复杂时,再逐步引入 ABAC 策略。

权限控制中的几个关键问题

权限缓存与实时生效

  实际系统中,用户的角色、权限和数据范围通常不会每次请求都从数据库重新查询。为了减少数据库访问,系统可能会将用户权限缓存到 Redis 或本地缓存中。

  但缓存也会带来一个问题:当管理员修改用户角色或权限时,如何让变更及时生效?

  常见做法包括:

  • 对权限缓存设置合理的过期时间
  • 通过消息通知各个业务服务清理本地缓存
  • 对高风险操作,在执行前重新校验关键授权信息
  • 用户角色或权限变更后,主动删除对应用户的权限缓存

  需要根据业务对实时性的要求选择方案。对于普通菜单权限,短时间缓存通常可以接受;对于资金、审批、敏感数据等高风险操作,则应更加重视授权变更的及时性。

默认拒绝,而不是默认放行

  权限系统应当遵循最小权限原则:用户只获得完成工作所必需的权限。

  如果权限配置不存在、策略计算失败或授权服务不可用,不能简单地跳过权限校验继续执行业务。对于敏感操作,应该采用失败关闭(Fail Closed)的处理方式,即无法确认允许时就拒绝访问。

不要只在前端控制权限

  前端可以根据权限隐藏菜单、按钮和字段,以改善用户体验,但前端控制不能作为安全边界。

  用户可以绕过页面,直接调用后端接口;也可以修改请求参数,尝试访问其他用户的数据。因此,功能权限、数据权限和字段权限都必须在服务端进行必要校验。

写操作同样需要数据权限

  数据权限不能只应用于列表查询。

  例如,用户只能查看本部门订单,但如果系统允许他通过 PUT /orders/1002 修改其他部门的订单,那么数据权限仍然存在漏洞。

  对于详情、修改、删除、审核等操作,服务端都应该验证目标记录是否在当前用户的授权范围内。尤其是更新和删除,建议将授权范围条件落实到 SQL 或在事务中完成可靠的资源校验,避免仅凭客户端传入的 ID 操作数据。

字段权限需要覆盖所有数据出口

  字段权限也不能只考虑页面详情接口。列表、导出、打印、报表、消息通知和文件下载等数据出口,都可能包含敏感字段。

  因此,设计字段权限时,需要明确不同操作对应的字段访问规则,并确保所有相关出口都经过统一处理。

授权决策需要可追踪

  当用户反馈“为什么我看不到这条数据”或“为什么我不能执行这个操作”时,如果系统没有记录授权决策过程,排查会非常困难。

  对于重要业务,可以记录:

  • 当前用户和租户
  • 请求的资源和操作
  • 匹配的角色或策略
  • 数据权限范围
  • 授权决策结果
  • 拒绝原因
  • 操作时间和请求标识

  需要注意,日志中不应直接记录密码、Token 或不必要的敏感业务字段。审计日志本身也需要访问控制和保留策略。

最佳实践

  经过前面的介绍,我们已经了解了 RBAC、ABAC、数据权限和字段权限的基本思想。那么,在实际项目中应该如何选择和组合这些方案呢?

普通后台管理系统优先采用 RBAC

  如果系统主要是菜单、按钮、接口等功能权限,用户岗位相对稳定,那么可以优先采用 RBAC。

  例如,使用用户、角色、权限以及两张关联表完成基础授权。对于常见后台系统而言,这种设计已经足够成熟,也便于管理员理解和维护。

业务数据范围复杂时引入数据权限

  如果不同岗位可以访问的数据范围不同,可以在角色上配置数据范围,例如本人、本部门、本部门及下级部门、全部数据和自定义范围。

  在 Java 项目中,可以通过 MyBatis 拦截器或统一查询组件减少重复代码,但需要确保查询、详情、修改、删除和导出等入口都受到保护。

授权规则依赖动态属性时考虑 ABAC

  如果授权条件不仅取决于角色,还取决于订单状态、金额、部门归属、时间或网络环境等属性,可以考虑引入 ABAC。

  例如,用户需要同时满足“拥有订单审核权限”“属于订单所属部门”“订单状态为待审核”等条件,才能完成审核。

  如果这些规则数量不多,也可以先在业务服务中封装统一的授权判断;当规则数量增加、需要跨服务复用或由管理端动态配置时,再考虑引入独立策略引擎。

敏感信息较多时引入字段权限

  如果系统涉及身份证号、手机号、客户资料、资金成本、利润等敏感信息,就需要进一步考虑字段权限。

  可以先通过 DTO/VO 转换层统一控制字段可见性和脱敏;对于高敏感字段,还应在查询、修改和导出等环节增加更严格的服务端校验。

  总的来说,权限体系应当从业务需求出发,先建立清晰、可靠的基础模型,再根据复杂度逐步扩展,而不是一开始就引入过多抽象。

结语

  Web 权限控制的核心目标,是在识别用户身份之后,准确判断用户能够访问哪些资源、执行哪些操作,以及能够接触到哪些数据。

  从授权模型来看,RBAC 通过用户、角色和权限之间的关系完成授权,结构清晰,适合岗位职责相对稳定的业务系统;ABAC 则通过主体、资源、操作和环境等属性动态计算授权结果,适合业务规则复杂、授权条件多变的场景。

  从权限粒度来看,功能权限控制用户能否执行某项操作,数据权限控制用户能够访问哪些记录,字段权限控制用户能够查看或修改记录中的哪些字段。

  这些机制并不是互相替代的关系。一个完整的企业权限体系,往往需要将 RBAC、ABAC、数据权限和字段权限组合起来使用。

  实际开发中,没有一种权限模型能够适用于所有业务场景。我们需要根据系统规模、组织结构、业务规则、数据敏感程度以及权限变更的实时性要求,选择合适的设计方案。

  权限体系也不应该只关注“如何让有权限的人访问”,还需要认真考虑“如何确保没有权限的人无法访问”。默认拒绝、最小权限、服务端校验、写操作保护和审计追踪,都是权限系统不可忽视的基础要求。

  归根结底,好的权限设计不是把模型做得越复杂越好,而是能够以清晰、可维护的方式,将业务中的授权规则准确地落实到每一次请求、每一条数据和每一个敏感字段上。

文章信息

时间 说明
2026-07-28 初稿
0%