Skip to content

IAM 系统参考设计 ​

以下设计面向一个自有 SaaS:浏览器通过同源后端访问业务 API;用户可以密码或 OIDC 登录;一个账号可加入多个企业;管理员与敏感操作需要近期强认证。

浏览器使用服务端 Session。移动端或对外 API 确实需要时,再引入独立 Access Token 与 Refresh Token。这样可以先把账号控制权、撤销和租户权限做清楚,避免仅为“前后端分离”而增加全部令牌复杂度。

这是一份职责与事务设计,不是经过压测的完整认证服务。SQL 使用 PostgreSQL 语法展示核心约束,未覆盖全部字段、索引、迁移和生产配置。

组件边界 ​

初期可在一个服务中实现认证模块,不必为每个概念拆微服务。关键是接口与数据所有权清晰:业务模块不能直接把账号设为“已通过 MFA”,认证模块也不负责解释订单退款状态。

关系数据库作为安全状态的权威来源。缓存是经过约束的性能优化;跨库撤销不假设天然原子。外部提供方只返回身份或目录结果,本地仍校验账号、成员与资源权限。

核心表约束 ​

下面是可独立阅读的最小结构示例。UUID 由应用生成,时间按 UTC 处理;手机号、密码、外部身份、认证器与 Refresh Token 表参见对应文章,未在此重复。

sql
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);登录标识的唯一索引按既定全局或租户范围建立,不能仅依赖应用查询。

密码登录事务 ​

登录分为成本较高的凭据验证与提交前状态复核:

  1. 做输入限制、来源与账号维度限流,读取凭据及安全版本快照。
  2. 使用密码库验证;不存在账号走受限的哑元校验路径。
  3. 如需要 MFA,只创建短期预认证挑战,不创建可访问业务的 Session。
  4. 全部因素完成后,开启事务并锁定账号,复核状态和凭据安全版本。
  5. 生成随机会话凭据,将摘要、当前安全版本和期限写入 Session。
  6. 写安全事件,提交成功后返回 Cookie。

若用户在密码校验期间重置密码,步骤 4 的版本检查使旧密码验证结果不能继续创建会话。MFA 挑战同样绑定安全版本;版本变化后重走认证。

这里的加锁只有在改密、封禁等流程遵循相容的事务协议时才有效。PostgreSQL 的行锁规则提供并发工具,但不能代替整个系统的锁定顺序设计。

请求认证与业务授权 ​

后端处理每个受保护请求时:

  1. 从指定 Cookie 读取会话凭据,计算摘要,查询 Session 与账号状态。
  2. 检查未撤销、空闲与绝对期限、安全版本一致及账号可用。
  3. 需要租户上下文时验证当前成员有效,再检查动作与资源权限。
  4. 敏感操作检查近期强认证和必要业务状态,写入事务保持幂等。

登录响应、恢复页面和敏感资料接口使用适当的禁缓存策略;错误响应与日志不携带秘密。依赖不可用时返回明确的临时错误,不跳过验证,也不把所有异常转换为“密码错误”。

接口契约 ​

下表描述职责,不规定框架。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、数据库隔离级别与密钥生命周期。

参考资料 ​

协议要求和安全措施的来源分别见密码与恢复、Session、令牌、OIDC与运行保障。