跳到主要内容

水鱼账号 OAuth 接口文档

本文是水鱼账号授权服务器与查分器 OAuth 端点的完整说明。若您是初次接入,建议先阅读 快速开始;若您正在从 Developer-Token 迁移,请先阅读 迁移指南

1. 端点

水鱼账号授权服务器(以下简称授权服务器)的地址为:

https://auth.diving-fish.com
提示

请在代码中只硬编码上述根地址,其余端点一律从发现文档读取。 这样端点日后有所调整时,您不需要修改代码。

发现文档遵循 OpenID Connect Discovery 与 RFC 8414 两套路径,内容相同:

端点路径请求方法说明
/.well-known/openid-configurationGETOIDC 发现文档
/.well-known/oauth-authorization-serverGET同上,供只识别此路径的客户端库使用
/.well-known/jwks.jsonGET验签公钥集合,建议缓存 5 分钟

标准 OIDC 客户端库(如 authliboidc-client-tsopenid-client )只需提供上述发现文档地址即可自动完成配置。

发现文档中声明的端点如下:

端点路径请求方法说明参见
/oauth/authorizeGET / POST授权端点第 3 节
/oauth/tokenPOST令牌端点第 4 节
/oauth/device_authorizationPOST设备码端点第 5 节
/oauth/userinfoGET / POST用户信息端点第 7 节
/oauth/revokePOST撤销端点第 8 节
/oauth/logoutGET / POST登出端点第 8 节

此外还有两个供用户使用的页面:

地址说明
https://auth.diving-fish.com/console开发者控制台:登记应用、补全信息、生成 client_secret
https://auth.diving-fish.com/device设备码绑定页:用户在此输入用户码并确认授权

2. scope

scope性质含义
openid身份必须携带,否则无法取得 id_token ,也无法调用 /oauth/userinfo
profile读取用户名、昵称
prober.profile.read读取查分器资料:段位、姓名框、绑定的 QQ 号
prober.records.read读取读取舞萌 DX 成绩。大部分 bot 只需要这一项
prober.records.write写入代用户上传、修改舞萌 DX 成绩
chunithm.records.read读取读取 CHUNITHM 成绩
chunithm.records.write写入代用户上传、修改 CHUNITHM 成绩

多个 scope 之间以空格分隔。

请只申请实际用到的 scope 。申请项越多,用户在授权页上放弃授权的概率越大,凭据泄露时的波及面也越大。

信息

含写入权限的申请一律转为人工审核。在审核通过之前,该应用取得的令牌中不会包含任何写入 scope ,只读部分不受影响。

3. 授权端点

端点路径请求方法
/oauth/authorizeGET / POST

供有回调地址的应用使用。将用户浏览器导向该地址,用户登录并同意后,浏览器携带授权码跳回您登记的回调地址。

3.1 请求参数

参数必填说明
response_type固定为 code
client_id您的应用标识
redirect_uri必须与登记值完全一致,尾斜杠、协议、端口都计入比较。唯一的例外是 http://127.0.0.1http://[::1] ,这两者的端口不参与比对,详见下方说明
scope空格分隔的 scope 列表
state随机串,回调时原样返回。您必须自行保存并核对
nonce随机串,将写入 id_token ,用于防止重放
code_challengecode_verifier 的 sha256 摘要,经 base64url 编码且去掉填充
code_challenge_method固定为 S256

本机回调地址。 桌面程序、代理工具一类在本机监听端口接收授权码的应用,登记 http://127.0.0.1/callbackhttp://[::1]/callback 即可,端口不参与比对RFC 8252 §7.3):运行时 监听哪个端口都能通过校验,不必写死一个端口,也不必因换了端口回控制台改登记。 协议、路径与查询串照旧逐字符比对。

警告

放宽的只有 127.0.0.1::1 这两个 IP 字面量。http://localhost/callback 仍按普通地址逐字符比对(端口计入),因为它要经过 DNS 与 hosts 解析,落点未必在本机。 本机程序请一律填 IP 字面量。

注意

PKCE 为强制要求,机密客户端也不例外。 缺少 code_challenge 的请求直接返回 400 ; code_challenge_method 只接受 S256 ,不接受 plain

3.2 响应

用户同意后跳转至:

<redirect_uri>?code=your_authorization_code&state=your_state

请务必先核对 state ,不一致则丢弃本次回调。 这是防止 CSRF 的唯一手段。

授权码为一次性,有效期 60 秒。重复使用不仅会失败,还会连带吊销由该授权码换出的令牌。

用户拒绝授权时跳转至:

<redirect_uri>?error=access_denied&state=your_state

4. 令牌端点

端点路径请求方法请求体格式
/oauth/tokenPOSTapplication/x-www-form-urlencoded

客户端认证。 应用登记时选定的部署形态决定了它属于哪一类客户端,二者的认证方式不同:

部署形态客户端类型认证方式可用的 grant_type
由开发者自己部署运行机密客户端每次请求携带 client_secret授权码 / 刷新 / 设备码 / 换票
分发给用户各自部署公开客户端不携带 client_secret ,由 PKCE 与令牌本身担保授权码 / 刷新 / 设备码

机密客户端的 client_secret 写在请求体中(client_secret_post)。

危险

认证方式按登记值严格比对,多传与少传同样会被拒。

机密客户端未携带 client_secret ,或公开客户端携带了 client_secret ,令牌端点均返回 401 invalid_client ,两种情形的响应完全相同。若您在接入之初就持续收到 401 ,请先在控制台确认该应用的部署形态,以及机密客户端的 client_secret 是否已经生成——登记应用与生成 client_secret 是两个独立的动作,只完成前者时该应用没有任何可用凭据。

该端点支持四种 grant_type

4.1 authorization_code 授权码换取令牌

参数必填说明
grant_typeauthorization_code
code授权端点返回的授权码
redirect_uri与授权请求中完全一致
client_id应用标识
client_secret视情况机密客户端必填,公开客户端不传
code_verifier生成 code_challenge 时所用的原串

响应:

{
"token_type": "Bearer",
"access_token": "eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJSUzI1NiIsImtpZCI6...",
"expires_in": 900,
"refresh_token": "your_refresh_token",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
"scope": "openid profile prober.records.read"
}

4.2 refresh_token 刷新令牌

参数必填说明
grant_typerefresh_token
refresh_token当前持有的 refresh token
client_id应用标识
client_secret视情况机密客户端必填

响应结构与 4.1 相同。refresh token 有效期为 30 天。

危险

refresh token 强制轮换:每次刷新都会签发一把新的,旧的立即作废。

旧的 refresh token 再次出现时,服务器会将其视为凭据泄露,吊销整条令牌链,包括本次刚刚签发的那一把,用户需要重新授权。这是发现 refresh token 被窃取的唯一手段,因此不会放宽。

实践上需要注意三点:不要并发刷新(多个线程或进程同时持同一把令牌去刷,第二个即触发吊销);新令牌必须在使用前完成持久化,不要等到进程退出时才落盘;不要在多台机器上共享同一把 refresh token 。

4.3 on-behalf-of 换票:代用户获取令牌

这是查分器体系的自定义授权类型,供已经取得用户授权的服务端应用使用。它以「应用凭据 + 用户标识」换取一张代表该用户的短期令牌。

参数必填说明
grant_typeurn:diving-fish:params:oauth:grant-type:on-behalf-of
client_id应用标识
client_secret必填。公开客户端不能使用该授权类型,改用 4.4 节的设备码取得 refresh token
subject用户标识,四种写法见 第 6 节
scope不填则取「用户授权范围」与「应用已获批准范围」的交集

响应中没有 refresh token ,这条路径不需要它:

{
"token_type": "Bearer",
"access_token": "eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJSUzI1NiIsImtpZCI6...",
"expires_in": 300,
"scope": "prober.records.read"
}

服务端在签发前会依次校验三项,缺一不可:

  1. client_secret 是否正确,即您是哪个应用;
  2. 该用户是否授权过您的应用;
  3. 本次申请的 scope 是否在用户的授权范围之内。
信息

subject 只是查找键,不是凭据。这是它与 Developer-Token 时代 qq 参数最本质的区别:过去知道 QQ 号加上任意一个开发者 Token 即可读取该用户的成绩,现在它只能在「已授权集合」中查表命中。

注意

令牌有效期只有 5 分钟。它比授权码方式的 15 分钟更短,是因为这条路径上的令牌不落库、不可即时吊销,用户撤销授权只能等令牌自然过期后生效。请在有效期内复用令牌,不要每次业务请求都换票。

错误响应:

error状态码含义
consent_required400该用户未授权您的应用,或该用户不存在。两者不作区分
consent_required (描述含 scope not granted400用户已授权,但本次申请的 scope 超出其授权范围
invalid_client401client_idclient_secret 有误,或应用已停用
invalid_scope400申请的 scope 不在该应用已获批准的范围内
invalid_request400subject 缺失或格式错误
unauthorized_client400该应用未被允许使用此授权类型
slow_down429换票过于频繁,见 第 10 节
信息

「该用户未授权」与「该用户不存在」返回同一个错误,是有意为之。若两者可以区分,subject=qq: 就成了一个「这个 QQ 号在查分器注册过没有」的枚举接口。

对您而言两种情况的处理方式完全相同:引导用户完成一次绑定。

4.4 device_code 设备码换取令牌

参数必填说明
grant_typeurn:ietf:params:oauth:grant-type:device_code
device_code设备码端点返回的设备码
client_id应用标识
client_secret视情况机密客户端必填

用户尚未操作时返回 authorization_pending ,轮询过快时返回 slow_down ,用户拒绝时返回 access_denied ,设备码过期时返回 expired_token

成功时的响应在标准字段之外,额外包含一个 sub 字段,即该用户的水鱼用户 ID :

{
"token_type": "Bearer",
"access_token": "eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJSUzI1NiIsImtpZCI6...",
"expires_in": 900,
"scope": "prober.records.read",
"sub": "12345"
}

同一个设备码只能换取一次令牌,换取后即失效。

公开客户端的响应中还包含 refresh_token ,此后凭它按 4.2 节自行续期。这是公开客户端取得长期访问能力的唯一途径:它不能使用换票,因此必须轮询,且必须为每位用户各自保存这把令牌。机密客户端不在此列——它的 grant_type 中不含 refresh_token ,续期请走换票。

5. 设备码端点

端点路径请求方法请求体格式
/oauth/device_authorizationPOSTapplication/x-www-form-urlencoded

供没有浏览器、也无法开放公网回调地址的应用使用,用于引导用户完成一次绑定。

5.1 请求参数

参数必填说明
client_id应用标识
client_secret视情况机密客户端必填
scope本次绑定申请的权限,必须在应用已获批准的范围内
subject_ref您自己的用户标识的摘要,见 6.1 节。仅供换票使用,公开客户端用不到
binding_label遮挡后的身份展示串,例如 QQ 11****14 ,最长 64 字符
注意

binding_label 虽非必填,但请务必填写

设备码流程没有回调地址,用户无从通过「跳转到哪里」判断自己在授权给谁。若攻击者向您的应用索要一条绑定链接(其中的标识是攻击者自己的)并转发给受害者,受害者点击同意后,攻击者即可通过您的应用读写受害者的数据。同意页上那一行「绑定身份」,是受害者唯一可能察觉异常的地方。

该字段会被作为不可信文本处理,即转义后截断显示。

危险

公开客户端另有一重风险:该端点不校验凭据,任何人都能用您的 client_id 生成绑定链接。

对机密客户端而言,冒名者最多只能让受害者绑到自己的标识上;而公开客户端在轮询中直接取得用户的 refresh token ,冒名者把链接转发给受害者、受害者点击同意之后,对方即持有该账号的长期访问权限。

因此请让用户在您的程序界面中看到那串用户码,不要以「把这个链接发给需要绑定的人」的形式使用它。同意页会对公开客户端额外提示这一点,但那是最后一道防线,不是第一道。

5.2 响应

{
"device_code": "your_device_code",
"user_code": "BCDF-GHJK",
"verification_uri": "https://auth.diving-fish.com/device",
"verification_uri_complete": "https://auth.diving-fish.com/device?user_code=BCDF-GHJK",
"expires_in": 600,
"interval": 5
}

verification_uri_complete 提供给用户即可。用户码有效期为 10 分钟。

对机密客户端而言,用户点击同意的那一刻,授权记录与 subject_ref 的映射即告建立,因此轮询并非必需:您可以不理会 device_code ,下一次换票自然会成功。轮询的价值在于明确知晓用户点击的是同意还是拒绝。

公开客户端则必须轮询:它无法换票,用户令牌只在这一次轮询的响应中出现。

5.3 错误响应

error含义
invalid_clientclient_idclient_secret 有误
unauthorized_client该应用未被允许使用设备码方式
unauthorized_client (描述含 client profile is incomplete应用信息尚未补全,见 迁移指南 3.1 节
unauthorized_client (描述含 client is disabled该应用已被停用
invalid_scope申请的 scope 不在该应用已获批准的范围内
invalid_requestsubject_ref 格式错误,须为小写十六进制的 sha256 摘要
slow_down发起绑定过于频繁,见 第 10 节

6. 用户标识 subject

换票时的 subject 参数用于指明「代哪个用户操作」。共有四种写法。

6.1 ref:应用自有标识的摘要

长期方案,所有应用均可使用。

subject=ref:9a1b2c3d...

摘要的计算方式为:

import hashlib

def subject_ref(client_id: str, external_id: str) -> str:
"""external_id 是您的应用自己使用的用户标识,QQ 号、频道 ID、自有用户 ID 均可。"""
return hashlib.sha256(f"{client_id}:{external_id}".encode()).hexdigest()

必须为小写十六进制,共 64 位。该摘要与用户账号的映射在设备码绑定完成时建立,因此使用这种写法前,需要用户走一次 设备码绑定,且发起绑定时提交了对应的 subject_ref

Developer-Token 迁移而来的应用是例外:其存量映射已在迁移时一并建好,这些用户无需重新绑定,但摘要必须与迁移时算出的完全一致——算法、拼接方式、external_id 的取值任一不同都会查不到人,且返回的同样是 consent_required 。详见 迁移文档 5.1 节

采用这种写法的理由:摘要中混入了 client_id ,同一个用户在不同应用中的标识互不相同,任一应用的数据泄露都不会暴露该用户在其他应用中的身份;同时它允许您使用自己的用户体系,不必要求用户在您的服务中以 QQ 号出现。

6.2 sub:水鱼用户 ID

长期方案,所有应用均可使用。

subject=sub:12345

取值为水鱼用户 ID ,即设备码换取令牌时响应中的 sub 字段,也是 access token 中 sub claim 的值。适用于愿意自行保存映射关系的应用。

6.3 qq:QQ 号

过渡方案,仅对从 Developer-Token 迁移过来的应用开放,且将于 2026 年 10 月 1 日 00:00(UTC+8)停止工作,与 Developer-Token 同时。

subject=qq:123456

取值同时匹配用户绑定的 QQ 号与频道 ID ,与 Developer-Token 时代 qq 参数的行为一致。

请在该日期之前改用 6.1 的 ref :您手上已经有这个 QQ 号,按 6.1 的公式算出摘要即可,不需要用户重新绑定(见 迁移文档 5.1 节)。届时该写法将返回:

{"error": "invalid_request", "error_description": "'subject=qq:' is retired for this client; use ref: or sub:"}

保留期限止于此日的原因:QQ 号与频道 ID 共用同一个字段,用户可以随时改绑或解绑,改绑之后同一个取值指向的将是另一个账号;而它并不提供 ref 之外的任何能力。

6.4 username:查分器用户名

长期方案,所有应用均可使用。

subject=username:your_username

取值为查分器用户名。

这种写法解决的是 6.1 与 6.2 都覆盖不到的场景:用户名由第三方在查询时临时给出,例如群聊中一位用户要求您的 bot 查询另一位用户的成绩,而后者的标识您在绑定阶段从未记录过,因而算不出可命中的 ref 摘要。

它不会让您读到授权范围以外的数据:解析出用户后仍需通过该用户对您的应用的授权检查,未授权者一律返回 consent_required ,与用户不存在的情形无法区分。因此能够查到的始终是您自己的已授权用户集合。

注意

仅 6.3 一种写法有截止时间,见该节。6.4 长期可用。

7. 用户信息端点

端点路径请求方法权限要求
/oauth/userinfoGET / POST携带 openid scope 的 access token
GET https://auth.diving-fish.com/oauth/userinfo
Authorization: Bearer your_access_token
{
"sub": "12345",
"preferred_username": "your_username",
"name": "your_nickname",
"nickname": "your_nickname"
}

返回哪些字段取决于令牌的 scope 。

提示

请通过该端点获取用户资料,不要把用户资料塞进 access token 自行解析——令牌会进入日志与代理缓存,其中不应包含个人信息。

8. 撤销端点

用户可以随时在水鱼账号的设置页中撤销对您的授权,撤销后您持有的相关令牌立即失效。您也可以主动撤销:

端点路径请求方法
/oauth/revokePOST
参数必填说明
token要撤销的令牌
token_type_hintrefresh_tokenaccess_token
client_id应用标识
client_secret视情况机密客户端必填

登出端点用于结束用户在水鱼账号的登录会话:

GET https://auth.diving-fish.com/oauth/logout?id_token_hint=your_id_token&post_logout_redirect_uri=your_registered_uri&state=your_state

post_logout_redirect_uri 必须是该应用登记过的地址,否则只执行登出,不予跳转。

9. 查分器端点

取得 access token 后,即可携带它请求查分器:

GET https://www.diving-fish.com/api/maimaidxprober/player/records
Authorization: Bearer your_access_token

请求中不需要、也不接受 qqusername 参数,查询对象由令牌决定。

端点路径的写法与 查分器 API 文档 一致,即:

https://www.diving-fish.com/api/{游戏数据类别}/{端点路径}

支持 Bearer 令牌的端点如下。这些端点同时也接受 登录验证Import-Token,三种验证方式择一即可。

游戏数据类别端点路径请求方法所需 scope替代的旧端点
maimaidxprober/player/recordsGETprober.records.read/dev/player/records
maimaidxprober/player/recordPOSTprober.records.read/dev/player/record
maimaidxprober/player/platePOSTprober.records.read/query/plate
maimaidxprober/player/update_recordsPOSTprober.records.write——
maimaidxprober/player/update_records_htmlPOSTprober.records.write——
maimaidxprober/player/update_recordPOSTprober.records.write——
maimaidxprober/player/delete_recordsDELETEprober.records.write——
chunithmprober/player/recordsGETchunithm.records.read/dev/player/records
chunithmprober/player/update_recordsPOSTchunithm.records.write——
chunithmprober/player/update_records_htmlPOSTchunithm.records.write——
chunithmprober/player/delete_recordsDELETEchunithm.records.write——

写入类端点的请求体与响应结构,与 查分器 API 文档 中同名端点的说明一致,此处不再重复。

9.1 获取用户的完整成绩信息

游戏数据类别端点路径请求方法所需 scope
maimaidxprober/player/recordsGETprober.records.read

无需任何参数。响应结构与 查分器 API 文档 3.1.5/dev/player/records 的响应完全一致:

{
"username": "your_username",
"nickname": "your_nickname",
"rating": 15000,
"additional_rating": 22,
"plate": "舞神",
"records": []
}
提示

完整成绩信息的数据量较大。若您只需要用于绘制 b50 的简略成绩信息,请使用无需验证的 /query/player 端点;若您只需要其中一部分成绩,可以用查询参数在服务端过滤,详见 服务端过滤

9.2 获取用户的单曲成绩信息

游戏数据类别端点路径请求方法所需 scope
maimaidxprober/player/recordPOSTprober.records.read

请求体为 JSON ,包含 music_id 参数,可以是单个值,也可以是列表:

{"music_id": [11663, 834]}

响应结构与 查分器 API 文档 3.1.6 一致,为以歌曲 ID 为键、该歌曲各难度成绩数组为值的对象。

9.3 按版本获取用户的成绩信息

游戏数据类别端点路径请求方法所需 scope
maimaidxprober/player/platePOSTprober.records.read

请求体为 JSON ,包含 version 参数,必须为列表,即使只有一个元素

{"version": ["maimai"]}

响应结构与 查分器 API 文档 3.1.8 一致:

{"verlist": []}
提示

本端点的功能已并入 /player/records 的过滤参数:GET /player/records?plate=真 按牌子筛选,?version=... 按版本筛选,且返回结构与其他成绩端点一致。详见 服务端过滤

9.4 获取用户的 CHUNITHM 成绩数据

游戏数据类别端点路径请求方法所需 scope
chunithmprober/player/recordsGETchunithm.records.read

https://www.diving-fish.com/api/chunithmprober/player/records 。无需任何参数,也可以用查询参数在服务端过滤,详见 查分器 API 文档 3.2.3 的服务端过滤。响应结构与 查分器 API 文档 3.2.3/dev/player/records 的响应完全一致:

{
"username": "your_username",
"nickname": "your_nickname",
"rating": 16.5,
"records": {"best": [], "r10": []}
}

9.5 错误响应

状态码响应体含义
401{"status": "error", "message": "<验签失败的原因>"}令牌无效、已过期,或 Authorization 头格式有误。请重新换取令牌
403{"status": "error", "message": "access token 缺少权限:..."}令牌中缺少该端点所需的 scope
403{"status": "error", "message": "该用户未同意用户协议"}该用户未同意查分器的用户协议
400{"status": "error", "message": "用户不存在"}令牌中的用户已注销
429{"status": "error", "message": "已超出今日请求上限"}第 10 节
503{"status": "error", "message": "服务端未启用 OAuth"}服务端暂时未启用该验证方式

9.6 与 Developer-Token 时代的行为差异

项目变化
已设置隐私的用户Developer-Token 一律返回 403 。授权体系下,用户既然主动授权了您的应用,即可正常读取
未同意用户协议的用户仍然拒绝,返回 403 ,与此前一致
成绩掩码用户勾选「对非网页查询的成绩使用掩码」时,第三方应用读到的仍是掩码值,与此前一致
未授权的用户在换票阶段即返回 consent_required ,请求不会到达查分器

10. 配额与限流

限制分为两层,作用对象不同。

10.1 接口调用配额

由查分器执行,按自然日(UTC)计算,超出后返回 429 。

维度默认额度
每个应用对每位用户每日 200 次
每个应用合计每日 1000 + 授权用户数 × 20

一次查分通常对应一次成绩拉取,因此每位用户每日 200 次对正常使用场景是充裕的。

应用维度的额度跟随您的授权用户数增长,因此不需要随规模扩大反复申请调整。其中的基础量 1000 次是为用户数尚未积累起来的应用预留的余量,调试、重试与定时批量刷新都发生在这一阶段。授权用户数的变化会在数分钟内反映到额度上。

若您的服务确有特殊需求,无法被上述额度覆盖,请通过控制台联系。

10.2 授权服务器限流

由授权服务器执行,用于防止滥用,超出后返回 slow_down 与状态码 429 。

动作限制
换票(同一应用对同一用户)每小时 60 次
换票(同一应用合计)每小时 3000 次
发起设备码绑定(同一应用)每小时 600 次
提示

正常实现会把令牌缓存到过期,5 分钟一张,一小时最多换 12 次。若您触及了每小时 60 次的上限,通常意味着令牌没有被缓存,或存在重复换票的逻辑。

10.3 未审核应用的用户数上限

未通过审核的应用,授权用户数上限为 50 。通过审核后取消该限制。

11. 校验 access token

本节供自建资源服务器的开发者参考。若您只是调用查分器接口,可以跳过。

access token 是 RS256 签名的 JWT ,请在本地验签,不要每次都请求授权服务器。

import time

import requests
from joserfc import jwt
from joserfc.jwk import KeySet

ISSUER = "https://auth.diving-fish.com"
jwks = KeySet.import_key_set(
requests.get(f"{ISSUER}/.well-known/jwks.json", timeout=10).json()
)

def verify(token: str) -> dict:
decoded = jwt.decode(token, jwks, algorithms=["RS256"])
claims = decoded.claims
assert claims["iss"] == ISSUER
assert claims["exp"] > time.time()
return claims

必须校验的项目:issexpaud ,以及本次操作所需的 scope 是否在 scope 之中。不校验 scope 等同于没有做授权。

令牌中的 claim :

claim说明
sub水鱼用户 ID 。它是账号的唯一标识,不随用户资料的任何改动而变化
scope空格分隔的 scope 列表
client_id签发给哪个应用
aud该令牌的目标受众。涉及查分器数据的令牌会包含查分器的资源标识
act仅 on-behalf-of 换来的令牌包含。其中的 client_id 表明本次操作由应用代为发起
df_quota调用配额,u 为每用户每日额度,c 为每应用每日额度
iss / exp / iat / jti标准字段

JWKS 缓存 5 分钟即可。密钥轮换时,新的 kid 会先出现在 JWKS 中,再开始用于签名,因此缓存不会导致验签失败。

注意

access token 不查库,不可即时吊销,最长存在 15 分钟的窗口。需要立即切断访问时,请撤销 refresh token ;用户在设置页撤销授权时会自动执行这一步。

12. 常见错误

现象原因
授权页返回 400 ,提示缺少 code_challenge未实现 PKCE 。所有客户端均须实现
授权页返回 400 ,提示 redirect_uri 未登记与登记值不完全一致,尾斜杠、协议、端口都计入比较(本机回环地址的端口除外)。注意 localhost 不等同于 127.0.0.1
令牌端点返回 401 invalid_client机密客户端未传 client_secret 、公开客户端多传了 client_secret ,或 client_secret 尚未生成,见 第 4 节
令牌端点返回 400 invalid_grant授权码已过期(超过 60 秒)、已被使用,或 code_verifier 不正确
刷新后所有令牌突然失效并发刷新,或旧的 refresh token 被重复使用,见 4.2 节
换票返回 consent_required该用户未授权,或其账号未绑定您提供的标识,见 4.3 节
发起绑定返回 client profile is incomplete应用信息尚未补全,见 迁移指南 3.1 节
资源服务器返回 invalid_tokeniss 不匹配,或 access token 已过期