切换深色模式
第 1 章:登录接口能跑就够了吗?身份系统先过分布式系统这一关
本章主线:通过一次“登录偶尔超时、离职权限没有及时失效”的故障,理解身份平台为何首先需要扎实的软件工程能力。
开场问题
小安写完登录接口,本地测试全部通过。上线后,Redis 抖动导致部分用户被退出,消息重复使下游反复禁用账号,服务器时间快了五分钟又让 token 集体失效。
问题:这些是“普通稳定性问题”,还是身份安全问题?答案是两者都是。身份系统的错误会直接改变谁能访问什么,可用性与安全性必须共同设计。
学习目标与前置知识
学完后应能:追踪一次请求;设计幂等状态变更;定义身份 SLO;区分调试日志和审计;对依赖失败做风险分级降级。前置需要 HTTP、SQL、Linux 和基本容器知识。
1.1 为什么身份系统首先是分布式系统
登录和授权位于几乎所有业务请求的关键路径。一次 IdP 故障可能让全公司无法办公;一次缓存不一致可能让已离职员工继续访问;一次时钟漂移可能让全部 token 看似过期。
你必须同时保证:正确性、安全性、可用性、低延迟、可审计和可恢复。安全控制若导致业务不可用,同样是失败。
1.2 HTTP 请求从浏览器到身份服务
图 1-1:1.2 HTTP 请求从浏览器到身份服务 · 可编辑 Mermaid 源码
逐层要会排查:DNS 是否解析错误;证书/SNI 是否匹配;代理是否错误信任 X-Forwarded-*;Cookie 的 Domain/Path/SameSite/Secure;回调 URI;数据库和缓存;时钟;密钥版本。
1.3 Linux 与进程权限
重点不是背命令,而是理解安全边界:UID/GID、文件权限、capabilities、namespace、cgroup、进程环境、Unix socket。服务应以非 root 运行,只读根文件系统,密钥不进入命令行或普通日志。
实验:创建两个不同用户的进程,证明私钥文件和 Unix socket 只对目标进程开放;观察容器内 root 与宿主 root 的差异。
1.4 数据库与一致性
身份数据常见不变量:
- 一个租户内规范化用户名唯一;
- 一个认证器只能绑定到明确主体;
- 权限审批成功与授权生效最终一致,并有补偿;
- 离职撤权必须优先且可证明;
- 审计事件只追加,不能被业务管理员静默修改。
事务保护同库不变量;消息/outbox 解决跨系统传播;幂等键处理重复;版本号或条件更新处理并发;对账任务发现长期漂移。
1.4.1 身份状态机
账号状态不要用多个相互冲突的布尔值表示。示意状态:
text
PENDING → ACTIVE → SUSPENDED → DISABLED → DELETED
↘ LOCKED ↗LOCKED通常是短期认证保护,不等于 HR 离职的DISABLED。DELETED可能受审计和数据保留约束,不应被普通恢复接口复活。- 所有转换列出允许的 actor、前置状态、幂等键、审计和补偿。
示意 SQL:
sql
UPDATE identity_account
SET status = 'DISABLED', version = version + 1, disabled_at = :event_time
WHERE account_id = :id
AND status IN ('ACTIVE', 'LOCKED', 'SUSPENDED')
AND version = :expected_version;受影响行数为 0 不一定是失败:可能事件重复,也可能版本冲突。服务需要读取当前状态,区分“已达到目标”和“被其他变更抢先”。
图 1-2:1.4.1 身份状态机 · 可编辑 Mermaid 源码
1.5 缓存的安全含义
缓存会扩大撤销延迟。设计授权缓存时必须定义:key 包含哪些策略/主体版本;TTL;主动失效;失效消息丢失后的上限;缓存不可用时 fail-open 还是 fail-closed。
- 金融写操作通常倾向 fail-closed。
- 低风险只读且对可用性敏感的场景可能使用短时陈旧结果。
- 决策不能只写“安全优先”,必须量化最大暴露窗口和业务损失。
1.5.1 缓存键示例
text
authz:{tenant}:{subject_id}:{subject_version}:{resource_id}:{resource_version}:{action}:{policy_version}若不包含策略或身份版本,发布新 deny 策略后旧 allow 可能继续命中。版本并不替代主动失效;它用于让旧条目自然不可达。还要监控失效消息积压、缓存年龄和 fallback 次数。
1.6 可用性与灾备
- SLI:认证成功率、P99、token 签发错误、策略决策延迟、目录同步滞后。
- SLO:例,月度认证可用性 99.99%,但要明确定义用户输入错误是否计入。
- RTO:灾难后多快恢复;RPO:最多丢多少数据。
- 降级:IdP 故障时是否允许已有低风险会话短暂继续?新高权限操作必须怎样处理?
灾备演练必须包含密钥、配置、策略、目录和审计,而不仅是数据库备份。
1.6.1 分级降级表
| 故障 | 低风险读取 | 高风险写入 | 管理操作 |
|---|---|---|---|
| IdP 新登录不可用 | 已有短会话可按策略继续 | 要求恢复后重认证 | 拒绝 |
| PDP 超时 | 可用极短缓存且记录 | 拒绝或人工通道 | 拒绝 |
| 审计管道积压 | 本地可靠缓冲 | 缓冲不足则停止 | 停止并告警 |
| 风险引擎不可用 | 使用确定性基线 | 强制 step-up | 强制高保证认证 |
此表必须由业务和安全共同批准,并在演练中验证;不能临时由值班工程师猜。
1.7 日志与可观测性
一个身份事件至少有:时间、tenant、subject pseudonymous ID、actor、session、request/correlation ID、action、resource、decision、reason code、policy version、risk、result。严禁记录密码、完整 token、私钥、恢复码和不必要的个人信息。
三种信号:
- Metrics 告诉你“有多少、是否异常”;
- Logs 告诉你“具体发生了什么”;
- Traces 告诉你“跨服务在哪里变慢或失败”。
审计日志不同于调试日志:前者服务于追责和合规,应稳定 schema、权限隔离、留存受控、具备完整性保护。
1.7.1 Reason code 设计
不要只记录 login failed。使用稳定机器码与可本地化用户文案分离:
text
AUTH_PASSWORD_MISMATCH
AUTH_ACCOUNT_DISABLED
TOKEN_ISSUER_UNTRUSTED
TOKEN_AUDIENCE_MISMATCH
AUTHZ_RELATION_NOT_FOUND
AUTHZ_STEP_UP_REQUIRED外部响应可模糊化防账号枚举,内部审计保留精确原因。指标按 reason code 聚合,异常突增才能定位配置、攻击还是依赖故障。
1.7.2 SLO 样例
- 认证服务端成功请求月可用性 ≥ 99.99%;
- token 签发 P99 < 300 ms;
- 高优先级离职撤权 P99 < 5 分钟;
- 授权决策 P99 < 20 ms,超时率 < 0.01%;
- 审计事件 99.9% 在 60 秒内可查询。
数值需按组织调整,关键是明确分母、排除项、测量点和错误预算动作。
1.8 安全开发生命周期
图 1-3:1.8 安全开发生命周期 · 可编辑 Mermaid 源码
身份项目必须有负向测试:错误 issuer/audience、过期、撤销、并发、重放、越权、跨租户、下游超时、时钟漂移、密钥轮换和降级。
1.9 威胁建模入门
先画数据流和信任边界,再用 STRIDE 提问:伪造、篡改、抵赖、信息泄露、拒绝服务、权限提升。每个威胁记录资产、前置条件、影响、现有控制、残余风险、owner 和验证方式。
不要把“使用 HTTPS”“用了 JWT”“在内网”当完整缓解措施。说明谁验证、验证什么、失败如何处理、密钥如何管理。
1.9.1 一个信任头攻击例子
架构由网关验证 token,并向应用写 X-User-Id。如果外部请求可以自带同名头,而网关没有先删除,攻击者可能伪造身份。
缓解不是“告诉客户端不要传”:网关剥离外部头,在受保护连接上重新写入;应用只接受来自已认证网关的请求;更高保证场景传递签名上下文或让应用独立验证 token。
1.10 本章实验与验收
实验:为一个登录 demo 添加 request ID、结构化审计、Prometheus 指标、超时和熔断;模拟 IdP 慢、数据库失败、消息重复、时间快 5 分钟。
验收:能解释认证服务为何不能无条件 fail-open;如何保证离职事件重复 10 次仍只产生正确结果;为什么审计日志和普通应用日志要分开。
1.10.1 故障注入清单
- DNS 返回错误或变慢;
- IdP JWKS 获取失败;
- 数据库主从延迟;
- Redis 数据全部丢失;
- 消息重复、乱序和延迟 20 分钟;
- 一台实例时钟快 5 分钟;
- 审计下游返回 429;
- 策略发布产生全量 deny。
每个场景记录用户影响、控制是否安全、告警时间、恢复步骤和残余状态。
工程复盘:一次 Redis 抖动为什么变成安全事故
小安的第一版登录服务把 session 全放在 Redis:读取超时就返回未登录,写入失败则让用户重新尝试。压测时一切正常,直到 Redis 短暂抖动,大量用户同时重新登录,认证服务和短信通道随即被流量放大。
更危险的是,撤销事件也依赖同一个 Redis。故障期间系统既无法确认正常会话,也无法及时传播高风险账号的撤销。团队最终没有采用一句简单的“失败时放行”或“失败时拒绝”,而是按动作风险分层:普通页面可使用短时间、有版本约束的缓存;转账、改密和管理员操作必须重新确认;明确撤销的主体永不使用旧缓存。
复盘证据不是“服务恢复了”,而是下面这组结果:
text
auth_dependency_timeout_total{dependency="redis"}
session_cache_age_seconds
revocation_propagation_seconds
login_success_rate / login_retry_rate小安据此补上限流、退避、依赖隔离、撤销旁路和故障演练。工程能力与安全能力在这里不是两门课:系统怎样失败,决定了权限在失败期间怎样变化。
面试与设计评审
- 认证服务缓存故障时该 fail-open 还是 fail-closed?先按资源风险分层,再说明最大暴露窗口和降级路径。
- 离职事件如何做到“至少一次投递但只生效一次”?回答 outbox、幂等键、状态机和对账。
- 身份服务 SLO 如何定义?区分系统错误、用户输入错误、核心路径与依赖。
本章小结
身份代码不是“加了安全逻辑的 CRUD”。事务、幂等、缓存失效、时钟、可观测性和灾备都会改变真实权限状态;不会处理这些问题,就无法对身份安全结果负责。