Appearance
统一账号接入规范
类型:
INVARIANT(必须遵守) · 适用范围:MintPop 品牌下所有产品MintPop 各产品共用一套账号体系,统一由 MintPop 账号中心(基于 Logto,标准 OIDC / OAuth 2.1) 提供登录与身份。新产品一律作为 OIDC 应用接入,不自建账号体系。 本页回答「一个新产品该怎么接、什么归账号中心、什么归自己、token 谁签、跨产品数据怎么互通」。
敏感值不在本公开站
client_secret、内部管理端点、租户管理凭据、真实 issuer / client_id 等敏感信息不在这里。本页只写「对外可讲的接入约定与架构」。具体配置向账号中心管理员申请。
一句话心智模型
统一的是「身份」,不是「所有 token」,也不是「用户主键」。 账号中心是中心授权方,负责「用户是谁」——认证 / 授权、登录凭证(邮箱 / 密码 / MFA)、全组织唯一的
sub;它不托管用户个人资料(昵称、头像、性别、生日……一概不存、不管)。每个产品负责「这个用户在本产品里是谁、有什么」,并以自己的内部userid作为用户主键——sub不下沉成业务主键。
配套的边界原则:sub 只在「账号中心签发的凭证进入本产品」的边界上被解析成 userid;产品内部(会话 token、业务表、日志)一律流通 userid,不流通 sub。
一、四条架构主张(组织级,不可协商)
- 只能用统一登录。 产品不自建注册 / 登录 / 找回密码 / 验证码体系,登录入口一律走账号中心 OIDC。
- 各产品维护自己的用户表,主键用自己的
userid。 存业务档案(等级、偏好、产品内状态、性别 / 生日等产品属性),并持一张「sub↔userid」映射表把本地用户锚到统一身份。业务表一律以userid为外键,不用sub当主键(见第七节)。 - 登录信息只能在账号中心改,业务系统只读。 邮箱、密码、手机号、MFA 的唯一可写入口是账号中心;产品侧对这些字段一律只读,不提供任何修改入口。
- 互联互通以
sub关联,但以账号中心签发的 JWT 授权。 跨产品访问用户数据时,sub标识「是谁」,JWT 证明「凭什么问」;接收方从验签后的 token 取sub、再映射到自己的userid(见第六节)。
这四条是「账号中台」的标准形态(对标 Google / Apple 的多产品账号体系)。
二、接入模型
- 协议:OpenID Connect(Authorization Code + PKCE)。
- 身份来源唯一:用户身份、邮箱等以账号中心为准,产品侧只存
sub与业务数据的关联,不重复维护密码。 - 应用类型:
- 前端 SPA / 桌面端 / CLI → 公开客户端(Public,PKCE / Device Flow,无 secret)。
- 有后端的 Web / 服务端 → 机密客户端(Confidential,带 secret)。
三、token 谁签:会话 token 与跨产品 token 分开
这是最容易搞混的一刀,必须切开。它们是两个独立的东西,由不同的人签:
| token 类型 | 谁签发 | 载荷里的用户标识 | 用途 | 谁能验 |
|---|---|---|---|---|
| 会话 token(浏览器 ↔ 本产品的日常登录态) | 产品自己 | 本产品 userid(不放 sub) | 自家 web 登录态 | 仅本产品 |
| 跨产品 token(A 调 B 的用户数据) | 账号中心(带 resource / aud / scope) | sub(接收方再映射成自己的 userid) | 产品间互调 | 全组织任意产品(JWKS 公钥) |
API 凭证(如 sk- 形态的 API Key) | 产品自己 | 本产品 userid | 与用户身份无关的机器调用 | 仅本产品 |
sub 只在签发 / 接收凭证的边界上出现:产品自签会话 token 时,已在登录环节把 sub 换成了 userid,故自签 token 内只有 userid;账号中心签的跨产品 token 里必然是 sub(账号中心不知道各产品的 userid),由接收方在入口再解析一次。
会话 token 为什么各产品自签(web 应用默认)
- 会话控制权是签发方特权:改密码即时踢下线、封号即时生效、按产品定制 TTL——只有自己签的 token 才能做(例如维护一个会话版本号,改密码即令旧 token 失效)。用账号中心 token 当会话就丢了这个能力。
- 安全(BFF 模式):现代浏览器应用最佳实践是不把 IdP 的 access token 暴露到浏览器,后端在 OIDC 握手后换成自己的会话。直接拿账号中心 token 当会话 = 把 IdP token 塞进前端,是被劝退的方向。
- 解耦:账号中心短暂抖动时,已登录用户的会话不受影响(token 是产品自己的),只有「新登录」这一下需要账号中心在线。
代价:每个产品各自实现一遍会话 token 的签发 / 刷新 / 吊销。用现成库(各语言都有)摊得很薄,换来的会话控制与安全边界是长期价值——这点代价值得付。
跨产品 token 为什么必须账号中心签
只有账号中心的签名是全组织公认的:接收方拿 jwks_uri 公钥就能验,不需要认识调用方。产品自签的 token 出了本产品谁都不认,跨产品无从谈起。这是唯一「非账号中心签不可」的场景。
客户端形态差异
- Web 应用:会话 token 产品自签(BFF)。
- 桌面端 / CLI / 移动原生:公开客户端,走 PKCE / Device Flow,直接持有账号中心 token 存于安全存储是标准做法(不存在「浏览器暴露」问题)。
- 纯前端 / 静态站:无自有后端、无用户状态时可直接用账号中心 token 调各产品 API;一旦有了需要保护的自有后端,回到 BFF 自签会话。
四、sub 是锚点:数据分层与单一写入方
数据分三层,各有各的归属与更新策略
| 层 | 例子 | 真相源 | 产品侧策略 |
|---|---|---|---|
| 身份锚点 | sub ↔ userid 映射 | 账号中心给 sub,产品给 userid | 存一张映射表,认人靠它把 sub 解析成本地 userid;产品内部一律用 userid |
| 身份凭证 | 邮箱、密码、MFA | 账号中心 | 只读副本,登录时用账号中心最新值刷新;产品内无修改入口 |
| 业务级资料(含全部个人资料) | 昵称、头像、性别、生日、余额、订单、API Key、等级、通知偏好 | 产品自己 | 以 userid 为外键,产品自由读写,与账号中心无关 |
组织决策(已定,非产品自选):账号中心只做「中心授权 + 身份凭证」,不托管用户个人资料。 集中到账号中心的字段只有邮箱、密码、MFA(且必须集中、无例外);昵称、头像及其余一切个人信息(性别、生日、偏好……)一律由各产品自己的业务系统托管。一句话:登录凭证(邮箱 / 密码 / MFA)集中到账号中心,其余个人资料全部本地。
同步时机:
sub↔userid映射不存在(首次登录=注册)时,可用 ID Token 里的资料(如昵称、头像)作为业务档案的初始种子建号;此后该用户的个人资料完全归业务系统托管。只有邮箱(及密码 / MFA 这类登录凭证)在每次登录都用账号中心最新已验证值刷新本地副本;其余个人资料登录时一律不回写、不覆盖。一句话:邮箱每次登录跟随账号中心,其余个人资料注册后由业务系统说了算。
单一写入方(single writer)
主张 3 的本质是 single source of truth + single writer:同一个身份字段,全组织只有一个可写入口(账号中心),其余全是只读副本。同一字段两边都能写 = 设计错误,发现即收口到账号中心。
用户改邮箱 → 去账号中心改 → 各产品下次登录自动跟随。这不是「冻结邮箱」,是「改,但只在一个地方改」——对 C 端产品是必答题(用户联系方式必然随时间流失)。
五、硬约束:业务系统认 sub,不认 email
这是「登录信息只在账号中心改」能成立的技术前提,也是最容易写错、后果最严重的一处。
登录同步逻辑必须是:sub 一致即同一个人,本地 email 只是可被账号中心覆盖的下游副本。 用户在账号中心改了邮箱后再登录,产品应当认 sub、用 ID Token 的最新已验证 email 刷新本地副本,而不是拿「本地旧 email ≠ token 新 email」当冲突把人拒之门外。
- 新产品:天然如此——本来就该用
sub找人、email 当展示副本。 - 存量产品:若曾按 email 匹配用户、在 email 不一致时拒绝登录,则违反此约束。要支持用户改邮箱,把该分支从「拒绝」改为「以
sub为准、刷新本地副本」即可(改动很小)。改动前的过渡期,可在账号中心暂时关闭邮箱修改回避该场景。
六、跨产品互通:sub 是标识,不是凭证
主张 4 的安全底线。绝不允许「A 拿着某个 sub 就能去 B 查该用户数据」——sub 不是秘密(它出现在 token、日志、URL 里),谁拿到谁能冒充,这是标准的越权漏洞(IDOR)形状。
正确形态二选一:
- 用户级互通(首选):请求携带账号中心签发的 access token(授权时带
resource=<B 的 API>)。B 从验签后的 token 里取sub,而不是从请求参数里信任sub;用aud/scope限定权限范围。 - 服务级互通:A 用 M2M 凭证(
client_credentials)调 B 的内部接口,sub作为参数传。此时 B 信任的是「A 这个服务」而非用户授权,要求内网隔离 + 接口白名单 + 只读,仅适合可信服务间的场景。
一句话:sub 负责「说的是谁」,token 负责「凭什么问」,两者缺一不可。
被访问方(资源服务器)要做的事
想暴露用户数据给兄弟产品的产品,加一个标准 JWT 验签中间件即可:
- 在账号中心把本产品 API 注册成一个 API Resource(如
https://api.<product>.mintpop.ai),定义 scope(如read:profile、read:usage); - 中间件用
jwks_uri公钥验签 → 校验iss/aud/exp/scope; - 取
sub→ 用映射表解析成本地userid→ 复用本产品既有的「按userid查业务表」逻辑。
这里的 sub → userid 映射,和「自家登录」用的是同一张映射表、同一个动作——只是凭证来源不同(自家登录来自 ID Token,跨产品调用来自 access token)。无论从哪个入口进来,跨过中间件后业务层看到的都只有 userid。既有的自签会话体系原样保留,两种凭证并行、业务 handler 不用改。
七、用户主键:一律用内部 userid
硬规则:每个产品的业务表主键一律用自己的内部 userid(自增或 UUID),另建一张「sub ↔ userid」唯一映射表把本地用户锚到统一身份。禁止用 sub 直接当业务主键。 sub 只是账号中心侧的外部身份标识,进了产品就要在边界换成 userid。
理由:
- 防 IdP 锁定:换 IdP(或自建)时,所有用户的
sub会变。业务表用userid,换 IdP 只需改映射表一列;用sub当主键则等于全量迁移每一张业务表。这层「防迁移的间接层」正是把用户从自建体系无感迁进统一账号中心的关键。 - 全组织一个模型:新老产品都是「内部
userid+sub映射表」,彻底同构,不再有「特例」。 sub不下沉到业务层:sub会出现在日志、URL、跨产品 token 里,不是秘密。不当业务主键,就少一整类越权(IDOR)面。- 成本几乎为零:
sub→userid映射只在凭证入口发生(登录、接收跨产品调用各一次),换算完 token 里就是userid,后续每个请求零额外开销。
sub 的作用边界
sub 只在下面两个「账号中心签发的凭证进入本产品」的边界上出现,各解析一次成 userid:
| 入口 | 凭证来源 | 动作 |
|---|---|---|
| 自家登录 | 账号中心 ID Token | 验 ID Token → 取 sub → 映射 / 建号得 userid → 自签会话 token(内含 userid) |
| 接收跨产品调用 | 账号中心 access token | 验签 → 取 sub → 映射得 userid → 走业务逻辑 |
两个入口共用同一张映射表、同一个解析动作。跨过入口后,会话 token、业务表、context、日志里只有 userid,不再出现 sub。
八、接入步骤(新产品照做)
- 申请一个 OIDC 应用:向账号中心管理员提供「产品名 + 应用类型 + 回调地址」,领取
client_id(机密客户端另发client_secret)。 - 配置回调地址:遵循下方回调地址约定。
- 走标准 Authorization Code + PKCE 流程登录,用返回的
id_token/access_token换取本地会话。 - 校验令牌:用账号中心的 JWKS 公钥验签,校验
iss/aud/exp。 - 建立会话:web 应用在 OIDC 握手后自签会话 token(内放
userid,不放sub),不把账号中心 token 暴露到浏览器;原生 / CLI 直接持有账号中心 token。 - 登出:接入 OIDC 的 end-session 端点做单点登出。
九、回调地址约定(INVARIANT)
统一按产品域名组织,便于账号中心侧登记与排查:
https://<product-domain>/auth/callback # 登录回调
https://<product-domain>/auth/logout/callback # 登出回调本地开发允许 http://localhost:<port>/auth/callback。
十、Scope 与 Claims(INVARIANT)
- 默认申请的 scope:
openid profile email(按需再加offline_access拿 refresh token)。 - 产品侧统一读取的标准 claims:
sub(用户唯一 ID,映射到本地userid)、email、name、picture。 - 不要把
email当唯一主键——邮箱可变,唯一主键永远用本地userid,认人永远用sub。
十一、令牌处理(INVARIANT)
access_token只走Authorization: Bearer <token>头,禁止放 URL query。- 有后端的产品:账号中心 token 存服务端、只给浏览器下发自签会话(HttpOnly Cookie),不落 localStorage。
- 校验必须验签 + 校验
aud指向本产品的client_id,防止 token 混用。
十二、账号注销(级联)
C 端产品受个保法 / GDPR 约束必须支持注销,而注销在本架构里是跨系统级联动作:账号中心删身份 + 各产品删除 / 匿名化业务数据。
- 用账号中心的 webhook(
User.Deleted事件)通知各产品清理本地数据; - 在第二个产品上线前就设计好这条链路——事后补最痛(漏一个产品就是一处合规缺口)。
十三、存量产品:渐进靠拢
存量产品若已经是「自签会话 token + 内部 userid 主键 + sub 映射表」,就恰好是本规范要求的形态——不是妥协,而是范本,新产品照此结构写即可。反过来说,「全量换成账号中心签发 token / 用 sub 当主键」不但代价大(重写鉴权 + 迁移所有业务表),而且违反本规范。
差别只在两处,且都可增量补齐、每步可回退:
附一、常见接入方式对照
| 产品形态 | 应用类型 | 推荐库 |
|---|---|---|
| Vue / React SPA | Public + PKCE | @logto/browser / 各框架 SDK |
| 有后端的 Web | Confidential + BFF 自签会话 | 后端 OIDC 中间件 |
| 桌面端(Tauri / Electron)/ CLI | Public + PKCE + 系统浏览器 | 系统浏览器 + loopback 回调 |
具体 SDK 用法与
issuer等参数,接入时找账号中心索取配置片段;本页只约束「必须怎么接」,不放密钥。
附二、术语
- IdP:身份提供方,这里指 MintPop 账号中心(基于 Logto)。
sub:OIDC 令牌里的 subject,账号中心分配的全组织唯一用户标识。是外部身份标识,只在凭证入口出现,不当业务主键。userid:产品内部的用户主键(自增 / UUID),业务表以它为外键。经「sub↔userid」映射表与统一身份关联;产品内部只流通它。- BFF(Backend for Frontend):前端的后端在服务端持有 IdP token、只给浏览器下发自有会话,避免 IdP token 暴露到前端。
- API Resource:账号中心里对「一个受保护 API」的注册,对应 OAuth 的
resource/aud,是跨产品授权的基础。 - 单一写入方(single writer):一个数据字段全组织只有一个可写入口,其余为只读副本。