鉴权与凭据
读目录可以什么凭据都不带。要带的话,一条请求只带一个;带哪一个,由你读的是「目录里的公共事实」还是「某个用户自己的东西」决定。
三种身份
| 身份 | 请求头 | 代表谁 | 用在哪些前缀 |
|---|---|---|---|
| 匿名 | 不带 | 任何人 | /v2/catalog 只读面(claim-events 除外,按 IP 限流);/v2/news、/v2/vocabularies、/v2/problems、/v2/catalog/stats、/v2/catalog/schemas/{object} |
| 应用密钥 | Authorization: Bearer nmk_live_… |
你的应用 | /v2/catalog、/v2/store、/v2/moyu/*、/v2/sticker/* |
| 用户访问令牌 | Authorization: Bearer <access_token> |
授权给你的那个用户 | /v2/me、/v2/moderation;带 catalog:read 时也可用于 /v2/catalog |
一条请求只带一个凭据。
/v2/catalog收两种凭据,意思是二选一,不是两个都带。服务端按Authorization里那一个值的前缀分道:nmk_是应用密钥,其余按用户令牌解析;一种失败了不会再当另一种试一次。
不带凭据
/v2/catalog 只读面(/v2/catalog/claim-events 除外)不带 Authorization 也能调。返回的数据和带密钥时逐字节相同,nsfw=true 照样可用;差别只在额度:匿名请求按来源 IPv4 地址或 IPv6 /64 共用一份很小的额度(默认每分钟 30 次、每天 1,000 次,见 限流与配额)。
它是给个人脚本、浏览器扩展和纯前端小项目准备的——这些场景要么没有服务端可放密钥,要么量小到不值得注册。CORS 对所有来源开放,浏览器里直接 fetch 就行,不要为此把密钥写进前端。
只有完全不带
Authorization头才算匿名。带了却无效、过期或已吊销的凭据在/v2/catalog上仍然是401 INVALID_CREDENTIAL,不会悄悄降级成匿名——否则密钥一失效,你的服务就会在毫无报错的情况下掉进那份很小的匿名额度。同理,只带X-API-Key头也不算匿名:/v2不读这个头,仍回401 MISSING_CREDENTIAL。
应用密钥
- 前缀
nmk_live_(生产)或nmk_test_(开发联调),尾部带 CRC32 校验位——手抖改错一个字符能在到达服务端前就被认出来。 - 只在铸造时显示一次。丢了就吊销重铸,没有找回。
- 它是机密:只放服务端。浏览器、移动端二进制、公开仓库、CI 日志都不行。前端要用数据,请让自己的后端代理;分发出去的桌面客户端没有后端可代理,改用用户访问令牌(见下)。
- 每个应用最多 5 把在用密钥——轮换时先铸新的、灰度切流、再吊销旧的,不必停机。
scope
| scope | 开什么 | 怎么拿 |
|---|---|---|
catalog:read |
整个 /v2/catalog 只读面 |
控制台自助勾选;也可作为用户 scope 向人索取 |
store:read |
/v2/store 商店联盟链接与统计 |
控制台自助勾选 |
claim_events:read |
认领事件 feed 与自家站点的审核队列状态(claim_state=pending,declined,hidden) |
运营方按需授予,不能自助 |
claim_events:read 不开放自助是有原因的:那条 feed 里带着每次拒绝的理由和做出决定的审核员 uid。
/v2/moyu/*与/v2/sticker/*两个下游面(面联邦,gateway-terminated)不需要任何 scope——任意有效应用密钥都能调用,网关只看密钥本身与限流。它们也不支持浏览器直接fetch:预检OPTIONS不带认证头,会被网关 401。这不是你的 CORS 配置问题——密钥本就不该出现在浏览器里,请从你自己的后端调用。
用户访问令牌
/v2/me 与 /v2/moderation 读写的是某个用户自己的东西——他的游玩时长、他提交的编辑提案、他的认领、他投的封面票。应用密钥在这两个前缀下一律无效,因为它证明不了「哪个用户」。
自 2026-09-06 起用户令牌还能读 /v2/catalog:令牌只要带 catalog:read(在 user_login.scopes 里申请,用户在同意页勾)就行——这份 user_login 就在控制台应用详情页的「用户登录」卡片上自助配置,回调地址与 scope 都在那里改。这条路是给分发出去的原生客户端准备的——Tauri / Wails 写的游戏管理器把二进制交到用户手里,里面放不下任何机密,而用户令牌本来就是用户自己的。请求按用户计配额(该用户跨所有已授权应用共池),不按应用;/v2/catalog/claim-events 与 /v2/store 仍只收应用密钥。完整走查见原生桌面应用接入。
拿令牌走标准的 OAuth 2.0 授权码 + PKCE:
- 把用户跳到
https://account.nextmoe.com/api/v1/oauth/authorize,带上response_type=code、client_id、redirect_uri、scope、state与code_challenge/code_challenge_method=S256。 - 用户同意后回调你的
redirect_uri,带回code。校验state。 POST https://account.nextmoe.com/api/v1/oauth/token,用code+code_verifier换access_token(JWT,15 分钟)与refresh_token。- 带
access_token调/v2/me/*;过期后用refresh_token刷新,每次刷新都会轮换。
refresh_token是不透明随机串,不是 JWT。不要解析它、不要从里面读过期时间——过期的唯一信号是刷新失败。
OAuth 端点的线格式是裸 RFC 6749({access_token, token_type, expires_in, …}),同样没有信封;失败是 {"error": "...", "error_description": "..."}。完整契约见统一文档门户 docs-kungal.nextmoe.dev。
认证失败长什么样
| 状态 | code |
含义 | 怎么办 |
|---|---|---|---|
| 401 | MISSING_CREDENTIAL |
没有 Authorization 头,而这个面不收匿名(/v2/me、/v2/moderation、/v2/folders、/v2/store、claim-events) |
带上对应的凭据 |
| 401 | INVALID_CREDENTIAL |
凭据存在,但无效、过期或已被吊销 | 换一把密钥;用户令牌则去刷新 |
| 403 | SCOPE_REQUIRED |
凭据有效,但缺这个操作要的 scope | 在控制台补勾 scope 并重铸密钥;用户令牌则带上该 scope 重新授权 |
| 403 | USER_IDENTITY_REQUIRED |
这个面要用户身份,你带的是应用密钥 | 改用用户访问令牌 |
错误体是 RFC 9457 application/problem+json,字段与分支写法见 错误处理。
限流身份跟着凭据走
密钥按密钥所属应用计数,用户令牌按用户计数,匿名按 IPv4 地址或 IPv6 /64 计数。也就是说,从自己的服务端调用时请始终带上密钥(读目录)或用户令牌(用户面)——不带凭据的服务端会让它背后的所有用户挤进同一个匿名 IP 桶。详见 限流与配额。