Skip to content

第 1 章:登录接口能跑就够了吗?身份系统先过分布式系统这一关 ​

本章主线:通过一次“登录偶尔超时、离职权限没有及时失效”的故障,理解身份平台为何首先需要扎实的软件工程能力。

开场问题 ​

小安写完登录接口,本地测试全部通过。上线后,Redis 抖动导致部分用户被退出,消息重复使下游反复禁用账号,服务器时间快了五分钟又让 token 集体失效。

问题:这些是“普通稳定性问题”,还是身份安全问题?答案是两者都是。身份系统的错误会直接改变谁能访问什么,可用性与安全性必须共同设计。

学习目标与前置知识 ​

学完后应能:追踪一次请求;设计幂等状态变更;定义身份 SLO;区分调试日志和审计;对依赖失败做风险分级降级。前置需要 HTTP、SQL、Linux 和基本容器知识。

1.1 为什么身份系统首先是分布式系统 ​

登录和授权位于几乎所有业务请求的关键路径。一次 IdP 故障可能让全公司无法办公;一次缓存不一致可能让已离职员工继续访问;一次时钟漂移可能让全部 token 看似过期。

你必须同时保证:正确性、安全性、可用性、低延迟、可审计和可恢复。安全控制若导致业务不可用,同样是失败。

1.2 HTTP 请求从浏览器到身份服务 ​

图 1-1: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 身份状态机

图 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 安全开发生命周期

图 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”。事务、幂等、缓存失效、时钟、可观测性和灾备都会改变真实权限状态;不会处理这些问题,就无法对身份安全结果负责。