跳到主要内容
新品云 ERP 正式上线,几分钟开通专属实例

技术分享

两个独立账号体系如何互通:一次性票据 SSO 的设计与三个坑

开石官网与宏斋云 ERP 是两个独立部署的站点,各自维护一套账号体系。我们刻意没有合并用户库:两个站的用户边界、运维节奏和安全爆炸半径本来就不同,合库会把"改一处、两站一起抖"的风险常态化。但用户不该被这条边界挡在门外——本文记录怎么用一张一次性票据做到会话层互通。

为什么是会话层

先排除三个更"顺手的"方案:

  • 合并用户库:改动两端应用的全部账号逻辑,还牵动两个团队的发布节奏——不改;
  • 共享会话 cookie:两个域名、两套会话管理,共享 cookie 等于把两边的会话安全绑在一起——不共享;
  • 复制密码哈希:密码体系跟着对方升级/轮换,安全责任分不清——不复制。

剩下的是"凭证交换":一处登录后,拿一张对方可验证、且只能用一次的短时效凭证,去对方那里换自己的会话。互通由此收窄到一个明确的边界面上。

票据设计

票据是一张 RS256 签名的 JWT,有效期 120 秒,字段各有明确职责:

字段 值 作用
iss 签发方标识 消费端校验来源
aud 消费端标识 防止票据被用于其他服务
email 用户邮箱 唯一的账号匹配依据
jti 随机唯一编号 一次性防重放
iat / exp 签发/过期时间 120 秒窗口

私钥只在签发端,公钥配在消费端——两站只需交换公钥,不共享任何私密材料。

时序:一张票的完整旅程

  1. 用户在母公司站已登录,点击「前往控制台」;
  2. 签发端校验当前会话 → 用私钥签发票据 → 返回带票据的跳转地址;
  3. 浏览器跳转,票据出现在消费端地址栏;
  4. 消费端验签 → 校验签发方、受众与有效期 → 将 jti 写入一次性登记表(唯一约束);
  5. 按 email 查找本端账号:找到走本端标准登录流程建立会话;没找到返回"该邮箱尚无账号"+ 注册入口,不自动建号;
  6. 用户带着新会话落在控制台。

安全属性核对表

攻击面 对策 验证方式
重放同一张票 jti 唯一约束,第二次提交必冲突 实测:同票二次消费 → 明确报"已使用"
票据过期后使用 120 秒 exp,验签即校验 实测:过期票 → ticket_expired
伪造/篡改 RS256 签名,公钥只在消费端 实测:坏票 → ticket_invalid
跨服务挪用 aud 固定为消费端 验签参数强制断言
误建账号 邮箱无匹配时只提示不建号 实测:陌生邮箱 → "尚无账号"+注册入口

三个坑

一、票据被消费了两次

联调时控制台停在错误页,提示"票据已被使用"。服务端日志显示同一张票在几毫秒内被提交两次——第一次成功签发了会话,第二次撞上唯一约束。

根因在前端:消费逻辑放在页面副作用里,副作用在依赖变化时会重跑(语言资源就绪后翻译函数换了身份,触发第二次执行)。修复两步:

  • 单飞守卫:用 ref 保证每次挂载只执行一次;
  • 存活标记与依赖解耦:把"组件是否还在"的标记从副作用清理函数里挪出来,只在真实卸载时失效——否则第一次请求的成功响应会在依赖变化时被丢弃,页面永远停在"正在登录"。

一次性凭证类的前端逻辑,天然不适合"可重跑的副作用"模型,必须显式约束执行次数。

二、页面放错了路由树

联调初期页面一直 404。排查发现:产品站有一条路由重写规则,把带语言前缀的 /auth/* 统一重写到根级 auth 目录——应用类页面都住在根级树里,新页面按"直觉"放进语言目录,永远匹配不到。

教训:在别人的框架里加页面,先找到同类页面的实际落点,再动手写文件。

三、密钥文件"读不到"却不报错

票据签发接口一度返回"未配置"。根因是密钥文件权限 0600,容器内进程以另一用户运行读不到;"读不到就返回空"的容错把错误静默吞掉,最终表现为服务端声称自己没配置。

修法:密钥权限与容器用户对齐(进部署检查单);"配置缺失"显式报 503,并由启动自检提前暴露——静默降级会把配置错误伪装成业务错误。

小结

两套账号体系的互通,不需要合并用户数据,也不该共享凭证。把边界收窄到"一张短时效、一次性的票据 + 邮箱匹配 + 各自标准登录",可审计、可回滚;把重放、路由与权限三类问题逐个排掉,这条链路才真正可以交付。

相关产品与 ERP 实践文章在宏斋博客。 宏斋云ERP · Blog

返回列表