ACL 数据结构设计
做 ACL 系统,第一步就是搭数据架子。架子搭不好,后面全是坑。我个人习惯先把最核心的三个要素定下来:谁、干什么、对谁干。说白了就是用户、权限、资源这三样。
用户-资源-权限三元组模型
这个模型其实不复杂。你想想看,每次访问控制本质上就是在回答三个问题:
- 谁在请求?(用户 / 角色)
- 对什么请求?(资源 / 对象)
- 做什么?(操作 / 权限)
把这三个答案拼在一起,就是一个三元组:(用户, 资源, 权限)。我在项目中遇到过有人把用户和角色混在一起存,结果后面权限继承逻辑乱成一锅粥。所以一开始就要分清楚:用户是实体,角色是集合,别搞混。
核心公式:访问决策 = 匹配 (Subject, Resource, Action) → 返回 Allow / Deny
三元组的好处是直观。但实际落地时你会发现,如果每个用户对每个资源都存一条记录,数据量会爆炸。所以一般会引入「用户组」或「角色」来压缩条目数。不过那是后面章节的事,咱们先把基础架子搭稳。
ACL 条目结构设计
一条 ACL 条目长什么样?我直接给一个我在生产环境用过的结构:
// ACL 条目核心字段
{
"id": "acl_001", // 条目唯一标识
"subject": "user_1001", // 主体(用户/角色/组)
"subject_type": "user", // 主体类型:user | role | group
"resource": "doc_2024", // 资源标识
"resource_type": "document", // 资源类型:document | api | menu
"permissions": 0b00000111, // 权限掩码(后面细讲)
"effect": "allow", // 效果:allow | deny
"priority": 100, // 优先级(数字越小越优先)
"conditions": {}, // 条件约束(时间/IP/设备等)
"create_time": 1700000000,
"expire_time": null // 过期时间,null 表示永久
}
嗯,这里要注意几个关键点:
- subject_type 一定要单独存。我曾经接手过一个老系统,主体类型靠字段前缀猜(比如 user_ 开头就是用户),结果维护了两年后彻底失控。
- effect 字段。别小看它,Deny 优先级高于 Allow 是 ACL 的铁律,后面讲决策引擎时会细说。
- priority 用来处理多条匹配时的冲突。我建议留 0-100 的区间,别用负数,容易出 bug。
我的习惯:每条 ACL 条目都带一个 expire_time。哪怕你当前不需要过期功能,也先占个字段。等业务方突然说「这个权限下周失效」时,你会感谢这个字段的。
权限掩码设计
权限掩码是 ACL 里最巧妙的设计之一。说白了就是用二进制位来表示权限集合。为什么用位?因为快,而且省空间。
我直接给一个 8 位掩码的经典分配方案:
| 位位置 | 位掩码 | 权限名 | 说明 |
|---|---|---|---|
| Bit 0 | 0b00000001 (1) | READ | 读取/查看 |
| Bit 1 | 0b00000010 (2) | WRITE | 写入/编辑 |
| Bit 2 | 0b00000100 (4) | EXECUTE | 执行/调用 |
| Bit 3 | 0b00001000 (8) | DELETE | 删除 |
| Bit 4 | 0b00010000 (16) | ADMIN | 管理/授权 |
| Bit 5-7 | 0b11100000 | 预留 | 业务自定义 |
为什么用 2 的幂?因为每个位独立,组合权限时直接按位或(|)就行。比如读写权限就是 READ | WRITE = 0b00000011 = 3。检查权限时用按位与(&),一行代码搞定。
// 权限检查示例(Go 语言)
const (
READ = 1 << 0 // 1
WRITE = 1 << 1 // 2
EXECUTE = 1 << 2 // 4
DELETE = 1 << 3 // 8
ADMIN = 1 << 4 // 16
)
// 检查是否有读权限
func CanRead(permMask int) bool {
return permMask & READ != 0
}
// 检查是否有读写权限
func CanReadWrite(permMask int) bool {
return permMask & (READ | WRITE) == (READ | WRITE)
}
我曾经踩过的坑:一开始用 32 位掩码,觉得够用。结果业务方要求每个资源类型有 20 多种自定义操作,32 位根本不够。后来改成 64 位,但数据库存 bigint 又有一堆兼容问题。所以建议一开始就设计成可扩展的,比如用字符串存多个掩码,或者直接上位图数组。
权限掩码还有一个好处:可以快速做集合运算。比如你想给某个角色「除了删除以外的所有权限」,直接 ALL_PERMS & ^DELETE 就行。这种位运算在决策引擎里跑起来飞快,毫秒级判断几万条规则不是问题。
下面我把整个 ACL 数据结构的关系画成一张图,方便你理解:
这张图把三个核心要素和 ACL 条目的关系串起来了。你注意看,权限掩码是独立于用户和资源存在的,它通过位运算和条目绑定。这样做的好处是:权限定义可以复用,不同资源类型可以共用一套掩码体系。
实际项目建议:权限掩码的定义最好放在配置文件或枚举里,别硬编码。我见过有人在代码里到处写 magic number(比如 if perm == 3),后来加个权限要改 50 个文件。用常量或枚举,维护成本低一个数量级。
最后总结一下这节的核心:三元组是骨架,条目结构是血肉,权限掩码是灵魂。这三样东西定好了,后面的决策引擎、缓存策略、扩展设计都会顺很多。别急着写代码,先把数据结构画清楚,想清楚边界情况——比如同一条 ACL 既匹配用户又匹配角色时怎么处理?这些细节我会在后面的章节展开。