权限系统的入门模型很简单:用户拥有角色,角色拥有权限。把三张表建立起来后,很快会遇到真实问题:同一个人在不同项目里角色不同,测试环境和生产环境权限不同,临时授权需要过期,审批人又不能审批自己的操作。
权限不是一个独立技术模块,而是业务规则的一部分。
图 1:角色只是授权来源之一。最终决策还要计算资源范围、临时授权、职责分离和策略版本。
从动作和资源开始
比起先列角色,我更愿意先列受保护的动作:谁可以在什么范围内对什么资源做什么。例如“发布生产环境”包含操作者、工程、环境和动作,缺一不可。
角色只是权限集合的管理方式,不应成为代码里到处出现的判断。业务服务询问授权系统“是否允许这个动作”,而不是判断用户是否叫某个角色。
我会把一次授权请求写成四元组:主体、动作、资源、上下文。
type AuthorizationRequest = {
subject: { userId: string; organizationId: string };
action: "release.read" | "release.create" | "release.approve";
resource: { projectId: string; environment: "test" | "production" };
context: { now: string; requestId: string };
};
type AuthorizationDecision = {
allowed: boolean;
policyVersion: string;
reason: string;
missingPermissions?: string[];
};“生产发布审批人不能是发起人”就不适合塞进角色表。它依赖当前发布任务的 createdBy,属于资源和上下文共同决定的领域策略。把它写成明确规则,才能测试,也能在拒绝时解释。
拒绝也需要解释
一个成熟权限系统不仅返回允许或拒绝,还应给出缺少的条件。用户知道需要哪种权限、去哪里申请,平台支持成本会显著下降。
授权变化要被审计:谁在何时授予了什么范围,为什么授予,何时过期。高风险操作还需要职责分离,避免同一人发起并批准。
审计日志保存的是当时的决策证据,而不只是操作结果:
requestId / subject / action / resource
decision / reason / policyVersion
matchedGrants / evaluatedAt如果半年后只知道“用户当时有管理员角色”,无法复原这个角色在当时包含哪些权限。保存策略版本和匹配授权,才能回答一次高风险操作为什么被允许。
缓存不能改变授权事实
权限查询频繁,团队自然会考虑缓存。缓存键必须包含用户、资源范围和动作,失效要响应组织与授权变更。管理员刚撤销生产权限,旧缓存不能继续允许操作。
对于高风险动作,可以接受额外延迟以换取实时检查;低风险只读能力则可使用短缓存。安全与性能不是统一答案,而要按后果分级。所有最终判断仍应记录当时使用的策略与权限版本,方便审计。
前端隐藏入口可以改善体验,真正的权限验证必须发生在服务端。安全边界不能依赖用户看不见按钮。
前端仍然应该消费同一份能力结果,用于禁用按钮和解释申请路径,但不能自己复制策略。否则服务端改规则后,页面可能显示可操作,提交却被拒绝;更糟糕的是页面隐藏了入口,API 却没有检查。
RBAC 是起点,不是答案。只有把权限放回具体领域,模型才会既安全又可维护。
权限规则必须拥有反例测试
每条允许规则至少配一个“相似但必须拒绝”的反例:项目管理员能发布本项目,不代表能发布别的项目;报表查看者能导出可见字段,不代表能导出隐藏个人信息。
allow: actor.role=project_admin, action=release.deploy, resource.projectId=actor.projectId
deny case: same role, different projectId
deny case: production environment without approval
audit: policyVersion + matchedRule反例让权限从角色名称回到真实资源和上下文。