统一登录中心(Hub)接入指南:从创建应用到跑通 OAuth#
本文面向“接入方 SaaS”(app1.com/app2.com…),目标是在**本项目(Hub)**中完成:
- 创建 SaaS Application(Hub 业务概念)
- 创建 OAuth Client(Better-Auth OIDC Provider 依赖的
oauth_application) - 走通 OAuth 2.1 授权码 + PKCE 的完整流程:
authorize → consent/login → callback(code) → token
如果你需要更底层的实现解释,请同时参考:
docs/oauth-implementation-guide.mddocs/oauth-flow-testing-guide.md
0. 关键概念(先对齐名词)#
- Hub:本项目,统一登录中心 + 订阅/权益中心。
- SaaS App:接入方产品(你的业务站点),作为 OAuth Client。
- Application(Hub 业务):Hub 内部“应用注册”概念(表
apps),用于权限/来源/计费等归属。 - OAuth Client(OAuth 协议):OAuth 2.1 客户端(表
oauth_application,Better-Auth OIDC Provider 的事实源)。 - 授权端点:
/api/auth/oauth2/authorize - Token 端点:
/api/auth/oauth2/token - Consent 页面:
/auth/consent(展示客户端信息、scope 等) - Login 页面:
/auth/login
1. 本地启动(最小闭环)#
1.1 安装依赖#
npm i
1.2 运行本地数据库迁移(D1 local)#
wrangler d1 migrations apply windchat-members-db --local
1.3 启动 Hub + 两个 demo SaaS(推荐)#
./scripts/start-oauth-demo.sh --migrate
启动后:
- Hub:
http://localhost:3000 - demo SaaS BFF:
http://localhost:4001 - demo SaaS SPA:
http://localhost:4002
2. 创建 Application(Hub 业务应用)#
这一步的目的是:在 Hub 的
apps表里注册一个 SaaS 应用(用于归属、审计、计费等)。
2.1 方式 A:直接写入 D1(本地)#
wrangler d1 execute windchat-members-db --local --command "
INSERT INTO apps (id, name, status, created_at)
VALUES ('my_saas_app', 'My SaaS App', 'active', unixepoch() * 1000);
"
2.2 方式 B:使用 Admin API(需要管理员会话)#
项目内提供了 /api/admin/apps(POST/PUT/DELETE),但需要满足 requireApiAdmin:
- 先用管理员账号登录(生成
better-auth.session_tokencookie) - 再携带该 cookie 调用接口
可参考 src/lib/api-guards.ts 与 src/routes/api/admin/apps.ts 的实现。
3. 创建 OAuth Client(Better-Auth:oauth_application)#
Better-Auth 的 OIDC Provider 会从数据库表 oauth_application 读取客户端信息(见 src/lib/auth.ts 注释)。
3.1 写入一条 OAuth Client 记录(本地)#
下面示例创建一个 public client(推荐给 SPA),并把 app_id 写进 metadata 方便归属:
wrangler d1 execute windchat-members-db --local --command "
INSERT INTO oauth_application (
id, client_id, client_secret, type, name, icon, metadata,
disabled, redirect_urls, user_id, created_at, updated_at
) VALUES (
'my_client_id',
'my_client_id',
NULL,
'public',
'My SaaS OAuth Client',
NULL,
'{\\\"app_id\\\":\\\"my_saas_app\\\"}',
0,
'[\\\"http://localhost:4002/callback.html\\\"]',
NULL,
unixepoch() * 1000,
unixepoch() * 1000
);
"
字段说明(关键的几项):
client_id:OAuth 的 client_id(唯一)type:public/confidentialredirect_urls:JSON 数组字符串(回调白名单)disabled:禁用开关(1=禁用)
你也可以直接复用项目提供的测试数据:
wrangler d1 execute windchat-members-db --local --file scripts/seed-oauth-test-data.sql
4. 走通 OAuth 2.1(Authorization Code + PKCE)#
4.1 生成 PKCE(SaaS 侧)#
SaaS 需要生成:
code_verifier:随机字符串(43~128 chars)code_challenge:BASE64URL(SHA256(code_verifier))code_challenge_method=S256
4.2 拼出 authorize URL(跳转到 Hub)#
示例(把参数替换成你的值):
http://localhost:3000/api/auth/oauth2/authorize
?response_type=code
&client_id=my_client_id
&redirect_uri=http%3A%2F%2Flocalhost%3A4002%2Fcallback.html
&scope=openid%20profile%20email
&state=YOUR_STATE
&code_challenge=YOUR_CODE_CHALLENGE
&code_challenge_method=S256
浏览器访问后流程为:
- 未登录 → 跳转
/auth/login - 登录成功 → 进入
/auth/consent - 同意授权 → 重定向回
redirect_uri?code=...&state=...
4.3 用 code 换 token(SaaS 后端/BFF 执行)#
向 token 端点发起请求:
curl -X POST http://localhost:3000/api/auth/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "client_id=my_client_id" \
--data-urlencode "redirect_uri=http://localhost:4002/callback.html" \
--data-urlencode "code=YOUR_CODE_FROM_CALLBACK" \
--data-urlencode "code_verifier=YOUR_CODE_VERIFIER"
拿到 access_token(以及可能的 refresh_token)后,SaaS 在自己的 BFF 内保存并用于调用 Hub 的受保护 API(或换取业务 token,按你的接入模式设计)。
5. 常见问题(排障清单)#
- redirect_uri 不匹配:确认
oauth_application.redirect_urls中包含完全一致的回调地址(协议/host/path 都要一致)。 - PKCE 错误:
code_challenge必须是S256;code_verifier要与生成 challenge 的那一条一致。 - CORS 问题:
/api/auth/oauth2/*的 CORS 白名单由OAUTH_DEMO_TRUSTED_ORIGINS控制(见src/routes/api/auth/$.ts)。 - 客户端信息拉不到(consent 页展示):
/api/oauth/client/:clientId需要用户已登录,且客户端存在于oauth_application表。
6. 下一步(生产接入建议)#
- 只开放
authorization_code + PKCE,SPA 不要持有长期 token;推荐 BFF 模式。 - 把 SaaS 的
app_id与 OAuth client 的client_id做一对一映射(可写入oauth_application.metadata或后续专门的映射表)。 - 对接完成后,把本地 demo origin(4001/4002)替换为实际域名并加入 trusted origins。