从登记应用到拿到 userinfo 的完整链路,含每一步的实际请求样例。
在 https://auth.cx4.cn 注册账号 → 「我的应用」里登记一个应用 → 等管理员审核通过(若站内关闭了审核则立即可用)。 登记成功后 client_secret 只显示一次,关掉页面就再也看不到;忘了只能重置,重置会让旧 secret 立即失效。
三种客户端形态,选错后面全不对:
| 形态 | 你的代码能不能藏住 secret | PKCE | 用哪种 grant |
|---|---|---|---|
| 机密客户端(服务端渲染的 Web 应用) | 能 | 建议开 | code + refresh + client_credentials |
| 公开客户端(SPA / 移动端 / 桌面端) | 不能 | 强制开,关不掉 | code + refresh |
| 纯机器对机器(没有用户参与) | 能 | 不涉及 | client_credentials |
公开客户端不给 secret:浏览器里的"凭据"根本不是凭据,防授权码被拦截的唯一手段是 PKCE,
所以本站对 is_confidential=0 的应用写死 pkce_required=1。
$verifier = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
$challenge = rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '=');
只接受 code_challenge_method=S256,plain 会被拒(回跳带 error=invalid_request)。
verifier 保存在你自己的会话里,第 3 步要用;它不进 URL。
GET https://auth.cx4.cn/oauth/authorize
?response_type=code
&client_id=你的 client_id
&redirect_uri=登记过的回调地址 ← 必须逐字节相等,不比前缀、不忽略尾斜杠
&scope=profile email
&state=随机串 ← 你自己校验回来的那个 state
&code_challenge=第 1 步的 challenge
&code_challenge_method=S256
必须是顶层跳转,不能 iframe:本站会话 cookie 是 SameSite=Lax,iframe 里带不进来,
用户会明明已登录却被要求重登;而且同意页被嵌套本身就是点击劫持授权。
两种结果:
/login?redirect=<完整的 authorize 查询串>,登录后自动回到授权页。本站不发 id_token、不提供 OIDC 的 JWKS 端点,也不认
openid这个 scope (请求它会被判invalid_scope)。要用户身份请用下面的/oauth/userinfo。
回来的是 GET your://cb?code=...&state=...。先比 state,再换票:
POST https://auth.cx4.cn/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&client_id=...
&client_secret=... ← 机密客户端;也可用 HTTP Basic 认证
&code=回调里的 code
&redirect_uri=和第 2 步逐字节相同的值
&code_verifier=第 1 步的 verifier
成功返回:
{"access_token":"eyJ0eXAi...","token_type":"Bearer","expires_in":900,
"scope":"profile email","refresh_token":"..."}
⚠️ refresh_token 只有该应用开了 refresh_token 这一授权类型才会出现;没开就只回上面那三样,
意思是"到期请重新走一次 authorize",不是漏字段。
失败一律是 RFC 6749 §5.2 的 JSON + 正确状态码(不会是 HTML 异常栈):
invalid_request invalid_client invalid_grant unsupported_grant_type invalid_scope。
授权码只能兑换一次。兑换失败(比如 verifier 错)也会把它消耗掉,同一个码不给第二次机会。
GET https://auth.cx4.cn/oauth/userinfo
Authorization: Bearer 你的 access_token
只认请求头,不接受把令牌写在查询串里(URL 会进 access log 与 Referer)。
字段按 scope 给,稳定契约:
始终: sub ← auth_user.uuid,永不变的下游主键
scope=profile → nickname, avatar, created_at
scope=email → email, email_verified(0/1)
email_verified 必须给你:本站的政策是社交/授权中心身份不自动合并到你的账号,
只有邮箱已验证时你才可以用它做账号匹配;不给这个字段等于逼你自己猜邮箱可不可信。
用 sub 建你自己的映射表,不要用邮箱当主键。首次通过授权中心登录时,
由你的应用创建一条全新的用户记录,之后用户可以把它和已有账号绑定。
POST /oauth/token grant_type=refresh_token&refresh_token=...&client_id=...&client_secret=...
invalid_grant。这是设计,不是 bug —— 请按"需要用户重新授权"处理。撤销(RFC 7009,要带客户端凭据,和 token 端点同一套认证):
POST https://auth.cx4.cn/oauth/revoke
client_id=...&client_secret=...&token=... (refresh_token 或 access_token 都收)
认证不过 → 401 invalid_client;缺 token 参数 → 400 invalid_request。
在这两个前提下恒返回 200:撤销一个本站不认识的串和撤销一个已撤销的串回同样的形态,
不给探测"这个 token 是否存在"的旁路。
GET https://auth.cx4.cn/oauth/logout?client_id=...&post_logout_redirect_uri=...
只清本站会话。本站不做 SLO 广播:其他标签页里的其他应用不会因此掉线。
post_logout_redirect_uri 必须精确命中你在登记时填的登出回跳白名单,白名单为空则一律不回跳
(否则这里就是个开放重定向器);回跳地址不合法时照样清会话,不能因为应用配错让用户退不出去。
默认本站不发任何 CORS 头。需要 SPA 直接调 token 端点时,让管理员在后台把该应用的
cors_enabled 打开。即便打开,本站也永不发 Access-Control-Allow-Credentials:
带着本站会话 cookie 跨站读用户数据这条路必须堵死。
预检(OPTIONS)总能拿到 204,只是不一定带 CORS 头 —— 免得接入方把"没有路由"误判成"CORS 被拒"。
set_real_ip_from 收窄前可由请求方伪造,别把它当强判据用于风控。sub 会一直查不到人:本站无法主动通知你,只能等你下次调 /oauth/userinfo 时发现。最近更新:2026-10-10 08:04