开石官网与宏斋云 ERP 是两个独立部署的站点,各自维护一套账号体系。我们刻意没有合并用户库:两个站的用户边界、运维节奏和安全爆炸半径本来就不同,合库会把"改一处、两站一起抖"的风险常态化。但用户不该被这条边界挡在门外——本文记录怎么用一张一次性票据做到会话层互通。
为什么是会话层
先排除三个更"顺手的"方案:
- 合并用户库:改动两端应用的全部账号逻辑,还牵动两个团队的发布节奏——不改;
- 共享会话 cookie:两个域名、两套会话管理,共享 cookie 等于把两边的会话安全绑在一起——不共享;
- 复制密码哈希:密码体系跟着对方升级/轮换,安全责任分不清——不复制。
剩下的是"凭证交换":一处登录后,拿一张对方可验证、且只能用一次的短时效凭证,去对方那里换自己的会话。互通由此收窄到一个明确的边界面上。
票据设计
票据是一张 RS256 签名的 JWT,有效期 120 秒,字段各有明确职责:
| 字段 | 值 | 作用 |
|---|---|---|
iss |
签发方标识 | 消费端校验来源 |
aud |
消费端标识 | 防止票据被用于其他服务 |
email |
用户邮箱 | 唯一的账号匹配依据 |
jti |
随机唯一编号 | 一次性防重放 |
iat / exp |
签发/过期时间 | 120 秒窗口 |
私钥只在签发端,公钥配在消费端——两站只需交换公钥,不共享任何私密材料。
时序:一张票的完整旅程
- 用户在母公司站已登录,点击「前往控制台」;
- 签发端校验当前会话 → 用私钥签发票据 → 返回带票据的跳转地址;
- 浏览器跳转,票据出现在消费端地址栏;
- 消费端验签 → 校验签发方、受众与有效期 → 将
jti写入一次性登记表(唯一约束); - 按
email查找本端账号:找到走本端标准登录流程建立会话;没找到返回"该邮箱尚无账号"+ 注册入口,不自动建号; - 用户带着新会话落在控制台。
安全属性核对表
| 攻击面 | 对策 | 验证方式 |
|---|---|---|
| 重放同一张票 | jti 唯一约束,第二次提交必冲突 |
实测:同票二次消费 → 明确报"已使用" |
| 票据过期后使用 | 120 秒 exp,验签即校验 |
实测:过期票 → ticket_expired |
| 伪造/篡改 | RS256 签名,公钥只在消费端 | 实测:坏票 → ticket_invalid |
| 跨服务挪用 | aud 固定为消费端 |
验签参数强制断言 |
| 误建账号 | 邮箱无匹配时只提示不建号 | 实测:陌生邮箱 → "尚无账号"+注册入口 |
三个坑
一、票据被消费了两次
联调时控制台停在错误页,提示"票据已被使用"。服务端日志显示同一张票在几毫秒内被提交两次——第一次成功签发了会话,第二次撞上唯一约束。
根因在前端:消费逻辑放在页面副作用里,副作用在依赖变化时会重跑(语言资源就绪后翻译函数换了身份,触发第二次执行)。修复两步:
- 单飞守卫:用 ref 保证每次挂载只执行一次;
- 存活标记与依赖解耦:把"组件是否还在"的标记从副作用清理函数里挪出来,只在真实卸载时失效——否则第一次请求的成功响应会在依赖变化时被丢弃,页面永远停在"正在登录"。
一次性凭证类的前端逻辑,天然不适合"可重跑的副作用"模型,必须显式约束执行次数。
二、页面放错了路由树
联调初期页面一直 404。排查发现:产品站有一条路由重写规则,把带语言前缀的 /auth/* 统一重写到根级 auth 目录——应用类页面都住在根级树里,新页面按"直觉"放进语言目录,永远匹配不到。
教训:在别人的框架里加页面,先找到同类页面的实际落点,再动手写文件。
三、密钥文件"读不到"却不报错
票据签发接口一度返回"未配置"。根因是密钥文件权限 0600,容器内进程以另一用户运行读不到;"读不到就返回空"的容错把错误静默吞掉,最终表现为服务端声称自己没配置。
修法:密钥权限与容器用户对齐(进部署检查单);"配置缺失"显式报 503,并由启动自检提前暴露——静默降级会把配置错误伪装成业务错误。
小结
两套账号体系的互通,不需要合并用户数据,也不该共享凭证。把边界收窄到"一张短时效、一次性的票据 + 邮箱匹配 + 各自标准登录",可审计、可回滚;把重放、路由与权限三类问题逐个排掉,这条链路才真正可以交付。
