IAM 系统参考设计
以下设计面向一个自有 SaaS:浏览器通过同源后端访问业务 API;用户可以密码或 OIDC 登录;一个账号可加入多个企业;管理员与敏感操作需要近期强认证。
浏览器使用服务端 Session。移动端或对外 API 确实需要时,再引入独立 Access Token 与 Refresh Token。这样可以先把账号控制权、撤销和租户权限做清楚,避免仅为“前后端分离”而增加全部令牌复杂度。
这是一份职责与事务设计,不是经过压测的完整认证服务。SQL 使用 PostgreSQL 语法展示核心约束,未覆盖全部字段、索引、迁移和生产配置。
组件边界
初期可在一个服务中实现认证模块,不必为每个概念拆微服务。关键是接口与数据所有权清晰:业务模块不能直接把账号设为“已通过 MFA”,认证模块也不负责解释订单退款状态。
关系数据库作为安全状态的权威来源。缓存是经过约束的性能优化;跨库撤销不假设天然原子。外部提供方只返回身份或目录结果,本地仍校验账号、成员与资源权限。
核心表约束
下面是可独立阅读的最小结构示例。UUID 由应用生成,时间按 UTC 处理;手机号、密码、外部身份、认证器与 Refresh Token 表参见对应文章,未在此重复。
CREATE TABLE users (
id UUID PRIMARY KEY,
status TEXT NOT NULL
CHECK (status IN ('pending', 'active', 'suspended',
'deletion_pending', 'deleted')),
security_version BIGINT NOT NULL DEFAULT 1
CHECK (security_version > 0),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE memberships (
tenant_id UUID NOT NULL,
user_id UUID NOT NULL REFERENCES users(id),
status TEXT NOT NULL CHECK (status IN ('active', 'disabled')),
authz_version BIGINT NOT NULL DEFAULT 1,
PRIMARY KEY (tenant_id, user_id)
);
CREATE TABLE sessions (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
token_digest BYTEA NOT NULL UNIQUE
CHECK (octet_length(token_digest) = 32),
security_version BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
idle_expires_at TIMESTAMPTZ NOT NULL,
absolute_expires_at TIMESTAMPTZ NOT NULL,
reauthenticated_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ,
CHECK (idle_expires_at <= absolute_expires_at),
CHECK (created_at < absolute_expires_at)
);
CREATE INDEX sessions_user_idx ON sessions(user_id);
CREATE TABLE recovery_challenges (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
token_digest BYTEA NOT NULL UNIQUE
CHECK (octet_length(token_digest) = 32),
security_version BIGINT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
consumed_at TIMESTAMPTZ
);
CREATE TABLE security_outbox (
event_id UUID PRIMARY KEY,
aggregate_id UUID NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
published_at TIMESTAMPTZ
);唯一性和外键由数据库强制,具体行为见 PostgreSQL Constraints。示例的 memberships.tenant_id 未声明外键,因为省略了租户表;实际系统应补上该约束。authz_version 与 security_version 分开,使单租户撤权不必退出所有企业。
业务角色独立关联到成员,不在全局账号上保存唯一租户角色。外部身份增加 UNIQUE(issuer, subject);登录标识的唯一索引按既定全局或租户范围建立,不能仅依赖应用查询。
密码登录事务
登录分为成本较高的凭据验证与提交前状态复核:
- 做输入限制、来源与账号维度限流,读取凭据及安全版本快照。
- 使用密码库验证;不存在账号走受限的哑元校验路径。
- 如需要 MFA,只创建短期预认证挑战,不创建可访问业务的 Session。
- 全部因素完成后,开启事务并锁定账号,复核状态和凭据安全版本。
- 生成随机会话凭据,将摘要、当前安全版本和期限写入 Session。
- 写安全事件,提交成功后返回 Cookie。
若用户在密码校验期间重置密码,步骤 4 的版本检查使旧密码验证结果不能继续创建会话。MFA 挑战同样绑定安全版本;版本变化后重走认证。
这里的加锁只有在改密、封禁等流程遵循相容的事务协议时才有效。PostgreSQL 的行锁规则提供并发工具,但不能代替整个系统的锁定顺序设计。
请求认证与业务授权
后端处理每个受保护请求时:
- 从指定 Cookie 读取会话凭据,计算摘要,查询 Session 与账号状态。
- 检查未撤销、空闲与绝对期限、安全版本一致及账号可用。
- 需要租户上下文时验证当前成员有效,再检查动作与资源权限。
- 敏感操作检查近期强认证和必要业务状态,写入事务保持幂等。
登录响应、恢复页面和敏感资料接口使用适当的禁缓存策略;错误响应与日志不携带秘密。依赖不可用时返回明确的临时错误,不跳过验证,也不把所有异常转换为“密码错误”。
接口契约
下表描述职责,不规定框架。Cookie 会话下的状态变更端点都需要相应 CSRF 防护;其他客户端按所用认证方案单独处理。
| 接口示例 | 关键约束 |
|---|---|
POST /auth/login | 限流;返回正式会话或有限的 MFA 挑战 |
POST /auth/mfa/verify | 绑定预认证上下文、限次数、单次完成 |
GET /auth/me | 返回当前主体和必要展示信息,不返回凭据 |
POST /auth/logout | 撤销当前会话,重复调用有明确幂等语义 |
GET /auth/sessions | 仅列当前账号的非秘密会话描述 |
DELETE /auth/sessions/{id} | 校验目标归属;必要时要求近期认证 |
POST /auth/password/reset-requests | 公开响应收敛,限制发送频率 |
POST /auth/password/resets | 原子消费高熵恢复凭据,更新安全状态 |
GET /auth/oidc/{provider}/start | 仅发起允许提供方的短期登录事务 |
GET /auth/oidc/{provider}/callback | 验证关联与令牌,不接受任意回跳 |
POST /auth/identities/link-requests | 近期认证,流程显式标记为绑定 |
POST /auth/reauthenticate | 提升结果绑定会话与有限有效期 |
OIDC 的提供方标识查本地配置,不把路由文本直接转成远程 URL。回调中 login 与 link 的目的保存在服务端事务中,不由最后一次回调参数任意指定。
改密、封禁与跨存储撤销
一次密码重置或封禁事务可以更新账号状态或 security_version,撤销该账号的会话与刷新资格,并写入 outbox。事务提交是本地安全事实的持久化点。
如果所有请求都从权威库读取账号版本,后续检查可以立即观察变更;已经完成检查的在途请求仍要按业务风险处理。若增加本地缓存,则需要明确延迟上限。
例如,假设低风险读请求最多缓存 15 秒,敏感写入直接读取权威状态:
- outbox 通知用于加快缓存失效,失败时重试并告警。
- 缓存 TTL 提供漏通知后的上限之一,实际还需考虑时钟与请求时长。
- 敏感写入在事务边界复核,不依赖那份读缓存。
- 对外承诺需依据故障测试,不将“15 秒配置”直接当成实测结果。
outbox 与业务修改同库提交,避免先发送事件后回滚或先提交后崩溃丢事件。投递仍可能重复,消费者按 event_id 幂等处理;通知也不携带会话原始凭据。
下图区分权威状态提交和缓存观察到变化的时间。事件用于加速失效,短期缓存上限与敏感操作回查用于约束漏通知后的影响。
管理操作的响应不等于所有服务已经完成撤销。图中的并行分支表示敏感请求不等待失效事件,两个分支没有先后保证;投递失败时任务重试并告警,在途请求与已持有的缓存仍按前述时效和事务策略处理。
容量、失败与恢复
认证服务的瓶颈与业务 API 不完全相同:密码哈希消耗 CPU 和内存,短信邮件有外部费用,刷新与回调容易在故障后集中重试。应分别控制这些工作的并发和预算。
| 场景 | 预期验证结果 |
|---|---|
| 同一标识并发注册 | 最终只有一条有效归属 |
| 同一恢复凭据并发提交 | 最多一个事务消费成功 |
| 改密与登录并发 | 旧凭据验证结果不能跨过版本复核 |
| MFA 尚未完成 | 业务 API 拒绝预认证上下文 |
| 撤销会话后访问 | 在既定时限内拒绝,敏感操作走权威检查 |
| OIDC 回调重放 | 不重复建号、绑定或创建无关联会话 |
| 刷新提交后响应丢失 | 按已设计的重登或幂等恢复策略处理 |
| 目录停权事件延迟 | 告警并执行本地权限时效策略 |
| 旧备份恢复 | 不恢复已撤销会话和过期的一次性请求 |
这些是未来实现的验收条件,不是本专题已执行的服务测试。正式落地还需要结合框架、身份提供方和部署环境验证密码库、代理头、Cookie、数据库隔离级别与密钥生命周期。
参考资料
- PostgreSQL:Constraints
- PostgreSQL:SELECT — Locking Clause