授权

OAuth 文档

通过 OAuth 2.0 授权第三方应用访问本站信息,从创建客户端到换取令牌,一步步带你走完。

介绍

什么是 OAuth 2.0

OAuth 2.0 可以理解为一种“授权第三方应用访问部分信息”的方式。它的重点不是把你的账号密码交给第三方应用,而是让你先在本站完成登录和确认,再决定是否允许对方读取你同意开放的信息。

在本网站的实际应用过程中,第三方应用指的是接入本网站账号体系的外部应用、网站或程序;授权页面则是本站展示给你的确认页面,用来让你决定是否同意授权,以及允许它访问哪些内容。

scope 可以理解为“权限范围”。它的作用是告诉本站,这个第三方应用想读取哪些信息;只有你同意、并且本站允许开放的范围,才会真的授权出去。这样可以避免一旦登录就把所有信息都交给对方。

access_token 可以理解为授权成功后发给第三方应用的访问凭证。它的作用是让应用在后续请求接口时证明“这位用户已经同意授权了”,因此它不等于你的账号密码,而是一份带有限制条件的授权结果。

当你确认授权后,本站会先返回一个一次性的 authorization code 给第三方应用。这个临时码不能长期使用,它还需要配合 PKCE 这个安全机制一起完成校验,避免授权码在中途被别人拿去冒用。这里的 redirect_uri 则是授权完成后浏览器要跳回去的回调地址。

在后续结果里,你还会看到 id_token 和 nonce 这类名词。id_token 主要用于告诉第三方应用当前授权对应的是哪个登录身份;nonce 可以理解为这次登录请求附带的另一串随机标记。第三方应用发起请求时先保存它,本站再把它写回 id_token,这样应用就能检查拿到的身份结果是不是确实对应当前这一次登录授权流程。

补充阅读: 《理解 OAuth 2.0》

概览

这份授权是怎么工作的

简单来说,整个过程只做三件事:创建客户端、发起授权、交换令牌。下面先讲清楚每一步在做什么。

当前能力

  • 授权模式固定为 Authorization Code,response_type 只能使用 code。
  • PKCE 是必填项,code_challenge_method 当前只支持 S256。
  • 令牌交换成功后返回 access_token、id_token、expires_in 和 scope。
  • 目前没有 refresh_token;access_token 有效期约 30 天(2592000 秒),过期后需要重新发起授权流程。

核心端点

/oauth/authorizeGET 必填

浏览器跳转入口。用户会在这里完成登录、授权确认,并最终被重定向到你注册过的 redirect_uri。

/oauth/tokenPOST 必填

授权码交换端点。客户端提交 authorization code 与 code_verifier 后,在这里换取 access_token 和 id_token。

/oauth/userinfoGET 必填

用户信息端点。应用带上 access_token 后可在这里读取 OIDC sub;如果 scope 允许,还会返回 preferred_username。

完整流程

  • 先登录 Open CRH Tracker 账号,在用户页的“开发”选项卡里创建一个 OAuth 客户端,登记回调地址并申请需要的 scope。
  • 普通用户的 scope 需要等待管理员审核通过后才能授权;客户端创建者可以在自己的 scope 仍处于待审核状态时,先完成登录和授权测试。
  • 用户在浏览器里完成登录与授权确认后,应用会在回调地址收到一个一次性 authorization code。
  • 应用拿着 code 和当初的 PKCE verifier,调用 /oauth/token 换取 access_token 和 id_token,之后按 Bearer 方式访问 /oauth/userinfo。

创建客户端

先创建 OAuth 客户端

创建客户端是在网页上完成的,但如果你更习惯用接口,也可以直接调用 v2 的客户端接口。

先登录站内账号,然后点击右上角用户名进入用户页,选择开发选项卡,切换到 OAuth 面板,填写信息创建 OAuth 客户端。

创建时要准备的信息

namestring 必填

客户端显示名称,会出现在授权确认页。

redirectUrisstring[] 必填

允许回调的地址列表。授权请求里的 redirect_uri 必须与这里的某一项完全一致。

requestedScopesstring[] 必填

客户端希望申请的 scope 列表。只有审核通过的 scope 才能在授权请求中使用。

descriptionstring | null

可选描述,便于用户理解这个应用的用途。

homepageUrlstring | null

可选主页地址,用于开发者识别和后续管理。

用 v2 接口创建客户端

curl -X POST /api/v2/oauth/clients \
    -H 'Content-Type: application/json' \
    -H 'Cookie: token=<你的网页登录会话>' \
    --data '{
        "name": "My Transit App",
        "description": "Open CRH Tracker OAuth demo",
        "homepageUrl": "https://app.example.com",
        "redirectUris": [
            "https://app.example.com/oauth/callback"
        ],
        "requestedScopes": [
            "api.auth.me.read"
        ]
    }'

客户端创建成功后,普通用户需要等待管理员审核 scope;客户端创建者可以在自己客户端的 scope 仍为待审核时继续完成 OAuth 登录与授权测试。

发起授权

发起授权请求

授权请求必须在浏览器里发起,让用户自己完成登录和确认。

授权请求参数

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=b2d0f7a1c81e41d7

交换令牌

处理回调并交换令牌

应用在回调地址拿到 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=9f4f1c8c4d5a4f43

POST /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": 2592000,
    "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Im9jcmgta2V5LTEifQ...",
    "scope": "api.auth.me.read"
}

注意 expires_in 的单位是秒:2592000 秒约等于 30 天。access_token 和 id_token 的有效期都是这个时长;由于没有 refresh_token,过期后需要让用户重新走一遍授权流程。

使用令牌

使用 access_token

拿到 access_token 后,第三方应用可以按 Bearer Token 访问当前用户相关接口。

更多接口请见 API 文档

请求 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 允许读取当前身份信息时才会出现。

限制与排障

限制与排障

多数接入失败都来自 scope 审核、redirect_uri 不匹配或 PKCE 参数错误。

当前限制

  • 仅支持 Authorization Code,不支持 implicit、device code 或 client credentials。
  • 仅支持 PKCE S256,不支持 plain。
  • 没有 refresh_token,access_token 约 30 天过期,过期后需要重新授权。
  • 请求里的所有 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,避免过度请求接口权限。