用户可以打开订单页面,却不一定能查看另一个部门的订单;拥有退款审批角色,也不应批准自己提交的退款。权限系统真正要回答的,是这个主体此刻能否对这个对象执行这项操作,以及业务执行时是否仍满足这些条件。
本文以多租户订单审批为贯穿场景。先选择合适的权限模型,再把决策落实到接口、数据和状态变更中,最后处理权限撤销、缓存及跨服务调用。设计重点不是建多少张权限表,而是让授权边界可表达、可执行、可验证。
认证提供身份,授权决定行为
访问控制的基本输入包括主体、资源、操作及必要的环境。主体可以是用户或服务,资源可以是订单、字段或导出任务,操作可以是读取、审批或删除。
AAA 中的 Authentication 负责认证,Authorization 负责授权,Accounting 记录活动和资源使用,可用于审计。它们是相关职责,不必理解成每次请求严格串行执行的三个独立服务。
认证并不保证后续所有行为都允许。OAuth 2.0 是授权框架,OIDC 在其上提供认证;Access Token 不一定是 JWT,也不能把发给客户端的 ID Token 随意当作业务 API 的访问凭据。JWT 只是载体,其签名、发行者、受众、有效期及用途都必须按接口约定验证。
登录响应包含用于渲染菜单的权限信息,本身不意味着架构混乱。问题在于后端是否把这份客户端状态当作授权依据。隐藏按钮可以改善体验,真正的授权仍在可信的服务端执行。
模型选择取决于谁制定策略、策略依据什么
DAC、MAC、RBAC、ABAC 不构成一条后者必然替代前者的能力排行榜,也可以在一个系统中组合。
| 模型 | 关注点 | 适合表达的关系 |
|---|---|---|
| DAC | 资源所有者在系统规定范围内管理访问 | 文件或文档共享 |
| MAC | 系统强制实施用户不能自行修改的策略 | 标签、域与信息流限制 |
| RBAC | 通过角色分配操作权限 | 审核员、出纳、运维等职责 |
| ABAC | 根据主体、资源、操作和环境属性求值 | 同租户、所属部门、状态、时间与设备条件 |
读取文件后可以复制内容,是信息流控制问题;不能由此推导 DAC 中所有接收者都天然有再次授权的权限。MAC 也不只表示一个高低等级:Bell–LaPadula 是关注保密性的特定模型,SELinux 等强制控制还可以使用类型和域。
RBAC 能显著简化职责分配,ABAC 则能避免把每个部门、区域、时间组合都固化成一个角色。但角色关系本身也可以参与属性判断,不需要把“功能只能用 RBAC,数据只能用 ABAC”当作硬规则。
RBAC 的继承、会话与约束各有含义
常见 RBAC96 框架中,RBAC0 表示核心关系,RBAC1 加入角色层次,RBAC2 加入约束,RBAC3 组合两者。RBAC1 与 RBAC2 都基于 RBAC0,并非 0→1→2→3 的单向升级链。NIST 推动的标准模型整合了相关研究,不能把这些编号直接说成 2004 年标准的四个递进模块。NIST RBAC FAQ
核心关系包括用户、角色、权限、用户角色分配和角色权限分配。会话激活用户已获分配角色的一个允许子集;会话角色不能由客户端随意指定,更不能通过激活动作越过有效期、租户或互斥限制。
角色继承不等于行政级别继承
先给边一个明确含义,例如“退款主管继承退款审核员的权限”。同一含义贯穿数据库、界面和计算,不要混用父角色、子角色、高级和下级的方向。
加入继承边时应检查:
- 两个角色属于允许关联的作用域且都存在;
- 不允许自环,新边不会形成循环;
- 继承闭包不会违反已定义的职责分离约束;
- 并发变更也不能分别通过检查后共同构成环。
遍历时设置 visited 可以避免死循环,但只是运行保护,不能把非法环悄悄变成合法配置。单个 parent_id 只能表达单父结构;需要多继承时,应使用单独的角色继承关系。
职责分离不能只检查角色名称
静态职责分离在分配阶段约束角色组合,动态职责分离约束一次会话中的激活组合。两者都不自动解决“同一人先提交,再换一个会话批准同一退款”的问题。
后一个场景需要对象级业务条件:审批人与申请人不同,且审批对应当前退款版本。约束检查还要考虑继承得到的权限。角色人数上限等限制需要与实际分配原子执行,先 count 再 insert 不能保证并发下不超额。
一次审批怎样组合授权条件
对某个退款申请,允许审批可能同时要求:
- 用户已认证且账户有效;
- 用户和退款属于同一租户;
- 当前激活职责包含退款审批能力;
- 退款处于可审批状态,并位于用户允许管理的范围;
- 审批人不是申请人,审批金额和版本符合本次请求;
- 所需的二次验证或复核已经完成且尚未失效。
这些条件来自不同证据。租户成员关系来自服务端身份目录,订单归属来自业务数据,客户端提交的 tenant_id、owner_id 或“已审批”字段都不能自行变成可信事实。
XACML 将策略管理、决策、执行和属性提供分别称作 PAP、PDP、PEP、PIP。决策结果除 Permit、Deny 外,还包括 NotApplicable 和 Indeterminate,并可能附带执行义务;PEP 需要处理这些结果,不能把无法求值当作允许。XACML 3.0 规范
可以用 Rego、Cedar、Casbin 或业务代码表达策略,选择时关注模型适配、类型校验、策略版本、调试和发布方式。时间条件应明确时区并使用可信时间;网络条件应来自可信代理链,不能直接信任用户提供的 IP Header。
功能权限、对象权限和数据返回一起收口
菜单树可以用于呈现产品结构,但它不是完整的权限模型。访问菜单不自动获得其全部操作,拥有审批操作也不自动获得任意订单的审批权。
后端应默认拒绝未获明确授权的受保护操作,并维护清晰的公开接口列表。接口路由、HTTP 方法及匹配优先级需要确定,不能遍历无序 Map、找到第一个模式就决定权限。OWASP 的授权建议也强调默认拒绝和每次请求校验。OWASP Authorization Cheat Sheet
使用注解或 AOP 可以统一部分检查,但要验证代理绕过、自调用、异步任务及其他入口。批量接口应检查全部目标或按约定逐项返回,不能只检查列表中的第一条。
查询约束先于分页和聚合
订单列表应在查询阶段限定租户和可见范围,再进行分页、计数、排序和汇总。查询全部记录后在应用层删掉无权限行,会泄露数量、聚合或排序信息,也可能产生错误分页。
授权范围应区分 ALL、NONE、SELF、明确集合等状态。空集合与 null 不应同时承担“无限制”和“无人可见”的含义;未知范围必须拒绝或报错。
下面是服务端获得可信上下文后的参数化查询片段,参数标记采用常见的命名形式,不是所有数据库驱动的原生语法:
SELECT id, status, amount_minor
FROM orders
WHERE tenant_id = :tenant_id
AND owner_id = :subject_id
ORDER BY id
LIMIT :page_size;
字段授权独立于行过滤。查询、导出、日志和缓存中都需避免把未获授权的字段带出去;页面上将手机号打星,不能补救 API 已返回完整号码。
SQL 改写可以减少重复代码,但要使用能理解语法的方案,并覆盖 JOIN、子查询、CTE、别名和写操作。不应对 SQL 字符串直接追加 WHERE,也不能遇到无法识别的语法就默认放行。
RLS 需要验证真实运行身份
PostgreSQL RLS 能在数据库侧限制行访问,但超管和 BYPASSRLS 角色会绕过,表所有者通常也绕过;FORCE ROW LEVEL SECURITY 可以约束表所有者,不能把它当作约束所有高权限角色的开关。读取和新行写入还分别涉及 USING 与 WITH CHECK。PostgreSQL 17 行安全文档
应用连接角色不应使用这些绕过能力。若策略读取应用写入的租户上下文,需要限制其设置来源,并处理连接池复用;不可信客户端能够任意 SET 一个自定义变量时,这个变量就不是可靠身份边界。
RLS、独立 Schema、独立数据库属于不同层面的隔离手段,不能排成一个无需条件的安全榜单。备份、运维身份、密钥、网络和跨租户管理路径同样决定实际隔离。
授权结果要与真正的状态变更对应
业务记录可能在“检查允许”之后改变。审批时应将仍然成立的对象条件放进事务或条件更新,避免审批旧版本。
以下是说明性的条件更新,执行前仍需验证身份、角色、租户成员关系与必要审批证据;它只展示对象和状态检查如何接近写入:
UPDATE refund_request
SET status = 'APPROVED',
approved_by = :subject_id,
version = version + 1
WHERE tenant_id = :tenant_id
AND id = :refund_id
AND status = 'PENDING'
AND requester_id <> :subject_id
AND amount_minor = :approved_amount_minor
AND version = :expected_version;
只有影响一行才进入成功路径。返回零行可能表示不可见、状态变化或条件失效,应按接口策略返回相应结果,避免为了区分原因泄露其他租户对象是否存在。
这段 SQL 本身不能阻止权限目录在检查后撤销角色。高风险业务还要规定授权有效性的时间边界,例如在同一提交协调中验证授权版本,或使用短期、绑定对象版本且有明确撤销语义的授权凭据。普通读取与资金操作可以采用不同强度,不能暗示所有系统都实现跨存储瞬时撤销。
审批通过也不等于实际退款已完成。资金动作另有幂等操作号、执行状态和结果核对,重试不能重复退款。
组织关系用于授权,不能直接取权限并集
组织、用户组和职位可以减少管理成本,但企业未必是一棵单纯的树,兼职、矩阵汇报和临时项目都可能存在。组织继承角色也需要单独定义,不能默认员工继承所有祖先部门绑定的权限。
对多个来源的授权取并集,只能得到候选权限。禁用状态、显式拒绝、租户范围、有效期、会话和职责分离等约束,仍需按明确组合规则生效。
数据模型可以从以下关系开始,按实际需求扩展:
| 关系 | 主要字段或约束 |
|---|---|
| 主体 | 身份引用、状态;凭据管理可以由身份系统承担 |
| 租户成员 | tenant_id、subject_id、状态与有效期 |
| 角色 | 作用域、稳定编码、状态 |
| 权限 | 资源类型与动作,或可验证的策略引用 |
| 主体角色 | 作用域、主体、角色、授权来源与有效期 |
| 角色权限 | 作用域、角色与权限 |
| 角色继承 | 继承方向明确的角色对及防环约束 |
| 授权变更 | 操作人、原因、版本及审计关联 |
唯一键和外键需要覆盖租户作用域,避免关联表把租户 A 的角色授给租户 B 的主体。角色删除、停用和迁移的语义也应定义,不能只依赖物理删行。
菜单与组织结构可以使用邻接表、物化路径或闭包表。插入节点、移动子树和查询的成本不同;不能把单节点插入的 O(depth) 当作所有树更新的复杂度,也不能从层级少直接推出唯一最佳存储方案。
缓存性能不能以撤销失效为代价
权限缓存应包含作用域、主体、激活角色、策略版本及实际参与判断的上下文。完整决策缓存还可能依赖对象版本、动作、时间和环境,不能只用 user_id 作键。
“没有权限”是有效结果,应与缓存未命中区分。权限集与 TTL 应一起发布;在旧集合上逐项 add 会留下已经撤销的条目。缓存构建过程中还要防止旧快照在失效通知之后重新写回。
一种做法是使用版本化快照:读取可信版本、构建完整结果,再核对版本仍有效后发布。角色、成员和组织变更需要覆盖实际受影响的主体,而不只是直接关联该角色的用户。
版本号也有前提:如果 JWT 和服务端缓存都是旧版本,两者相等并不能证明当前有效。必须定义最新版本从哪里获得、传播延迟多久,以及失联时怎样处理。短 Token 有效期只限制旧权限持续的最长窗口,黑名单和强制退出也只有被所有入口检查时才有效。
防缓存击穿使用租约锁时,每个持有者需要唯一标识,释放应原子比较持有者再删除;锁过期后,旧持有者不能删掉新锁。这个锁解决并发构建,不代替授权版本校验,也不证明缓存内容永远新鲜。
跨服务传播同时保留用户与调用方
网关可以校验凭据并执行粗粒度策略,业务服务负责对象、租户和状态相关的授权。内部接口也应验证调用方,不能因为来自内网就接受任意 X-User-Id。
若采用可信网关注入身份 Header,需要移除外部同名字段,验证来源并阻止绕过入口;更稳妥的服务边界可以使用适合目标受众的签名上下文或访问令牌。服务间调用还应保留代表哪个用户、由哪个服务发起、允许哪些动作,避免把某个高权限服务变成任意用户的代理。
mTLS 证明连接一方身份,并不自动授权某项业务操作。Kubernetes ServiceAccount 与 RBAC 主要控制 Kubernetes API;业务服务仍需自己的授权规则。
后台任务没有用户上下文时,应使用明确的服务授权,不能因用户信息缺失而自动退回权限更大的身份。异步任务保存必要的授权依据和对象版本,执行时根据业务约定重新核验。
用授权矩阵测试允许和拒绝
权限测试要覆盖实际业务入口,不能只对一个 matchesAny 函数做字符串测试。
| 测试变化 | 应确认的结果 |
|---|---|
| 同角色、不同租户 | 不能读取或修改另一租户对象 |
| 同租户、不同负责人 | 按对象范围决定,不能只看页面权限 |
| 申请人切换角色或会话 | 仍不能审批自己的退款 |
| 审批后金额或版本变化 | 旧审批依据不能授权新操作 |
| 权限撤销,Token 未过期 | 在承诺的撤销时限内停止旧授权 |
| 缓存重建与撤销同时发生 | 旧快照不能覆盖新状态 |
| 属性缺失、策略异常或超时 | 不得意外放行受保护操作 |
| 导出、批量、搜索和异步入口 | 与单对象 API 使用一致的边界 |
| RLS 用真实应用角色运行 | 绕过能力、连接池上下文和写入策略均受检验 |
审计应关联主体、调用服务、租户、对象、动作、策略版本、决策与实际执行结果。独立存储有助于隔离,但 Elasticsearch 本身不保证记录不可篡改;还需访问控制、留存与完整性措施,并避免记录凭据和不必要的敏感字段。
权限设计最终要形成一条可追踪的链:可信身份提供主体,策略确定允许条件,业务执行维持对象约束,撤销机制限制旧授权寿命,审计与测试验证实际结果。模型可以逐步扩展,这条链上的缺口却不能靠更多角色和权限表弥补。