权限管理

核心模型

ACL (Access Control List)

每个资源上直接记录哪些用户可以做什么操作

这是最原始最直观的权限模型,结构是这样

1
2
3
4
资源
 ├── userA : read
 ├── userB : read, write
 └── userC : admin

最典型就是 Linux 文件权限 -rwxr-xr-x

如果用数据库实现 ACL,通常是这样设计

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
users
-----
id
name

resources
---------
id
name

permissions
-----------
user_id
resource_id
action

例如

1
2
3
4
5
permissions

user1  file1  read
user1  file1  write
user2  file1  read

ACL 的优点是直观和灵活,谁能做什么一目了然,可以精确到用户,资源,动作。缺点是权限爆炸,10000 用户*10000 资源,最终会表数据会异常庞大。

缺点是无法表达角色。

ACL 适合少数资源共享的场景,例如某个仓库或某个文件的读写权限,不适合中后台系统。

RBAC (Role Based Access Control)

用户不直接拥有权限,而是通过角色获取权限

这是中后台管理系统的常用模型,结构变成三层,用户 > 角色 > 权限

如果用数据库实现 ACL,通常是这样设计:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
用户
-------
张三
李四

角色
-------
运营
审核员
管理员

权限
-------
video.review
video.delete
video.publish

关系是

1
2
3
4
5
6
张三 -> 运营
李四 -> 审核员

运营 -> video.review
审核员 -> video.review
管理员 -> video.delete

RBAC 的数据库设计几乎都是这 5 张表

1
2
3
4
5
6
7
# 资源
users
roles
permissions
# 关系
user_roles
role_permissions

RBAC 的优势

  1. 权限复用,不用给每个用户配权限,只需要给用户绑定角色,解决了 acl 的权限爆炸问题
  2. 权限管理集中,修改权限只需要修改角色

缺点

RBAC 只解决了接口权限,但是很多系统还有 数据权限,例如:

1
2
3
运营A 只能看自己视频
运营B 只能看自己视频
管理员 可以看所有视频

RBAC 无法表达,不能控制读取哪些数据 ,这是所有中后台最核心的设计问题。

接口权限 ≠ 数据权限,用户可以访问删除接口,但是不能删除别人的数据!

提问

下面两个接口设计哪个更好?

方案 A

1
2
GET /api/my_videos
GET /api/all_videos

方案 B

1
GET /api/videos

普通用户:

1
只返回自己视频

管理员:

1
返回所有视频

你觉得哪个更好?

回答

大部分系统采用 B 方案,原因是权限属于业务逻辑不属于 API 设计,RESTful API 接口设计应该表达资源,而不是 角色,所以权限控制应该是 service 层。否则会接口爆炸,每种权限都要设计对应的接口。

那么什么时候用 A 呢? 业务语义不同,例如

1
2
GET /users/:id/orders
GET /admin/orders

/users/:id/orders 就是一种资源,但是 all_videos 不符合 RESTful 设计。不要因为权限而增加接口,根据业务语义设计接口。

举个栗子

一个后台系统,根据"运营",“管理” 需要展示不同的数据内容,这属于数据权限,一个接口足以。

一个用户系统一个后台系统,用户系统展示的是用户的子资源,后台系统展示的顶级资源,可以设计为两个接口,根据业务设计接口,不要根据数据权限设计接口。

还可能需要查询出相同的数据结构,但是同一个用户在不同的页面需要查询出不同的结果,在个人页面仅查询出自己的。

RBAC + 数据权限

很多系统不仅要解决能不能访问接口,还需要能看到哪些数据。

场景的模型权限

1. 仅自己

我的订单,我的视频,我的文章,sql 条件是 where user_id = ?

2. 部门权限

企业系统常见

1
2
3
销售A
销售B
销售经理

权限

1
2
销售A -> 自己
销售经理 -> 整个部门

SQL

1
WHERE dept_id IN (...)

3. 组织树权限

大型公司

1
2
3
4
5
集团
 ├ 华东
 │   ├ 上海
 │   └ 杭州
 └ 华南

权限

1
华东负责人

能看

1
上海 + 杭州

SQL

1
org_path LIKE '1/2/%'

4. 自定义数据权限

例如

1
2
3
运营A
能看
tag=游戏 的视频

SQL

1
WHERE tag IN (...)

这种系统一般叫 ABAC。

互联网后台最常见的设计就是 RBAC+数据范围,例如角色表增加 data_scope 字段,参数例如

1
2
3
4
ALL
DEPT
SELF
CUSTOM

权限关系是

1
2
3
管理员 -> ALL
用户 -> SELF
经理 -> DEPT

伪代码逻辑

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
if role.data_scope == ALL {
    // 不加条件
}

if role.data_scope == SELF {
    where user_id = current_user
}

if role.data_scope == DEPT {
    where dept_id in (...)
}

这就是工业界最常见的方案 RBAC + data_scope

最佳实践

  • api 层不处理数据权限
  • service 层决定数据范围(决定规则)
  • db 层执行 sql(执行规则)

通过 SQL 查询就过滤 WHERE user_id = ? 权限更安全,且 SQL 性能会更好。

菜单权限 VS 接口权限

很多中后台系统都会有菜单的控制,根据用户角色展示哪些菜单,例如"用户管理",“订单管理”,“系统设置” 等。

菜单权限要不要等于接口权限? 答案是否定的,这一个非常大的坑!

错误设计

数据库

1
2
3
4
5
6
permissions
-----------
video:list
video:delete
video:create
video:update

然后

1
菜单 = permission

前端

1
if(hasPermission("video:list"))

问题来了!! 一个页面可能调用

1
2
3
4
GET /videos
GET /users
GET /tags
POST /videos

假设系统有 200 个API,那权限就有 200 个! 角色配置就变成某个角色绑定 200 个相关接口,这些接口对于人类很难读懂,假如让项目经理或 ui 设计师来看,GET /videos,他们不知道这个接口是查看视频还是审核视频。

最佳实践

菜单权限 ≠ 接口权限,但菜单需要依赖接口权限。

UI 权限只是体验控制,不是安全控制。

权限分为三层

  • 菜单权限
  • 按钮权限
  • 接口权限

结构

  • 菜单权限 > 控制页面访问,例如 video_page
  • 按钮权限 > 控制按钮是否显示,例如 video_delete_button
  • 接口权限 > 后端 API 控制,例如 DELETE /videos/:id

为什么必须是三层?

前端权限只是 UI,可以被绕过,最终权限必须在后端,成熟的权限架构是

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
前端
菜单权限(控制页面)

按钮权限(控制UI)

后端
RBAC接口权限
数据权限

结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
用户
角色
菜单权限
按钮权限
接口权限
数据权限

数据库设计

  1. 用户表

  2. 角色表

  3. 用户角色关联表

  4. 权限表 permissions(id, code, name)

  5. 角色权限关联表

  6. 菜单表 menus(id,name, type, permission_code)

    1
    2
    3
    
    1 视频管理 page video_read
    2 删除按钮 button video_delete
    3 发布按钮 button video_publish
    
  7. api 权限映射表 api_permissions(id, method, path, permission_code)

    1
    2
    3
    4
    
    GET     /videos             video_read
    POST    /videos             video_create
    DELETE  /videos/:id         video_delete
    POST    /videos/publish     video_publish
    

流程

当用户请求 DELETE /videos/10

  1. 根据 api DELETE /videos/10查询权限,得到 video_delete
  2. 找到用户角色 user
  3. 判断用户角色 user 权限是否有 video_delete

权限有三种来源

  1. 角色直接绑定权限
  2. 角色拥有菜单 > 菜单绑定权限
  3. 特殊用户权限

通常取并集来判断一个用户是否有相关权限。

ABAC(Attribute Based Access Control)

RBAC 在中小系统非常好用,但是当系统规模变大时,RBAC 就会开始出现三个严重问题!!

  1. 权限爆炸,当系统发展一年后,500 个 API,50 个页面,200 个按钮,权限数量 800+,角色 100+,权限关系 5000+,这个时候就没人敢改动权限,改错一个就可能影响多个角色。
  2. 角色爆炸,现实业务通常是部门不同,地区不同,业务线不同,因为产生非常多的角色
  3. RBAC 只能表达有没有权限,不能表达在什么条件下有权限,比如运营可以删除视频,但条件是 只能删除自己创建的视频

RBAC 解决的是谁能做什么,现实问题是,谁在什么条件下,对什么资源,做什么操作。

这时候就需要 ABAC,核心是 通过属性决定权限

一般会有一个 policy 引擎,例如规则

1
2
3
4
allow if
user.role == "operator"
AND
resource.owner_id == user.id

访问时

1
DELETE /videos/123

系统会读取

1
2
3
user attributes
resource attributes
policy

最后计算出

1
allow / deny

Go 比较流行的权限引擎有 Casbin,其规则是

1
2
p, role_admin, video, delete
g, alice, role_admin

多租户权限

多租户是很多 SaaS 系统的核心,模型是

1
2
3
4
5
6
7
Tenant (租户)
Users
Roles
Permissions

数据结构是

1
2
3
4
tenants
-------
id
name
1
2
3
4
users
-----
id
tenant_id
1
2
3
4
users
-----
id
tenant_id

所有的业务操作查询必须带上,否则就会出现 数据串租户

1
2
SELECT * FROM videos
WHERE tenant_id = ?

真正的高级问题

很多系统会遇到一个超级难的问题

1
数据权限怎么写 SQL

例如

1
2
3
运营可以看
自己部门
+ 下级部门

SQL 会变成

1
department_tree 查询

这会让查询变得很复杂。

本文阅读量 次, 总访问量 ,总访客数
Built with Hugo .   Theme Stack designed by Jimmy