Skip to content

RFC: 统一多 Provider 身份体系(user_identities)—— GitHub / Discord / Google / Yahoo 登录与绑定 #42

Description

@longsizhuo

动机

计划接入更多登录方式(Discord、Google、Yahoo…),同时 Discord Connector 路线(网站 ↔ GitHub ↔ Discord 三方身份互认)依赖一套干净的身份模型。现状是单 provider 时代的捷径,扩展到 N 家会塌:

  • user_accounts.github_id BIGINT —— provider 字段长在"人"的表上,每接一家就要加列
  • AuthService.java:79username = "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) 后:

  1. identities 命中 → 登入对应账号,刷新资料
  2. 未命中 + 当前有 Sa-Token 会话 → 绑定到当前账号(设置页"连接 Discord/Google"入口)
  3. 未命中 + 无会话 → 建新 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 的成本(本设计落地后)

  1. 到 provider 开 OAuth App,配 callback https://involutionhell.com/api/auth/callback/{provider}
  2. .env{PROVIDER}_CLIENT_ID/SECRET
  3. OAuthController 的 AuthRequest 工厂加一个 case(JustAuth 1.16.6 内置 Google/Microsoft 等;Discord/Yahoo 若不在内置列表,走 JustAuth 自定义 AuthSource 扩展点,约几十行,落地时以依赖源码为准)

零 schema 变更、零登录流程改动。

迁移计划

  • M0schema.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
  • M1loginByGithubloginByProvider(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

  1. Discord/Yahoo 用 JustAuth 自定义 AuthSource 还是手写 OAuth2(各两个请求)?
  2. 新用户 username 生成策略:provider login 冲突时加后缀?
  3. github_ 前缀用户名何时正式退役?

🤖 Generated with Claude Code

https://claude.ai/code/session_01RtSNVtrEkCsFVHrbt4Zjgi

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions