Repository navigation
RFC: 统一多 Provider 身份体系(user_identities)—— GitHub / Discord / Google / Yahoo 登录与绑定 #42
Copy link
Copy link
Closed
Description
动机
计划接入更多登录方式(Discord、Google、Yahoo…),同时 Discord Connector 路线(网站 ↔ GitHub ↔ Discord 三方身份互认)依赖一套干净的身份模型。现状是单 provider 时代的捷径,扩展到 N 家会塌:
user_accounts.github_id BIGINT—— provider 字段长在"人"的表上,每接一家就要加列AuthService.java:79:username = "github_" + uuid—— provider 烧进用户名,/u/{identifier}、follows 都靠"纯数字=github_id"的约定撑着- 讽刺对照:库里废弃的 NextAuth
accounts表(provider, providerAccountId, userId)正是教科书形状(Auth.js/Keycloak/Auth0 同款),Sa-Token 迁移时被塌缩成了一根列
设计
两张表,职责切开。user_accounts = 人(不含任何 provider 字段);user_identities = 登录方式:
CREATE TABLE IF NOT EXISTS user_identities (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL, -- user_accounts.id(无 FK,与库内惯例一致)
provider VARCHAR(32) NOT NULL, -- 'github' | 'discord' | 'google' | ...
provider_user_id VARCHAR(255) NOT NULL, -- provider 侧稳定 ID(github 数字 id / discord snowflake / google sub)
email_at_link VARCHAR(255),
display_name_at_link VARCHAR(255),
linked_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_login_at TIMESTAMPTZ,
UNIQUE (provider, provider_user_id)
);
CREATE INDEX IF NOT EXISTS idx_user_identities_user ON user_identities (user_id);统一流程(provider 无关),callback 拿到 (provider, provider_user_id) 后:
- identities 命中 → 登入对应账号,刷新资料
- 未命中 + 当前有 Sa-Token 会话 → 绑定到当前账号(设置页"连接 Discord/Google"入口)
- 未命中 + 无会话 → 建新
user_accounts+ identity(默认 role=user)
GitHub 的特殊性退到业务层:认证层 provider 平权;贡献归属/排行榜/认领档案 join user_identities WHERE provider='github'(文档 git-based 是业务事实,不泄漏进认证设计)。
Endpoint 泛化:/oauth/render/{provider}、/api/auth/callback/{provider}(旧 github 路径保留兼容)。前端 rewrites 已是 /oauth/*、/auth/* 通配,无需变更。
接入一个新 provider 的成本(本设计落地后)
- 到 provider 开 OAuth App,配 callback
https://involutionhell.com/api/auth/callback/{provider} .env加{PROVIDER}_CLIENT_ID/SECRET- OAuthController 的 AuthRequest 工厂加一个 case(JustAuth 1.16.6 内置 Google/Microsoft 等;Discord/Yahoo 若不在内置列表,走 JustAuth 自定义 AuthSource 扩展点,约几十行,落地时以依赖源码为准)
零 schema 变更、零登录流程改动。
迁移计划
- M0:
schema.sql加表 + 回填INSERT INTO user_identities (user_id, provider, provider_user_id) SELECT id, 'github', github_id::text FROM user_accounts WHERE github_id IS NOT NULL+JdbcUserIdentityRepository - M1:
loginByGithub→loginByProvider(provider, AuthUser)走统一流程;github_id列双写保留一个版本 - M2:绑定/解绑流程(state 带 CSRF + mode)+ 前端设置页 UI;解绑约束:至少保留一种登录方式
- M3:Discord 作为第二个 provider 上线验证通用性(解锁 Connector Phase 1/2);
/u/{identifier}与 follows 的查询改走 identities;末版删github_id列
Sa-Token session 存 user_accounts.id,全程不受影响;前端 leaderboard 的 hasProfile 探测契约不变。
安全不变量(新增进 SecurityInvariantsTests)
- 任何 provider 登录/注册默认只授予
userrole(superadmin 仍只能 DB 直改,INV 系列不受影响) - 禁止邮箱静默合并——"新 provider 邮箱 == 既有账号邮箱"只做提示,合并必须在已登录会话内主动完成(未验证邮箱的 provider 是经典账户接管向量)
- 绑定操作必须来自已登录会话;state 参数含 CSRF token
- 解绑不得移除账号最后一种登录方式
- provider 的 access/refresh token 一律不落库
不做的事(YAGNI)
独立 IdP(Keycloak 等)、多邮箱、组织/团队、既有双账号合并工具(出现真实案例再议)、存量 github_<id> 用户名强迁(显示层走 display_name)。
Open Questions
- Discord/Yahoo 用 JustAuth 自定义 AuthSource 还是手写 OAuth2(各两个请求)?
- 新用户 username 生成策略:provider login 冲突时加后缀?
- 旧
github_前缀用户名何时正式退役?
🤖 Generated with Claude Code
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
longsizhuo commentedon Jul 18, 2026
Review 修订(对抗性 review 结论 + 库选型)
🔴 开工前必须并入设计
page.tsx明确注释 SSR 拿不到会话)。绑定流程改为:callback 完成后由前端带 localStorage token 调POST /api/auth/link/confirm落地绑定;禁止把 loginId 塞进 state 让 callback 直接信任。ON CONFLICT (provider, provider_user_id) DO NOTHING:schema.sql 每次启动执行,裸 INSERT 第二次启动即撞 UNIQUE 崩溃;顺带自动补齐 M1 双写漂移。UNIQUE (user_id, provider):同账号绑两个同 provider 身份会让/u/{githubId}canonical 和贡献归属二义。真需求出现再 DROP。github_前缀那层意外的并发兜底,并发 callback 会建出无 identity 孤儿账号;identity 撞 UNIQUE 时回滚并回查-登入。🟡 一并修订
'!',schema 里 discord-bridge 已有先例;配hasUsablePassword()),否则 OAuth 用户解绑唯一 identity 会永久锁死。REFERENCES user_accounts(id) ON DELETE CASCADE(user_follows/events/shared_links 均有 FK)。findByIdentifier的"纯数字=github_id"路由,本人主页永久不可达。settings?linked=github/?link_error=identity_taken,"identity 已绑他人"是必然出现的用户可见错误,不能混进 oauth_failed。新增安全不变量(进 SecurityInvariantsTests)
库选型:留 JustAuth,不回 Auth.js/NextAuth
评估过"是否回退 NextAuth":NextAuth=Auth.js 是前端库,去年已迁走(Prisma 里 dead 的 users/accounts/sessions 是其残留)。搬回等于把认证边界从后端 Sa-Token 移回前端——要么重写全部
@SaCheckRole/satoken/X-Internal-Key 网关(巨大),要么它只做社交登录、身份仍交后端建号存 identity(RFC 的活一样不少,还多一个 Node 服务进认证链路)。JustAuth 1.16.6 已是 Java 版 Auth.js:provider 目录 + OAuth 协议(token 交换/PKCE/各家怪癖)都覆盖,我们不手写 OAuth2。手写的只有 ① provider→user 映射(user_identities 业务逻辑,任何库都替不了)② 可能一个 Discord 自定义 AuthSource(~30 行,社区有现成)。唯一 Auth.js 占优的 state/CSRF-免费,正好由上面 🔴1/🔴2 显式补齐。
Open Questions 定稿
github_<id>用户名永不强退(UNIQUE 占位无害,显示层走 display_name);该题从 RFC 删除。feat(usercenter): M1 loginByProvider 统一流程 + OAuth state/cookie 硬化
feat(usercenter): M1 loginByProvider 统一流程 + OAuth state/cookie 硬化 (#44)
feat(usercenter): M1 loginByProvider + M2a 解绑/列表(合并进 main) (#47)
feat(auth): Discord 登录后端 — 自定义 AuthSource + OAuth 端点泛化 (#48)