Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
3.9 KiB
ADR-010 鉴权会话方案
状态:已接受(2026-07-27)
背景出处:编码规范 §4「鉴权定调:单组织 cookie session —— 登录、会话与岗位权限校验中间件在
apps/api 的 app 层实现,权限判定读 org 出口;不引 OAuth/OIDC 与多租户机制」;
§12 留白清单「鉴权会话与凭证加密方案 —— 实现期再定,各建 ADR 后方可动工」。
问题
在此之前系统没有登录、也没有登出。中间件只做一件事:
const match = /(?:^|;\s*)jcai_account=([^;]+)/.exec(cookie);
c.set('account', match?.[1] ?? DEFAULT_LOGIN_ACCOUNT);
三个后果,前两个是缺功能,第三个是安全缺陷:
- 没有登录口 —— 谁打开页面都是种子登录者冯兵,无法验多人协作 (A1 冯兵 → A3 林佩萱 → AP 丁子乔 的流转根本走不通)
- 没有登出口 —— cookie 从未被写入,也就无从清除
- 身份可任意伪造 —— cookie 是明文账号,浏览器里改一个字符即可冒充任何人, 包括审批人。「驳回必附意见」「完成只出自放行通过」这类规矩全部形同虚设: 规矩管的是这个人做了什么,而这个人是谁可以随手改写
决策
一、凭据:口令 + scrypt
staff 增列 password_hash。散列用 Node 内置 crypto.scrypt(N=16384, r=8, p=1),
格式 scrypt$N$r$p$<salt-b64>$<hash-b64>,比对走 timingSafeEqual。
不引第三方散列库:scrypt 是 Node 标准库自带的内存硬散列,参数与格式自描述, 零依赖即可满足 ≤50 人单组织的口令存储要求。
种子口令取自环境变量 JCAI_SEED_PASSWORD,缺省 jcai-dev-2026,播种时在 stdout 明示。
源码里不写任何真实口令。
二、会话:服务端会话表 + 不透明 token
新表 sys.session:id(即 token,32 字节随机 base64url)、staff_id、
issued_at、expires_at、last_seen_at。cookie jcai_session,
HttpOnly; SameSite=Lax; Path=/,有效期 12 小时,每次命中滑动续期。
为什么不用签名 cookie(HMAC 自包含):登出必须立刻失效。签名 cookie 在到期前
无法作废,除非另建吊销表 —— 那还不如直接用会话表。会话表还顺带解决另一件事:
org 停用员工时可以一并踢掉他的在途会话,否则被停用的人凭旧 cookie 能继续办事。
三、归属:会话归 org,中间件归 app
会话是身份的一部分,而 org 已经拥有 staff / position / grant 与 loadSession。
所以 login / logout / resolveSession / setPassword 落 org 出口;
app 层只放中间件与两条路由 —— 与 §4「中间件在 app 层实现,权限判定读 org 出口」一致。
四、免鉴权白名单
只三条,其余一律 401:
| 路由 | 理由 |
|---|---|
GET /api/health |
存活探针 |
POST /api/login |
登录本身 |
POST /api/ingest/mail/:channelId |
外界边界:代替真实 IMAP 拉件,不是人的请求 |
/api/stream(SSE)此前完全无鉴权,现纳入鉴权 —— 它推的是「哪个目标动了」,
虽不含业务载荷,但目标 ID 本身即信息。
五、明文 jcai_account cookie 立即废止,不走三步弃用
三步弃用是给契约变更留的缓冲期。这一条是安全缺陷修复:保留兼容期
等于保留一条「改 cookie 即可冒充审批人」的旁路。同理,DEFAULT_LOGIN_ACCOUNT
不再作为请求身份的兜底,只保留其种子语义(播种时谁是第一个员工)。
后果
- 前端必须有登录页:
/api/session返回 401 即落登录页,登录成功后进原台面 - 开发期用 curl 打接口需要先
POST /api/login拿 cookie,不能再靠缺省身份 - 口令重置目前只在种子期给出;治理台的「员工」面后续可加重置动作(未做,见 debt)
- 未做:失败次数限制 / 锁定、口令强度校验、首登强制改密。单组织 ≤50 人、
部署在内网的前提下先不做,列入
docs/debt.md