INTRO
什么是 OAuth 2.0
OAuth 2.0 可以理解为一种“授权第三方应用访问部分信息”的方式。它的重点不是把你的账号密码交给第三方 App,而是让你先在本站完成登录和确认,再决定是否允许对方读取你同意开放的信息。
在本网站的实际应用过程中,`第三方 App` 指的是接入本网站账号体系的外部应用、网站或程序;`授权页面` 则是本站展示给你的确认页面,用来让你决定是否同意授权,以及允许它访问哪些内容。
`scope` 可以理解为“权限范围”。它的作用是告诉本站,这个第三方 App 想读取哪些信息;只有你同意、并且本站允许开放的范围,才会真的授权出去,这样可以避免一旦登录就把所有信息都交给对方。
`access_token` 可以理解为授权成功后发给第三方 App 的访问凭证。它的作用是让 App 在后续请求接口时证明“这位用户已经同意授权了”,因此它不等于你的账号密码,而是一份带有限制条件的授权结果。
当你确认授权后,本站会先返回一个一次性的 `authorization code` 给第三方 App。这个临时码不能直接长期使用,它还需要配合 `PKCE` 这个安全机制一起完成校验,避免授权码在中途被别人拿去冒用。这里的 `redirect_uri` 则是授权完成后浏览器要跳回去的回调地址。
在后续结果里,你还会看到 `id_token` 和 `nonce` 这类名词。`id_token` 主要用于告诉第三方 App 当前授权对应的是哪个登录身份;`nonce` 可以理解为这次登录请求附带的另一串随机标记。第三方 App 发起请求时先保存它,本站再把它写回 `id_token`,这样第三方 App 就能检查拿到的身份结果是不是确实对应当前这一次登录授权流程。
补充阅读: 《理解 OAuth 2.0》
overview
这份授权是怎么工作的
本站当前使用授权码模式完成登录授权,并用 PKCE 保护授权过程。
当前能力
- 授权模式固定为 Authorization Code,response_type 只能使用 code。
- PKCE 为必需项,code_challenge_method 当前仅支持 S256。
- 令牌交换成功后返回 access_token、id_token、expires_in 和 scope。
- 当前没有 refresh_token;access_token 过期后需要重新发起授权流程。
核心端点
/oauth/authorizeGET 必填 浏览器跳转入口。用户会在这里完成登录、授权确认,并最终被重定向到注册过的 redirect_uri。
/oauth/tokenPOST 必填 授权码交换端点。客户端提交 authorization code 与 code_verifier 后在这里换取 access_token 和 id_token。
/oauth/userinfoGET 必填 用户信息端点。应用拿到 access_token 后可在这里读取 OIDC sub,若 scope 允许还会返回 preferred_username。
client
先创建 OAuth 客户端
先登录站内账号,然后点击右上角用户名进入设置页,选择开发选项卡,切换面板到 OAuth 页,填写信息创建 OAuth 客户端。
创建时要准备的信息
namestring 必填 客户端显示名称,会出现在授权确认页。
redirectUrisstring[] 必填 允许回调的地址列表。授权请求里的 redirect_uri 必须与这里的某一项完全一致。
requestedScopesstring[] 必填 客户端希望申请的 scope 列表。只有审核通过的 scope 才能在授权请求中使用。
descriptionstring | null可选描述,便于用户理解该应用用途。
homepageUrlstring | null可选主页地址,用于开发者识别和后续管理。
客户端创建成功后,普通用户需要等待管理员审核 scope;客户端创建者可在自己客户端的 scope 仍为待审核时继续完成 OAuth 登录与授权测试。
authorize
发起授权请求
第三方应用应在浏览器里发起授权。
授权请求参数
response_typestring 必填 固定填写 code。
client_idstring 必填 创建客户端后分配的 client_id。
redirect_uristring 必填 授权完成后的回调地址,必须与注册列表里的某一项完全匹配。
scopespace-delimited string 必填 空格分隔的 scope 列表。
statestring 必填 建议填写一个随机字符串。发起授权时先把它保存起来,等回调回来后再对比返回值是否一致,用来确认这次回调确实是你自己刚才发起的那次请求。
code_challengestring 必填 由 code_verifier 经过 SHA-256 和 Base64URL 计算得到的 PKCE challenge。
code_challenge_methodstring 必填 固定填写 S256。当前实现不接受 plain。
noncestring可选。建议填写一个随机字符串并在本地保存;如果这次登录返回了 id_token,再检查里面带回来的 nonce 是否和你最初保存的一致,用来确认这份身份结果确实对应当前这次请求。
授权 URL 示例
/oauth/authorize
?response_type=code
&client_id=client_123
&redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback
&scope=api.auth.me.read
&state=9f4f1c8c4d5a4f43
&code_challenge=7w7Tt39Qn1m6M5Lx9c1xQ1kBrmR8J4t9wL4mUt5M2pU
&code_challenge_method=S256
&nonce=b2d0f7a1c81e41d7token
处理回调并交换令牌
应用在回调地址拿到 code 后,应先检查返回的 state 是否和发起授权前保存的一致,再用同一个 redirect_uri 和原始 PKCE verifier 换取令牌。
回调处理要点
- 先检查回调里是否带有 error;常见拒绝值为 access_denied。
- 把回调里的 state 和你发起授权前保存的随机值逐字对比;只有完全一致,才能继续处理这次登录结果。
- 确保 token 交换时使用的 redirect_uri 与授权阶段完全一致。
- code 是一次性的,过期或已消费后再次提交会收到 invalid_grant。
成功回调 URL 示例
https://app.example.com/oauth/callback
?code=SplxlOBeZQQYbYS6WxSbIA
&state=9f4f1c8c4d5a4f43失败回调 URL 示例
https://app.example.com/oauth/callback
?error=access_denied
&state=9f4f1c8c4d5a4f43POST /oauth/token 请求体
当前服务端会从请求体读取这些字段。建议使用 application/x-www-form-urlencoded 提交,这是 OAuth 2.0 中 token 交换最常见的提交方式。
grant_typestring 必填 固定填写 authorization_code。
codestring 必填 回调参数中的 authorization code。
client_idstring 必填 与你的 OAuth 客户端对应的 client_id。
redirect_uristring 必填 必须与授权阶段提交的 redirect_uri 完全一致。
code_verifierstring 必填 这是第三方 App 在发起授权前随机生成并保存在本地的一串字符串。服务端会用它与之前提交的 code_challenge 做校验。
cURL 交换令牌示例
curl -X POST /oauth/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'client_id=client_123' \
--data-urlencode 'code=<回调中的 code>' \
--data-urlencode 'redirect_uri=https://app.example.com/oauth/callback' \
--data-urlencode 'code_verifier=<浏览器里保存的 code_verifier>'成功响应示例
{
"access_token": "ocrh_u_xxxxxxxxxxxxxxxxxxxx",
"token_type": "Bearer",
"expires_in": 7200,
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Im9jcmgta2V5LTEifQ...",
"scope": "api.auth.me.read"
}userinfo
使用 access_token
拿到 access_token 后,第三方应用可以按 Bearer Token 访问当前用户相关接口。
请求 userinfo
curl /oauth/userinfo \
-H 'Authorization: Bearer <access_token>'userinfo 至少会返回 sub。只有当授权 scope 中包含 api.auth.me.read 时,服务端才会额外返回 preferred_username。id_token 里的身份信息也遵循相同约束,因此不要假设用户名字段一定存在。
userinfo 响应示例
{
"sub": "dGhpcy1pcy1hLXN0YWJsZS1vaWRjLXN1YmplY3Q"
}
{
"sub": "dGhpcy1pcy1hLXN0YWJsZS1vaWRjLXN1YmplY3Q",
"preferred_username": "demo-user"
}id_token 说明
- id_token 由服务端使用 RS256 签名,包含 iss、sub、aud、azp、exp、iat、auth_time 和 at_hash。
- 如果授权请求里提供了 nonce,服务端会把它写回 id_token;第三方 App 收到后应把它和自己发起请求前保存的值做对比,确认这份身份结果确实属于当前这一次登录。
- preferred_username 只有在 scope 允许读取当前身份信息时才会出现。
troubleshooting
限制与排障
多数接入失败都来自 scope 审核、redirect_uri 不匹配或 PKCE 参数错误。
当前限制
- 仅支持 Authorization Code,不支持 implicit、device code 或 client credentials。
- 仅支持 PKCE S256,不支持 plain。
- 当前没有 refresh_token,过期后需要重新授权。
- 请求里的所有 scope 必须全部审核通过,否则授权阶段直接失败。
常见失败原因
- invalid_request:缺失必填字段、response_type 不是 code,或 code_challenge_method 不是 S256。
- access_denied:用户拒绝授权,或者当前登录用户本身没有被请求的 scope。
- invalid_grant:authorization code 已过期、已被消费、redirect_uri 不一致,或 code_verifier 校验失败。
- 回调后直接失败:通常是 state 对比不一致,说明这次回调不是当前这次授权请求的结果,或者浏览器本地保存的值已经丢失。
安全建议
- 始终生成高熵 state 和 code_verifier,并把它们只保存在短期会话存储中。
- 只通过 HTTPS 传输 access_token,避免把令牌写进 URL、日志或前端埋点。
- 公共客户端不要尝试长期缓存 access_token;过期后直接重新走授权码流程。
- 如果你的应用只需要识别登录用户身份,优先申请最小 scope,避免过度请求接口权限。