Skip to content

统一账号与认证中心 — 真原子化架构方案 ​

对应 PRD 原文,总 PRD 见 lamolabs-docs#55(v1.4)。本文档定义"怎么做";"做什么"以迭代 PRD 为准。 状态:草案(待评审)

遵循第一性原理与真原子化设计原则:将用户主数据与认证体系收敛为单一服务 nexo-account(账号与认证中心)。通过单服务、单数据库(nexo_account)架构,将开户建号、设密激活、停用阻断、超时禁用四大核心操作完全收敛为本地原生数据库事务(ACID),从根源上消灭“拆而不分”的分布式单体硬伤、跨网络半入库脏数据与状态脑裂分叉;IM 仅在产生数据关联时按需拉取公开资料;账号停用时仅向下游推送轻量状态通知;取消未就绪的假权限与管理员判定逻辑;采用内部随机初始密码 + 24h 激活凭据映射直接引导员工自主设密。

1. 背景 ​

前两期把"谁能登录"这件事做对了:nexo-auth 独立签发凭证,资源服务本地验签,吊销 5 秒内生效。但"人员档案维护、账号生命周期"至今没有权威入口:

  • nexo_auth.users 是一张纯凭据表,只有 username / nickname / avatar_url / password_hash——没有账号状态、没有"停用"概念。想让一个人不能再登录,唯一办法是直接改数据库;
  • IM 的 user_profiles 靠每 5 分钟从认证中心拉一次全量 + 24 小时过期刷新维持(profile-sync.ts),新同事最坏 5 分钟后才出现在通讯录;
  • 建号靠 SEED_USERS 环境变量重跑 seed,改密靠 RESET_SEED_PASSWORDS=1。5 个人还能勉强维持,新员工入职或人员变动就必须修改部署配置。

1.1 过度微服务拆分的“伪原子化”教训 ​

在设计演进中,曾探讨将系统拆分为独立的 nexo-user(用户主数据)与 nexo-auth(凭据认证)两个微服务。但深入评估表明,在单机 Docker Compose 且缺乏分布式事务基础设施(2PC / Saga / Outbox)的现实下,强行物理拆分会导致严重的**“伪原子化”缺陷**:

  1. 服务层面“拆而不分”:建号(user 写 -> auth 写)、激活(auth 改密 -> user 改状态)、停用(user 改 -> auth 吊销 -> IM 踢线)的生命周期 100% 强耦合,谁挂了另一方都无法独立工作,本质上是被网络硬生生切开的分布式单体;
  2. 数据层面事实源分裂:status(账号状态)在两个服务的数据库里各存一份,网络抖动或超时必然导致一边 ACTIVE 一边 PENDING 的状态脑裂分叉;
  3. 操作层面缺乏 ACID 事务:建号跨网络三步、改密跨网络两步,任何一步网络超时都会产生半入库脏数据且无从回滚;
  4. 运维与胶水代码泛滥:5 个人团队被迫维护两个数据库、两套 DDL 迁移以及双向 HMAC 胶水调用。

1.2 真原子化收敛的必然性 ​

因此,架构回归本质:收敛为单一服务 nexo-account(账号与认证中心),实现:

  • 单库单服务:统一数据库 nexo_account,单应用独立部署(端口 3001);
  • 单一事实源:status 仅在 users 表维护全局唯一事实源,凭据表不再冗余状态;
  • 原生 ACID 事务保证:核心生命周期流转全部依托 PostgreSQL 本地事务原子完成,要么全成要么全滚,绝对零脏数据;
  • 密码物理分表隔离:虽然同库同事务,但 credentials 凭据独立分表存储,业务查询档案时绝对不触碰密码哈希,坚守 Zero-Knowledge 原则。

四条决定选型的现实:

  1. 单机 Docker Compose 部署(后端工程规范 §8):两套环境(test / prod)共用广州单台服务器,服务单副本、各自独立容器,外部网络互访。没有 Kafka、没有 Redis、没有 SMTP;
  2. 当前规模为 5 个种子用户,极简小型团队,日常极低频变更;
  3. 没有独立 API 网关:每个资源服务本地验签、本地持有内存吊销集合(08-23 §7.9);
  4. 开发窗口仅 5 天(09-08 至 09-12),需以最高效可靠的方式闭环交付。

基于以上现实,本期确立 10 项真原子化架构原则:

  1. 单服务单库真原子收敛:统一为 nexo-account(端口 3001),维护单数据库 nexo_account,消灭所有跨服务分布式事务与双写胶水代码;
  2. 状态单一事实源:status(PENDING/ACTIVE/DISABLED)全局仅在 users 表维护一份,彻底消除脑裂分叉;
  3. 全生命周期单事务原子化:建号、激活、停用、超时处理 100% 在本地单数据库事务内完成;
  4. 取消跨服务全量推送:IM 不再接收全量推送,仅在产生数据关联(如开启聊天、检索人员)时向 nexo-account 按需拉取并短时缓存;
  5. 账号停用仅推送状态通知:停用时仅向下游 IM 发送 { userId, status: 'DISABLED', disabledAt },0 个人资料传输,IM 收到后毫秒掐断长连接;
  6. 权限平台未上线前不进行鉴权:不写假管理员白名单(无 ADMIN_USERNAMES,无 requireAdmin),只要携带合法 Bearer JWT 即可调用管理接口;
  7. 物理分表实现 Zero-Knowledge:users(档案)与 credentials(密码哈希)物理分表,系统内部随机生成初始密码,员工打开 24h 链接自主改密;
  8. 建号即创建用户,超时未激活单条 SQL 停用:开户落库 PENDING 状态,24 小时未激活单条原子 SQL 自动转为 DISABLED;
  9. 彻底清除 HR 词汇:彻底去除“岗位”、“工号”、“职位”、“在职”、“离职”、“复职”,状态仅保留 PENDING(待激活)、ACTIVE(正常)、DISABLED(停用);
  10. 移除无外部通道支撑的死字段:彻底移除 email(无 SMTP)与 phone(无 SMS),仅保留核心档案。

2. 需求分析 ​

2.1 依赖关系 ​

依赖拓扑清晰单向:

  • nexo-account 是唯一的人员身份档案、凭证、Token 签发与生命周期事实源;
  • nexo-im-api 日常不接收任何资料推送,仅在发生会话或人员检索时按需向 nexo-account 查询;
  • 唯一的下行通知是账号停用事件,IM 接收后仅执行强制掐线。

2.2 不可退让的约束(红线) ​

约束来源不这么做的后果
全生命周期原生 ACID 原子性架构第一性原理坚决消灭跨网络分步调用,杜绝半入库脏数据与状态脑裂
状态全局单一事实源架构第一性原理status 仅在 users 表存储一份,严禁在凭据表二次冗余
登录热路径零外部依赖总 PRD DEC-01登录、续期、验签完全在 nexo-account 本地完成,无任何跨服务同步阻塞
资源服务本地验签08-23 技术方案 §6.1资源服务依靠 JWKS 本地校验 Token,不反向回查认证中心
密码零接触 (Zero-Knowledge)PRD §9、08-30 §7.10管理员不生成、不查看、不输入任何明文密码;初始密码内部随机并由员工自助改密
权限平台未上线时不进行伪鉴权本期共识杜绝硬编码假管理员角色逻辑,只要携带合法 JWT 即可放行

3. 产品方案 ​

详见 PRD 原文。技术实现与产品功能一一映射。

4. 相关资料 ​

5. 参与人 ​

角色职责
账号与认证服务端 (nexo-account)统一服务维护、users / credentials / account_tickets 表结构维护、用户管理 API、/activate 设密页、OAuth2 登录与 Token 签发、向 IM 发送停用通知
IM 服务端改造 (nexo-im-api)废除 5 分钟轮询器、实现按需拉取缓存、消费停用通知执行 closeUser(4001)
控制台前端 (nexo-user-console)用户列表、创建用户抽屉与激活链接展示、停用与启用操作
全流程验收全员依照 TC 离线用例实测核验

6. 整体设计 ​

6.1 改造范围 ​

6.2 职责边界 ​

服务管什么不管什么
nexo-account统一人员档案(users)、登录凭据(credentials)、Token 签发、激活与重置 Ticket、/activate 设密页、用户列表管理 API、向 IM 发送停用通知即时通讯、长连接维持、会话消息、角色权限分配(由未来独立权限平台接管)
nexo-im-api即时通讯、消息存储流转、在线长连接维护、按需缓存用户信息、closeUser 强制掐线账号开户、修改用户资料、密码凭证管理
控制台用户管理界面、录入用户名姓名、激活链接展示与复制、账号停用与启用权限判定逻辑(当前只要携带有效 JWT 即可操作)

6.3 极简数据模型原则 ​

  1. 单库单服务收敛:
    • 所有数据收敛在 nexo_account 数据库中;
    • users 表管档案与状态(唯一 status 事实源);
    • credentials 表管密码哈希,通过外键 user_id 1:1 级联;
    • account_tickets 表管单次激活与重置凭据;
  2. 四大操作原生 ACID 事务保证:
    • 开户建号:users + credentials + account_tickets 在同一个数据库事务内写入;
    • 设密激活:核销 ticket + 更新 credentials.password_hash + 更新 users.status='ACTIVE' 在同一个数据库事务内完成;
    • 停用账号:更新 users.status='DISABLED' + 吊销 sessions 在同一个数据库事务内完成,随后向 IM 发送通知;
    • 超时停用:单条 SQL 一键停用超过 24h 未激活的账号;
  3. 按需拉取与状态单向通知:
    • 废除所有全量资料推送机制;
    • 仅在账号变更为 DISABLED(停用)时,单向向 IM 发送轻量通知 { userId, status: 'DISABLED', disabledAt },触发强制断连。

6.4 账号停用阻断流程 ​

当管理员在控制台停用某个账号时:

  1. nexo-account 在本地单事务内将 users.status = 'DISABLED',同时将该用户全部活跃 sessions 标记为吊销;
  2. nexo-account 向 nexo-im-api 发送停用通知 POST /internal/users/{userId}/disabled,载荷含认证中心时钟的 disabledAt(ISO 8601 UTC,见 §7.3.3);
  3. IM 服务端收到通知后,立即执行 closeUser(userId, 4001, '账号已停用') 强掐客户端 WebSocket 连接;
  4. 该用户后续所有 HTTP API 请求被 401 拦截,无法重连,Access Token 180s 内自然失效。

6.5 配置管理总表 ​

配置项归属服务默认值分类说明
AUTH_ACCESS_TOKEN_TTL_SECONDSnexo-account180核心安全访问令牌有效期(秒),由 15 分钟缩短至 3 分钟
ACCESS_TOKEN_MAX_TTL_MSnexo-im-api180000核心安全吊销集合保留时长(毫秒),必须 ≥ 认证中心 TTL
ACTIVATION_TICKET_TTL_HOURSnexo-account24业务参数激活链接有效时长(小时),超时未激活账号自动停用
RESET_TICKET_TTL_MINUTESnexo-account60业务参数重置密码链接有效时长(分钟)
INTERNAL_SHARED_SECRETnexo-account, nexo-im-api无默认值密钥IM 按需拉取与停用通知的 HMAC 签名密钥

7. 模块设计 ​

7.1 数据模型(统一单库 nexo_account) ​

SQL DDL 定义 ​

sql
-- 1. 用户主数据与生命周期表(全局唯一 status 事实源!)
CREATE TABLE users (
  id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  username    text NOT NULL UNIQUE,
  real_name   text NOT NULL,
  nickname    text,
  avatar_url  text,
  status      text NOT NULL DEFAULT 'PENDING' 
              CHECK (status IN ('PENDING', 'ACTIVE', 'DISABLED')),
  created_at  timestamptz NOT NULL DEFAULT now(),
  updated_at  timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX idx_users_status ON users (status);

-- 2. 安全凭据表(与档案物理分表,杜绝资料查询触碰密码哈希,不存冗余 status)
CREATE TABLE credentials (
  user_id        uuid PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
  password_hash  text NOT NULL,
  updated_at     timestamptz NOT NULL DEFAULT now()
);

-- 3. 激活与改密凭据表(24h 单次 Ticket)
CREATE TABLE account_tickets (
  id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id     uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  purpose     text NOT NULL CHECK (purpose IN ('activation', 'reset')),
  token_hash  text NOT NULL UNIQUE,
  expires_at  timestamptz NOT NULL,
  used_at     timestamptz,
  created_at  timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX idx_tickets_valid ON account_tickets (user_id, purpose) 
  WHERE used_at IS NULL;

7.2 四大原生原子操作设计 ​

7.2.1 操作 ①:开户建号(本地单事务原子化) ​

原生原子保证:档案创建、随机凭据初始化与 24h 激活凭据在单个本地 PostgreSQL 事务内完成。绝无半入库脏数据!

7.2.2 操作 ②:设密激活(本地单事务原子化) ​

原生原子保证:Ticket 单次核销、新密码落库与全局状态激活在同一本地事务原子生效。绝无“改密成功但状态仍为 PENDING”或“多次重复使用链接”的漏洞。

7.2.3 操作 ③:停用账号(本地单事务 + 外部通知) ​

sql
-- 1. 本地单事务原子生效
BEGIN;
  UPDATE users SET status = 'DISABLED', updated_at = now() WHERE id = $userId;
  UPDATE sessions SET revoked_at = now() WHERE user_id = $userId;
COMMIT;

-- 2. 事务成功后,发送轻量通知至 IM
-- POST /internal/users/{userId}/disabled  Payload: { userId, status: 'DISABLED', disabledAt }(disabledAt: ISO 8601 UTC,认证中心时钟)

IM 接收后毫秒级调用 closeUser(userId, 4001, '账号已停用') 强掐客户端长连接。

7.2.4 操作 ④:24h 超时未激活自动停用(单条原子 SQL) ​

sql
UPDATE users 
   SET status = 'DISABLED', updated_at = now()
 WHERE status = 'PENDING' 
   AND created_at < now() - interval '24 hours';

无需跨库分别扫描,一条极简 SQL 搞定全部超时账号。后台提供【重新生成激活链接】操作,单事务重置状态为 PENDING 并生成新 Ticket。

7.3 IM 按需拉取与停用切断 ​

7.3.1 彻底清理历史轮询欠账 ​

  • nexo-im-api 彻底删除 profile-sync.ts 中的 startDirectorySync()(5分钟轮询)与 24 小时强制刷新逻辑(还清 DEBT-2)。

7.3.2 按需拉取与短时缓存 ​

  • 当用户进入聊天界面、查看群成员或检索用户时,IM 服务在内存中维护轻量 LRU 缓存(TTL 5分钟);
  • 若本地缓存未命中,调用 nexo-account 的内部查询接口: GET /internal/users?ids=uuid1,uuid2(HMAC 认证);
  • nexo-account 返回该批用户的公开基础资料(id, username, real_name, nickname, avatar_url, status)。

7.3.3 停用状态轻量通知 ​

当账号停用时,nexo-account 仅向 IM 发送状态更新:

  • 请求:POST /internal/users/{userId}/disabled
  • 载荷:{ "userId": "...", "status": "DISABLED", "disabledAt": "<ISO 8601 UTC>" }(0 冗余个人资料——时间戳不是个人资料)
  • disabledAt 语义:必填,取认证中心时钟的停用时刻(ISO 8601 UTC)。IM 以它为用户级吊销基准,执行只前进不后退的幂等判定:本次 disabledAt 不晚于已记录时刻则忽略。停用通知重投不得覆盖更晚的「重新启用」,否则重新启用后签发的合法令牌会被误拦。
  • IM 处理:
    ts
    export async function handleUserDisabled(userId: string, disabledAt: string) {
      // 1. 清理本地缓存
      userProfileCache.delete(userId);
      // 2. 以 disabledAt 记入用户级吊销集合(只前进不后退),并毫秒掐断长连接
      markUserRevoked(userId, new Date(disabledAt)); // 内部 closeUser(userId, 4001, '账号已停用')
    }

7.4 接口清单与免鉴权放行 ​

在独立的权限平台(#58)上线之前,本系统严禁引入任何硬编码管理员名单或伪鉴权逻辑。管理接口只要校验 Bearer JWT 合法即可放行。

接口方法鉴权说明用途
/api/usersGETBearer JWT(合法即放行)用户列表,支持 q(姓名/用户名/昵称模糊检索)、status 筛选、分页
/api/usersPOSTBearer JWT(合法即放行)创建用户(单事务生成用户、凭据并返回 24h 激活链接)
/api/users/{id}GETBearer JWT(合法即放行)获取用户详情
/api/users/{id}PATCHBearer JWT(合法即放行)修改基础资料(真实姓名、昵称、头像)
/api/users/{id}/disablePOSTBearer JWT(合法即放行)停用账号(单事务更新状态并吊销 Session,向 IM 发送停用通知)
/api/users/{id}/enablePOSTBearer JWT(合法即放行)恢复账号至 ACTIVE
/api/users/{id}/ticketsPOSTBearer JWT(合法即放行)重新生成激活链接或 60 分钟重置密码链接
/api/users/batch-importPOSTBearer JWT(合法即放行)CSV 批量创建用户并导出激活链接 CSV
/activateGET/POST公开端点员工通过激活码核销并设置新密码页面
/api/loginPOST公开端点用户名 + 密码登录换取 Token 与建立会话
/api/token/refreshPOST公开端点刷新 Access Token
/internal/usersGETHMAC 签名IM 按需批量拉取用户公开资料
/internal/users/{id}/disabledPOSTHMAC 签名向 IM 通知账号停用状态

7.5 控制台前端设计 (nexo-user-console) ​

  • 用户列表:展示【姓名】、【用户名】、【昵称】、【状态】、【创建时间】、【操作】;
  • 新建用户抽屉:输入用户名与真实姓名(昵称选填),点击确定直接生成激活链接与一键复制按钮;
  • 停用账号模态框:红色高危提示,必须输入员工真实姓名全称,防止误触;
  • 重新启用操作:一键使停用员工恢复正常;
  • CSV 批量导入:模板仅需两列 username, real_name(nickname 选填),导入后支持一键下载包含全员激活链接的 CSV 文件。

8. 排期 ​

5 个开发工作日(09-08 至 09-12)。服务收敛为单体、消灭了跨服务分布式事务与双库同步,开发范围极度聚焦:

日期核心交付内容风险控制
09-08 (周二)① 扩展 nexo-account 表结构(users / credentials / account_tickets);
② 落地单事务建号开户逻辑;
③ 落地 /activate 单事务设密激活页面
锁定单事务 ACID 逻辑
09-09 (周三)① 落地用户管理 REST API(列表、创建、停用、启用、重发链接);
② 接入停用单事务与 IM closeUser(4001) 掐线;
③ 搭建控制台用户列表前端与新建抽屉
联调停用断连与 4001 码提示
09-10 (周四)① IM 服务彻底删除 5 分钟轮询器,改造为按需拉取缓存;
② 落地 CSV 批量导入;
③ Access Token TTL 设为 180s(与 SDK 刷新阈值同批发布)
验证按需拉取响应速度
09-11 (周五)① 桌面壳 nexo-app 侧边栏挂载用户管理入口;
② 全链路联调冒烟(建号 → 激活 → 登录 → IM 通讯 → 停用断连);
③ 交互细节打磨
全链路端到端闭环验证
09-12 (周六)冒烟演练与预跑 TC 用例,产出离线核验证据,封版准备周日验收封版
09-13 (周日)召开 2026-09-13 迭代验收会,正式发布交付45 分钟验收 + 15 分钟规划下期

9. 发布计划 ​

单一后端服务部署,流程极其清晰顺畅:

  1. 第 1 步:发布 nexo-account:
    • 执行 DDL 迁移扩充 users、credentials 与 account_tickets;
    • 跑 seed 确保既有 5 个账号处于 ACTIVE 状态;
    • 生效 180s Access Token TTL。
  2. 第 2 步:发布 nexo-web-sdk 与 nexo-im-pc:
    • 生效新的 60s 静默刷新阈值,避免旧客户端高频续期。
  3. 第 3 步:发布 nexo-im-api:
    • 移除定时轮询器代码,接入按需拉取端点与停用断连处理。
  4. 第 4 步:发布 nexo-user-console:
    • 静态托管部署,桌面壳侧边栏显示图标。
  5. 第 5 步:线上冒烟:
    • 管理员建号 → 员工激活 → 发消息 → 停用即刻踢线。

10. 稳定性保障 ​

  • 真正的原子性(True Atomicity):核心操作全部在本地单事务内完成,底层数据库机制保障强一致,绝对杜绝脏数据;
  • 单一事实源:全局唯一 status 字段,不存在脑裂分叉可能;
  • 物理分表安全隔离:业务查询绝不加载密码散列字段,管理员全流程零接触明文密码;
  • 停用即时掐断:停用触发两路机制(IM 长连接 4001 毫秒切断 + Session 吊销进内存黑名单),配合 180s 短效 Token 筑牢阻断闭环;
  • 单机低开销:单表极简查询,内存占用低,单机单核环境负载极低。

11. 风险 ​

风险点应对方案
激活链接超过 24h 未使用账号自动变为 DISABLED 状态,控制台提供【重新生成链接】操作,一键恢复 PENDING 并生成新链接
停用误触停用弹窗强制要求手工输入员工真实姓名全称,后端全等校验阻断误操作
Token TTL 缩短带来的续期频率SDK 刷新阈值设为 60s,5 个用户平均每分钟仅产生 2~3 次轻量点查请求,无性能压力

12. 待评审确认点 ​

#确认项建议结论说明
1是否同意收敛为单一服务 nexo-account同意彻底消灭伪原子化与跨库分布式事务,实现原生 ACID 强一致
2是否同意取消向 IM 全量推送,改为按需拉取同意仅停用时通知状态,消除无意义的广播开销
3是否同意权限平台上线前不进行鉴权同意杜绝硬编码假管理员白名单,保持系统架构纯洁
4是否同意初始密码内部随机生成,链接直接引导改密同意管理员零知识、不碰密码,员工体验更直接
5是否同意彻底去除 HR 词汇,状态仅为待激活/正常/停用同意回归系统账号本真,简单清晰

13. 名词解释 ​

术语含义说明
统一账号与认证中心 (nexo-account)系统的企业人员档案、安全凭证与认证基础设施权威源
真原子化 (True Atomicity)核心生命周期流转(建号、激活、停用、超时)全部基于本地单数据库事务(ACID)完成,零跨网络半状态
单一事实源 (Single Source of Truth)status 仅在 users 表维护全局唯一字段,凭据表不再冗余状态,杜绝脑裂
待激活 (PENDING)账号已创建并生成激活码,员工尚未设置新密码的初始状态;超过 24 小时未激活自动转为停用
正常 (ACTIVE)账号已成功设置密码,可正常登录系统与收发消息
停用 (DISABLED)账号处于禁用状态,长连接被强制切断,禁止登录与 API 访问
按需拉取 (On-Demand Fetch)IM 服务仅在发生数据关联(如开启聊天、检索通讯录)时才向账号中心查询人员公开档案并短时缓存
closeUserIM 连接管理器的方法,接收到停用状态通知后毫秒掐断指定用户全部在线连接并返回 4001 码
Zero-Knowledge 设密管理员零接触明文密码,系统内部随机生成初始密码并通过有时效单次链接由员工自主设密