CurrentStation 通行证

Current 系产品的统一登录平台 · OAuth 2.0 授权码 + PKCE

一套账号,登录所有 Current 系产品。第三方应用全程拿不到用户密码:用户在我们的页面上授权, 应用只拿到一个只能读取被授权信息的令牌。 Issuer https://music.20110208.xyz/cm

CurrentStation 控制台

账户中心与开发者入口:打开控制台。控制台可查阅 CurrentMusic 自有后端与网易云侧完整接口目录,并管理应用登记。

1. 角色与流程

用户          应用(你的产品)             CurrentStation 通行证
 │                  │                              │
 │  点击「用通行证登录」                            │
 │─────────────────▶│  302 /passport/authorize     │
 │                  │  ?client_id&redirect_uri     │
 │                  │  &scope&state&code_challenge │
 │                  │─────────────────────────────▶│  未登录?先在本站登录
 │◀─────────────────────────────────────────────── │  展示同意页(应用名 + 申请的权限)
 │  点「同意」       │                              │
 │───────────────────────────────────────────────▶ │  生成一次性 code(60 秒)
 │                  │◀── 302 redirect_uri?code=…&state=… ─┘
 │                  │  POST /passport/token         │
 │                  │  code + code_verifier         │
 │                  │─────────────────────────────▶│  校验 PKCE → 发 access_token
 │                  │◀── {access_token, refresh_token?, sub} ─┘
 │                  │  GET /passport/userinfo       │
 │                  │  Authorization: Bearer …      │
 │                  │─────────────────────────────▶│  按 scope 返回昵称/头像/脱敏邮箱…

2. 快速开始(4 步)

  1. 登记应用(一次性,需要 CurrentStation 管理员账号)

    用管理员登录后的会话令牌调用;返回的 clientSecret 只显示这一次,请立刻存到服务端环境变量里。原生 App / 纯前端 SPA 请用 "public": true(不发密钥,强制 PKCE)。

    curl -X POST https://music.20110208.xyz/cm/passport/apps \
      -H "Authorization: Bearer <管理员会话令牌>" -H "Content-Type: application/json" \
      -d '{"name":"CurrentSpace","redirects":["https://app.example.com/auth/callback"],
           "scopes":"openid profile email offline_access","homepage":"https://app.example.com"}'
    
    # 201
    # { "clientId": "cs_app_1a2b3c4d5e6f", "clientSecret": "cs_sk_…", … }
  2. 把用户送到授权页

    生成 PKCE:verifier 为 43~128 位随机串,challenge = BASE64URL(SHA256(verifier))(去掉 =)。state 用于防 CSRF,回调时要原样校验。

    # verifier / challenge 生成(浏览器 WebCrypto,Node 18+ 亦可)
    const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
    const challenge = base64url(new Uint8Array(
      await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))));
    
    location.href = 'https://music.20110208.xyz/cm/passport/authorize'
      + '?response_type=code'
      + '&client_id=cs_app_1a2b3c4d5e6f'
      + '&redirect_uri=' + encodeURIComponent('https://app.example.com/auth/callback')
      + '&scope=' + encodeURIComponent('openid profile email offline_access')
      + '&state=' + state
      + '&code_challenge=' + challenge + '&code_challenge_method=S256';
  3. 用 code 换令牌

    code 只能用一次、60 秒过期;换令牌时 redirect_uri 必须与授权时完全一致。有密钥的客户端用 Basic 认证,公开客户端只传 code_verifier。

    curl -X POST https://music.20110208.xyz/cm/passport/token \
      -H "Content-Type: application/json" \
      -d '{"grant_type":"authorization_code","client_id":"cs_app_1a2b3c4d5e6f",
           "client_secret":"cs_sk_…","code":"cs_code_…","redirect_uri":"https://app.example.com/auth/callback",
           "code_verifier":"<PKCE verifier>"}'
    
    # 200
    # { "access_token":"cs_at_…", "token_type":"Bearer", "expires_in":7200,
    #   "scope":"openid profile email offline_access", "refresh_token":"cs_rt_…",
    #   "sub":"cs_1a2b3c4d5e6f" }
  4. 拿用户信息
    curl https://music.20110208.xyz/cm/passport/userinfo -H "Authorization: Bearer cs_at_…"
    
    # 200
    # { "sub":"cs_1a2b3c4d5e6f", "nickname":"小明", "avatar":"https://…",
    #   "email":"m***@qq.com", "scope":"openid profile email offline_access",
    #   "client_id":"cs_app_1a2b3c4d5e6f" }

3. 端点

开发者平台页面:GET /developer(也支持 /cm/developer,独立于音乐客户端)。

方法路径鉴权说明
GET/cm/passport/meta公开平台元信息:端点、scope 目录、令牌有效期、已接入产品
GET/cm/passport/docs公开本页
POST/cm/passport/apps登录用户登记应用 → clientId / clientSecret(仅此一次);每账号上限 5 个
GET/cm/passport/apps登录用户我登记的应用(含在用授权数、配额);管理员加 ?all=1 看全部
PUT/cm/passport/apps/<client_id>所有者改名称/主页/回调/scope/公开↔机密/停用(切机密时返回新密钥一次)
POST/cm/passport/apps/<client_id>/secret所有者轮换 client_secret(只返回一次);可选 revokeGrants 同时撤销已发出的授权
DELETE/cm/passport/apps/<client_id>所有者停用应用并撤销其全部授权
GET/cm/passport/authorize用户会话同意页数据(应用名、申请的权限、当前用户;未登录返回 401)
POST/cm/passport/authorize用户会话批准/拒绝 → 返回带 code 或 error 的回调地址
POST/cm/passport/token客户端authorization_code / refresh_token / client_credentials
GET/cm/passport/userinfoBearer按 scope 返回用户信息
POST/cm/passport/introspect客户端校验令牌是否有效(RFC 7662)
POST/cm/passport/revoke客户端/用户撤销令牌(RFC 7009,幂等)
GET/cm/passport/grants用户会话我授权过的应用
DELETE/cm/passport/grants/<授权id>用户会话解除某个应用的授权(其令牌立即失效)
GET/cm/passport/events管理员审计流水(授权/换令牌/撤销/异常)

账号本体(注册、登录、退出、在线设备)仍在 /cm/auth/*,与通行证共用同一批账号;现有 App 与网页无需改动。

4. scope

scope含义返回字段
openid登录身份(必需)sub(cs_ 开头的稳定标识,不含邮箱/手机号)
profile公开资料nickname avatar bio created_at
email邮箱(脱敏)email:a***@qq.com
phone手机号(脱敏)phone:138****8000
offline_access长期授权(30 天可刷新)额外返回 refresh_token

申请未登记或超出应用上限的 scope 会得到 invalid_scope;用户看到的是每个 scope 的中文说明(同意页)。

5. 令牌与有效期

授权码 cs_code_…

60 秒、一次性;取用即失效,天然防重放。

访问令牌 cs_at_…

2 小时;放 Authorization: Bearer 头。只存哈希,撤销即时生效。

刷新令牌 cs_rt_…

30 天,每次刷新都轮换(旧的立刻失效);泄漏后可被检测(旧令牌再用即失败)。

刷新

curl -X POST https://music.20110208.xyz/cm/passport/token -H "Content-Type: application/json" \
  -d '{"grant_type":"refresh_token","client_id":"cs_app_…","client_secret":"cs_sk_…",
       "refresh_token":"cs_rt_…"}'

服务端到服务端(client_credentials)

只代表「应用自己」,没有用户信息;/userinfo 会对这类令牌返回 403。

curl -u "cs_app_…:cs_sk_…" -X POST https://music.20110208.xyz/cm/passport/token \
  -H "Content-Type: application/json" -d '{"grant_type":"client_credentials","scope":"openid"}'

6. 错误码

errorHTTP含义 / 处理
invalid_client401client_id / client_secret 不对:检查是否把密钥写进了前端代码
invalid_request400缺参数或格式不对(如公开客户端没带 PKCE)
invalid_grant400code/refresh 无效、过期、已用过,或 PKCE/redirect_uri 不匹配
invalid_scope400申请了未登记或超出应用上限的 scope
unsupported_grant_type400grant_type 拼错
access_denied302用户点了拒绝(回调地址上带 error=access_denied)
invalid_token401/userinfo 令牌无效或过期(响应头带 WWW-Authenticate)
—429令牌端点限流(同一客户端+IP 60 秒内 30 次)

7. 安全约定

8. CurrentDeveloper 开发者平台(自助接入)

不用找管理员:任何通行证账号都能自助把自己的应用接进来。

入口:https://music.20110208.xyz/developer —— 独立页面,不依赖 CurrentMusic 客户端, 用自己的通行证会话(浏览器 localStorage 里的 csdev.token)登录,不读写音乐客户端的登录态。

能力说明
新建应用填名称、应用主页、回调地址(每行一个,精确匹配)、需要的 scope,并选择「公开客户端(PKCE)」或「机密客户端(带密钥)」
获取凭据client_id 随时可见可复制;client_secret 只在创建或轮换时显示一次(服务端只存哈希,我们自己也拿不回来)
获取令牌平台内可一键用刚拿到的凭据试取应用令牌(client_credentials),并生成可直接粘贴的接入代码(授权码 + PKCE 三段)
轮换密钥密钥泄漏或遗失时一键换新(旧密钥立即失效);可勾选「同时撤销该应用已发出的授权」
配额与权限每个账号最多 5 个应用;只能查看/修改/停用自己登记的应用(管理员可管理全部)

第三方产品的用户数据只经 /passport/userinfo 按 scope 返回,应用侧拿不到密码,也不该把密钥写进前端代码。

9. 已接入产品

正在读取…

10. 变更记录

版本内容
1.2(v1.28.19)开发者平台从 CurrentMusic 客户端里独立出来:GET /developer 单文件页面(自带登录/注册、独立会话键 csdev.token),客户端不再内置该页,只保留一个外链入口
1.1(v1.28.18)新增 CurrentDeveloper 开发者平台:任何账号可自助登记应用(每账号 5 个)、填主页与回调、获取/轮换凭据、一键试取应用令牌、停用/启用;POST /passport/apps/{client_id}/secret
1.0(v1.28.17)首个版本:授权码 + PKCE、刷新轮换、userinfo、introspect、revoke、应用登记、授权管理、审计、单点登出