动机
计划接入更多登录方式(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 = 登录方式:
CREATETABLEIF NOT EXISTS user_identities (
id BIGSERIALPRIMARY KEY,
user_id BIGINTNOT 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 TIMESTAMPTZNOT NULL DEFAULT now(),
last_login_at TIMESTAMPTZ,
UNIQUE (provider, provider_user_id)
);
CREATEINDEXIF 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 登录/注册默认只授予
user role(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
https://claude.ai/code/session_01RtSNVtrEkCsFVHrbt4Zjgi
动机
计划接入更多登录方式(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"的约定撑着accounts表(provider, providerAccountId, userId)正是教科书形状(Auth.js/Keycloak/Auth0 同款),Sa-Token 迁移时被塌缩成了一根列设计
两张表,职责切开。
user_accounts= 人(不含任何 provider 字段);user_identities= 登录方式:统一流程(provider 无关),callback 拿到
(provider, provider_user_id)后: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 的成本(本设计落地后)
https://involutionhell.com/api/auth/callback/{provider}.env加{PROVIDER}_CLIENT_ID/SECRET零 schema 变更、零登录流程改动。
迁移计划
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+JdbcUserIdentityRepositoryloginByGithub→loginByProvider(provider, AuthUser)走统一流程;github_id列双写保留一个版本/u/{identifier}与 follows 的查询改走 identities;末版删github_id列Sa-Token session 存
user_accounts.id,全程不受影响;前端 leaderboard 的 hasProfile 探测契约不变。安全不变量(新增进 SecurityInvariantsTests)
userrole(superadmin 仍只能 DB 直改,INV 系列不受影响)不做的事(YAGNI)
独立 IdP(Keycloak 等)、多邮箱、组织/团队、既有双账号合并工具(出现真实案例再议)、存量
github_<id>用户名强迁(显示层走 display_name)。Open Questions
github_前缀用户名何时正式退役?🤖 Generated with Claude Code
https://claude.ai/code/session_01RtSNVtrEkCsFVHrbt4Zjgi