做权限平台时,一个“能编辑”就足够让几个人讨论半天。有人指按钮能点,有人指某个空间里的页面能改,还有人把发布也包括在里面。角色表容易建,麻烦在于大家用的是同一个词,说的却不是同一件事。
最近接入应用,我把授权判断收到了几个明确对象上:谁发起操作,动作是什么,资源归谁,在什么范围内生效。角色只负责把一组职责组织起来,不能把后面这些条件全部塞进角色名。
编辑者的权限为什么不能写成一个角色名
假设某人能编辑工作空间 A 的页面,但不能发布。把角色起名为“A 空间编辑者”,再给 B 空间建一个角色,关系虽然存得下,管理却会越来越难。同一组动作被复制很多遍,增加一个动作时,还得逐个找出应该更新哪些角色。
我的设计把角色和范围分开。编辑者拥有读取、修改两个动作,角色绑定记录主体、角色、租户和作用域。资源自身保存所属租户与空间。判断时用资源的真实归属匹配绑定,绑定在 A,不能因为知道 B 的资源编号就访问 B。
页面是否已发布属于另一项规则。角色授予编辑职责,页面状态决定这次能不能直接修改。把生命周期规则留在业务侧,避免每出现一个状态就多建一种角色。
授权入口只接收可信的身份和资源
接口完成认证后,得到当前主体和租户,再读取待操作资源。请求体里的 userId、tenantId 或 scope 只能作为待核对的信息,不能覆盖已经验证过的身份。用户组展开也集中处理,业务接口不用各写一套组成员逻辑。
一个只支持允许规则的简化判断函数如下,它不包含继承、显式拒绝和跨空间关系。
def can(subject, action, resource, assignments, role_actions):
for binding in assignments:
if binding["subject"] != subject:
continue
if binding["scope"] != resource["scope"]:
continue
allowed = role_actions.get(binding["role"], set())
if action in allowed:
return True
return False这里最重要的条件是 resource 来自服务端读取。客户端传一个同名空间,不能替代资源归属。资源不存在、授权依据读取失败,都不能默认放行。以后加入拒绝规则,需要重新定义合并顺序,不能在原函数里随便多加一个分支。
一个拒绝结果也要能查明原因
业务接口可以拿到 allow、reasonCode、policyVersion 和 decisionId。普通用户看到与资源隐藏策略一致的拒绝提示,有诊断权限的管理员则通过 decisionId 查实际命中的绑定和未满足的条件。
“没有编辑动作”和“范围不包含这张页面”要能分辨。否则管理员只好不断增加角色,直到按钮亮起来,最终授出了多大权限却没人能说明。
前端隐藏按钮是在表达可用操作,服务端检查决定能否执行。直接调用接口、打开收藏的链接、替换资源编号,都必须经过同一判断。菜单是否正确只是其中一个检查点。
撤权和缓存放在同一套约定里
权限缓存至少区分租户、主体、动作、资源范围和策略版本。组成员、角色动作或资源归属发生变化,都可能使之前的判断过期。只缓存一个“用户有编辑者角色”,会把范围和变化依据丢掉。
要求立即撤权的写操作,需要在执行时核对当前策略,或采用能及时失效的版本机制。允许短暂延迟的读请求可以使用缓存,但最大陈旧时间得明确。缓存命中率不能代替撤权语义。
我会用同用户同范围、跨范围、换用户、动作缺失、组授权撤回这几组情况检查。检查结果既包括允许或拒绝,也包括原因与策略版本。这样一份授权既能执行,也能在后来被解释清楚。