外观
统一账号与认证中心 — 真原子化架构方案
对应 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)的现实下,强行物理拆分会导致严重的**“伪原子化”缺陷**:
- 服务层面“拆而不分”:建号(user 写 -> auth 写)、激活(auth 改密 -> user 改状态)、停用(user 改 -> auth 吊销 -> IM 踢线)的生命周期 100% 强耦合,谁挂了另一方都无法独立工作,本质上是被网络硬生生切开的分布式单体;
- 数据层面事实源分裂:
status(账号状态)在两个服务的数据库里各存一份,网络抖动或超时必然导致一边ACTIVE一边PENDING的状态脑裂分叉; - 操作层面缺乏 ACID 事务:建号跨网络三步、改密跨网络两步,任何一步网络超时都会产生半入库脏数据且无从回滚;
- 运维与胶水代码泛滥:5 个人团队被迫维护两个数据库、两套 DDL 迁移以及双向 HMAC 胶水调用。
1.2 真原子化收敛的必然性
因此,架构回归本质:收敛为单一服务 nexo-account(账号与认证中心),实现:
- 单库单服务:统一数据库
nexo_account,单应用独立部署(端口3001); - 单一事实源:
status仅在users表维护全局唯一事实源,凭据表不再冗余状态; - 原生 ACID 事务保证:核心生命周期流转全部依托 PostgreSQL 本地事务原子完成,要么全成要么全滚,绝对零脏数据;
- 密码物理分表隔离:虽然同库同事务,但
credentials凭据独立分表存储,业务查询档案时绝对不触碰密码哈希,坚守 Zero-Knowledge 原则。
四条决定选型的现实:
- 单机 Docker Compose 部署(后端工程规范 §8):两套环境(test / prod)共用广州单台服务器,服务单副本、各自独立容器,外部网络互访。没有 Kafka、没有 Redis、没有 SMTP;
- 当前规模为 5 个种子用户,极简小型团队,日常极低频变更;
- 没有独立 API 网关:每个资源服务本地验签、本地持有内存吊销集合(08-23 §7.9);
- 开发窗口仅 5 天(09-08 至 09-12),需以最高效可靠的方式闭环交付。
基于以上现实,本期确立 10 项真原子化架构原则:
- 单服务单库真原子收敛:统一为
nexo-account(端口3001),维护单数据库nexo_account,消灭所有跨服务分布式事务与双写胶水代码; - 状态单一事实源:
status(PENDING/ACTIVE/DISABLED)全局仅在users表维护一份,彻底消除脑裂分叉; - 全生命周期单事务原子化:建号、激活、停用、超时处理 100% 在本地单数据库事务内完成;
- 取消跨服务全量推送:IM 不再接收全量推送,仅在产生数据关联(如开启聊天、检索人员)时向
nexo-account按需拉取并短时缓存; - 账号停用仅推送状态通知:停用时仅向下游 IM 发送
{ userId, status: 'DISABLED', disabledAt },0 个人资料传输,IM 收到后毫秒掐断长连接; - 权限平台未上线前不进行鉴权:不写假管理员白名单(无
ADMIN_USERNAMES,无requireAdmin),只要携带合法 Bearer JWT 即可调用管理接口; - 物理分表实现 Zero-Knowledge:
users(档案)与credentials(密码哈希)物理分表,系统内部随机生成初始密码,员工打开 24h 链接自主改密; - 建号即创建用户,超时未激活单条 SQL 停用:开户落库
PENDING状态,24 小时未激活单条原子 SQL 自动转为DISABLED; - 彻底清除 HR 词汇:彻底去除“岗位”、“工号”、“职位”、“在职”、“离职”、“复职”,状态仅保留
PENDING(待激活)、ACTIVE(正常)、DISABLED(停用); - 移除无外部通道支撑的死字段:彻底移除
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. 相关资料
- 2026-09-13 PRD —— 迭代需求与范围
- 用户中心总 PRD lamolabs-docs#55 —— 领域模型演进与参考
- 权限平台总 PRD nexo-im-pc#58 —— 权限平台规划(正交解耦)
- 2026-08-23 技术方案 —— 吊销集合、Webhook、HMAC
/internal/*通道 - 2026-08-30 技术方案 —— 设备会话管理、密码安全规范
- 跨仓工程规范 —— 代码分层与数据库访问规范
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 极简数据模型原则
- 单库单服务收敛:
- 所有数据收敛在
nexo_account数据库中; users表管档案与状态(唯一status事实源);credentials表管密码哈希,通过外键user_id1:1 级联;account_tickets表管单次激活与重置凭据;
- 所有数据收敛在
- 四大操作原生 ACID 事务保证:
- 开户建号:
users+credentials+account_tickets在同一个数据库事务内写入; - 设密激活:核销 ticket + 更新
credentials.password_hash+ 更新users.status='ACTIVE'在同一个数据库事务内完成; - 停用账号:更新
users.status='DISABLED'+ 吊销sessions在同一个数据库事务内完成,随后向 IM 发送通知; - 超时停用:单条 SQL 一键停用超过 24h 未激活的账号;
- 开户建号:
- 按需拉取与状态单向通知:
- 废除所有全量资料推送机制;
- 仅在账号变更为
DISABLED(停用)时,单向向 IM 发送轻量通知{ userId, status: 'DISABLED', disabledAt },触发强制断连。
6.4 账号停用阻断流程
当管理员在控制台停用某个账号时:
nexo-account在本地单事务内将users.status = 'DISABLED',同时将该用户全部活跃sessions标记为吊销;nexo-account向nexo-im-api发送停用通知POST /internal/users/{userId}/disabled,载荷含认证中心时钟的disabledAt(ISO 8601 UTC,见 §7.3.3);- IM 服务端收到通知后,立即执行
closeUser(userId, 4001, '账号已停用')强掐客户端 WebSocket 连接; - 该用户后续所有 HTTP API 请求被 401 拦截,无法重连,Access Token 180s 内自然失效。
6.5 配置管理总表
| 配置项 | 归属服务 | 默认值 | 分类 | 说明 |
|---|---|---|---|---|
AUTH_ACCESS_TOKEN_TTL_SECONDS | nexo-account | 180 | 核心安全 | 访问令牌有效期(秒),由 15 分钟缩短至 3 分钟 |
ACCESS_TOKEN_MAX_TTL_MS | nexo-im-api | 180000 | 核心安全 | 吊销集合保留时长(毫秒),必须 ≥ 认证中心 TTL |
ACTIVATION_TICKET_TTL_HOURS | nexo-account | 24 | 业务参数 | 激活链接有效时长(小时),超时未激活账号自动停用 |
RESET_TICKET_TTL_MINUTES | nexo-account | 60 | 业务参数 | 重置密码链接有效时长(分钟) |
INTERNAL_SHARED_SECRET | nexo-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/users | GET | Bearer JWT(合法即放行) | 用户列表,支持 q(姓名/用户名/昵称模糊检索)、status 筛选、分页 |
/api/users | POST | Bearer JWT(合法即放行) | 创建用户(单事务生成用户、凭据并返回 24h 激活链接) |
/api/users/{id} | GET | Bearer JWT(合法即放行) | 获取用户详情 |
/api/users/{id} | PATCH | Bearer JWT(合法即放行) | 修改基础资料(真实姓名、昵称、头像) |
/api/users/{id}/disable | POST | Bearer JWT(合法即放行) | 停用账号(单事务更新状态并吊销 Session,向 IM 发送停用通知) |
/api/users/{id}/enable | POST | Bearer JWT(合法即放行) | 恢复账号至 ACTIVE |
/api/users/{id}/tickets | POST | Bearer JWT(合法即放行) | 重新生成激活链接或 60 分钟重置密码链接 |
/api/users/batch-import | POST | Bearer JWT(合法即放行) | CSV 批量创建用户并导出激活链接 CSV |
/activate | GET/POST | 公开端点 | 员工通过激活码核销并设置新密码页面 |
/api/login | POST | 公开端点 | 用户名 + 密码登录换取 Token 与建立会话 |
/api/token/refresh | POST | 公开端点 | 刷新 Access Token |
/internal/users | GET | HMAC 签名 | IM 按需批量拉取用户公开资料 |
/internal/users/{id}/disabled | POST | HMAC 签名 | 向 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 步:发布
nexo-account:- 执行 DDL 迁移扩充
users、credentials与account_tickets; - 跑 seed 确保既有 5 个账号处于
ACTIVE状态; - 生效 180s Access Token TTL。
- 执行 DDL 迁移扩充
- 第 2 步:发布
nexo-web-sdk与nexo-im-pc:- 生效新的 60s 静默刷新阈值,避免旧客户端高频续期。
- 第 3 步:发布
nexo-im-api:- 移除定时轮询器代码,接入按需拉取端点与停用断连处理。
- 第 4 步:发布
nexo-user-console:- 静态托管部署,桌面壳侧边栏显示图标。
- 第 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 服务仅在发生数据关联(如开启聊天、检索通讯录)时才向账号中心查询人员公开档案并短时缓存 |
closeUser | IM 连接管理器的方法,接收到停用状态通知后毫秒掐断指定用户全部在线连接并返回 4001 码 |
| Zero-Knowledge 设密 | 管理员零接触明文密码,系统内部随机生成初始密码并通过有时效单次链接由员工自主设密 |