第 11 章:用户模块:认证、授权与审计¶

能登录不等于能上线。0 到 1 阶段最容易出事故的地方,往往是权限边界不清、审计缺失、账号体系一改就崩。本章不讲怎么写登录页面,而是把用户模块当作生产系统的底座来写:边界清晰、可追溯、可回滚。[22][68]
AI 产品会把用户模块的风险放大:一旦越权,泄露的不只是页面数据,还可能是知识库、提示词、工具调用结果与审计日志本身。你必须把“默认安全”做成系统属性,而不是靠开发者记得在代码里写个 if 校验。[68]
章节定位¶
本章位于智能层之后,进入产品模块。原因很简单:只要你开始提供保存、共享、团队协作、计费或知识库,你就必须先把身份与权限边界写清楚。否则你后面的每一章都会在同一个地方反复返工:谁能看到什么、谁能做什么、出了事怎么追责。[22]
你将收获什么¶
- 一个可迁移的边界模型:AuthN(认证)与 AuthZ(授权)分层、硬隔离的租户 ID、资源级权限判定。
- 一套审计口径:关键事件、字段、追踪标识(Trace ID),让事故可复盘、可追责。[68]
- 一张最小门禁:越权阻断、敏感操作可追溯、凭证策略可回滚。
三层思考:用户模块的价值不是登录,是边界¶
第 1 层:读者目标¶
你要交付的是一个可托付的系统底座:权限清晰、越权可回归、关键操作可追溯。
第 2 层:论证链条¶
用户模块的链条是:
身份体系(AuthN) -> 权限模型(AuthZ) -> 租户隔离 -> 审计事件 -> 回归用例 -> 变更与回滚
缺一环,安全会变成人的记忆,而人的记忆在压力下最不可靠。[68]
第 3 层:落地与验收¶
验收不靠“看起来没问题”,而靠三条硬门槛: 1. 任何跨租户访问都必须失败:租户隔离必须在 DB/Cache 层实现逻辑注入,越权必阻断。[68] 2. 敏感操作必须产生审计证据:必须涵盖:谁、何时、对什么、做了什么、关联 Trace ID。[68] 3. 权限策略必须版本化:策略变更必须有止损线,支持一键灰度与回滚。
方法论速览:先定模型,再定事件,再写回归¶

关键流程图(纯文本):一次请求如何被允许或拒绝¶
请求(含凭证与 tenant_id)
|
v
AuthN(你是谁)
- 校验凭证有效性(过期 / 撤销 / 轮换)
- 失败:拒绝 + 记录审计(login_fail / token_invalid)
|
v
AuthZ(你能做什么)
- 取主体(user / service) -> 取资源(resource_id) -> 取动作(read / write / admin)
- 关键检查:tenant_id 必须一致;资源级谓词必须成立
- 失败:拒绝 + 记录审计(access_denied + reason + policy_version)
|
v
执行(只在允许后发生)
- 返回结果或业务错误(与权限错误区分)
|
v
审计(可搜索、可串联、可解释)
- 字段:who / when / where / what / action / result / reason / trace_id / policy_version
|
v
回归与变更治理
- 越权用例入回归:跨租户、降权后访问、敏感操作审计缺失
- 策略版本化与灰度:新旧策略并行窗口 -> 指标不退化才切换 -> 退化即回滚
示例(可复制):3 条阻断级权限回归¶
目标: 用最小回归集把“越权必失败”固化成可复跑门禁,并确保拒绝原因可解释(便于审计与对外沟通)。
前置条件: * Python 3 可用
步骤: 1. 复制并运行下面脚本:它模拟一个最小 RBAC + 租户隔离,并验证 3 条回归用例。
# gate_user_auth.py - 用户模块准入哨兵
import sys
from pathlib import Path
def validate_user_auth_design(file_path):
required_checks = {
"tenant_id": "租户隔离 ID 必须贯穿全链路。防止跨租户泄露。",
"policy_version": "权限策略必须版本化。确保变更可回滚。",
"trace_id": "审计日志必须关联 Trace ID。确保事故可定责。",
"默认拒绝": "授权策略必须默认拒绝 (Default Deny)。防止权限空洞。"
}
content = Path(file_path).read_text(encoding='utf-8')
missing = [v for k, v in required_checks.items() if k not in content]
if missing:
print("❌ FAILED: 用户与授权模块设计不合格。缺失以下关键要素:")
for m in missing:
print(f" - {m}")
sys.exit(1)
print(f"✅ PASS: {file_path} 用户权限模块校验通过。准许动工。")
if __name__ == "__main__":
validate_user_auth_design(sys.argv[1])
```
- 将这 3 条用例迁移到你的服务端授权层回归(真实 API / 真实租户 / 真实资源),并把拒绝原因落到审计字段(
reason+policy_version+trace_id)。 - 权限策略变更必须先灰度:新旧策略并行一个窗口;回归全绿且线上拒绝率不异常,才允许切换。
验证命令:
* 执行上述脚本,输出 ok 且退出码为 0。
* 在你的 CI/CD 管道中,对应回归任务应稳定通过。
失败判定:
* 任一越权用例被放行(出现 UNEXPECTED ALLOW)。
* 拒绝原因或策略版本口径不一致(出现 WRONG <...>)。
回滚动作:
* 立即切回上一策略版本(policy_version 回退)。
* 在回归集中加入触发问题的最小用例,复跑通过才允许继续变更。
1) 身份体系(AuthN):你是谁¶
AuthN 的核心是凭证生命周期:签发、过期、撤销、轮换。0 到 1 阶段不追求花哨,但必须可解释、可撤销。[22]
如果你要接第三方登录或企业 SSO,建议优先选成熟的 OIDC 提供商,别在 0 到 1 阶段自己发明一套 token 协议。真正折磨人的不是登录页,而是撤销、轮换、过期后的边界:用户登出后旧会话是否还在?管理员禁用账号后旧 token 是否还能访问?把这些问题写进验收与回归,比多做两种登录方式更值钱。[22][69][68]
你要回答三件事: 1. 支持哪些登录方式:邮箱、第三方、企业 SSO? 2. 凭证如何失效:登出、改密、风控封禁。 3. 会话如何被追踪:用于审计与风控。
一个更可落地的最小推荐是:短生命周期 Access Token + 可轮换 Refresh Token,并把撤销做成服务端事实而不是客户端愿望: * Access Token:过期时间短一些(如 15 分钟),降低泄露窗口。 * Refresh Token:绑定设备与会话。 * 轮换:每次刷新都签发新 Refresh Token,旧 Refresh Token 立即失效,降低被盗复用风险。 * 撤销与封禁:服务端维护会话表或撤销列表,禁用用户时让所有会话立即失效(而不是等过期)。[69][68]
2) 权限模型(AuthZ):你能做什么¶
推荐从最小可用三件套起步: * 主体:用户、服务账号、团队。 * 资源:项目、文档、知识库、账单等资源实例。 * 动作:read、write、admin 等动作集合。[68]
最重要的不是你选 RBAC 还是 ABAC,而是:权限必须可测试、可审计、可迁移。
模板:权限模型一页纸¶
| 项目 | 写法要求 | 示例 |
|---|---|---|
| 主体 | 用户 / 团队 / 服务账号的定义 | user:123, service:indexer |
| 资源 | 需要保护的资源清单与标识方式 | repo:456, kb:789 |
| 动作 | read / write / admin 等最小集合 | kb:read, kb:delete |
| 角色 | 角色与动作映射(可选) | Owner = All; Viewer = ReadOnly |
| 租户隔离 | tenant_id 如何贯穿与校验 | HTTP Header X-Tenant-ID + DB 字段 |
| 默认策略 | 默认拒绝还是默认允许 | 必须默认拒绝 |
界面设计:两栏权限视图¶
用“两栏 + 危险区”结构把权限变成可解释、可审计的产品能力。别让用户猜为什么不行。
- 左栏(成员列表):邮箱 | 角色 | 状态(已邀请 / 已加入 / 已停用)。支持搜索。
- 右栏(权限详情):选中成员后,展示资源范围 + 动作集合,并明确标示“任何变更都会产生审计事件”。
- 页底(危险操作区):移除成员 / 降权 / 停用。必须二次确认 + 输入理由,理由会写入审计日志,避免事后扯皮。
3) 租户隔离:0 到 1 最常见的致命漏洞¶
多租户产品里,最贵的事故是跨租户数据泄露。
最低要求: 1. 查询必带租户:任何资源查询 SQL 或 API 调用都必须带租户上下文。 2. 缓存必带租户:任何缓存键(Key)都必须包含租户 ID,避免 A 租户看到 B 租户的缓存数据。 3. 导出必授权:任何导出 / 分享动作都要有显式授权与审计。[68]
跨租户越权属于 S0 级事故,建议把 10 分钟止损动作写成 Runbook 并演练:见 E-runbooks.md(RB-04)。
4) 审计:把背锅变成可追责¶
审计日志是事故复盘的事实源,也是很多企业客户付费的理由之一。[68]
模板:审计事件规范¶
| 字段 | 说明 | 示例 |
|---|---|---|
| who | 操作者 ID | user_123, service_cron |
| when | 时间戳(UTC ISO8601) | 2025-10-24T10:00:00Z |
| where | 追踪 ID / IP | trace_abc123, 1.2.3.4 |
| what | 资源类型 + ID | kb:456 |
| action | 动作 | kb:delete |
| result | 结果 + 原因 | fail:permission_denied |
| context | 关键上下文 | tenant_id:t_001, policy:v2 |
| payload | 变更摘要(脱敏) | {"name_old": "A", "name_new": "B"} |
建议先列出必须审计的事件,再实现功能:登录、权限变更、导出 / 删除、计费操作、知识库变更。[68]
5) 邀请与协作:让加入团队像完成一次闭环¶
如果你的产品有团队 / 工作区概念,邀请流程往往决定了协作能否启动。
一个可控、可解释的邀请闭环建议包含: 发起邀请(管理员选择角色 / 资源范围) -> 发送邀请 -> 成员接受 -> 首次登录 / 绑定 -> 权限生效 -> 产生审计事件。[68]
状态机设计(Invite Flow):
* invited -> accepted -> joined -> active
* invited -> expired
* invited -> revoked
异常处理:
* 重复邀请:按 (tenant_id, email) 做幂等,返回现有邀请并提示可撤销 / 重发。
* 成员已存在:切换为“调整权限”流程(产生审计事件)。
* 过期:重发邀请(生成新 Token,旧 Token 立即失效)。
* 跨租户:服务端必须阻断并记录审计。
6) 回归:把越权当成阻断级失败¶
权限系统最怕改一处、崩一片。你需要把越权用例写成固定回归集,只要 CI 挂了就禁止发布: * 跨租户读取必须失败。 * 角色降权后,访问必须失败。 * 权限变更必须产生审计事件。[68]
复现检查清单(本章最低门槛)¶
- [ ] 模型就位:主体 / 资源 / 动作 / 租户隔离定义清晰,能映射到 API。
- [ ] 阻断回归:越权回归用例(跨租户、资源级)已入 CI,失败即阻断发布。[68]
- [ ] 审计规范:关键操作可追溯,字段齐全(含
trace_id与策略版本),不泄露敏感信息。[68] - [ ] 回滚机制:权限策略版本化,支持一键切回上一版本。
常见陷阱(失败样本)¶
-
现象:UI 隐藏了按钮,但接口可调用
- 根因:把前端显示逻辑当成授权;服务端缺少资源级校验。[68]
- 复现:用低权限账号 / 跨租户 Token 直接调用接口(Curl / Postman)。
- 修复:授权必须在服务端做。
tenant_id强校验 + 资源级谓词。
-
现象:权限改一次,线上崩一片
- 根因:权限模型缺少迁移策略;策略变更没有灰度。[68]
- 修复:新旧策略并行窗口 -> 指标不退化 -> 切换。保留一键回滚。
-
现象:出了事无法追责,只能猜
- 根因:审计字段缺失,无法串起链路;缺少
trace_id。[68] - 修复:统一审计规范,缺审计视为功能未完成。
- 根因:审计字段缺失,无法串起链路;缺少
下一章¶
用户模块决定谁能做什么。下一章把商业闭环落到系统:订阅、计费、账本、对账与风控。见:12-billing.md。
参考¶
详见本书统一参考文献列表:references.md。