Skip to content

授权页跳转登录 ​

第三方网站 / App 把浏览器跳到 Kango 授权页,用户在授权页登录 Kango 账号并点「同意」,Kango 带授权码 code 回跳你的 redirect_uri。适用于网站、App、SPA。

拿到 code 之后的换 token、拉用户信息、验 id_token,以及前置准备、scope、错误码,见 OAuth 2.0 授权登录。PC 网站想内嵌二维码让用户用手机扫,见 网站扫码登录。

测试页

apisvc/jssdk/oauth-demo.html 是一个纯前端测试页(AppSecret 填在网页里,只为一个文件跑通)。正式接入请参照 服务端完整示例,示例里的「跳转 Kango 授权页登录」就是本页的流程。

流程 ​

text
第三方网站                      Kango 平台
    │
    │ 1. 跳转 GET /api/oauth/authorize
    │    (client_id, redirect_uri, scope, state,
    │     code_challenge, code_challenge_method=S256)
    ├──────────────────────────────►
    │                               2. 校验 client / redirect_uri / PKCE
    │                               3. 302 跳转到用户同意页(登录 + 同意授权)
    │                               4. 用户点「同意」→ 签发授权码 code
    │ 5. 302 回跳 redirect_uri?code=xxx&state=xxx
    ◄──────────────────────────────┤
    │
    │ 6. POST /api/oauth/token(服务端 / 前端均可)
    │    (grant_type=authorization_code, code,
    │     redirect_uri, client_id, client_secret, code_verifier)
    ├──────────────────────────────►
    │ 7. 返回 access_token + refresh_token + id_token
    ◄──────────────────────────────┤
    │
    │ 8. GET /api/oauth/userinfo  (Bearer access_token)
    ├──────────────────────────────►
    │ 9. 返回用户 claims(sub=openId)
    ◄──────────────────────────────┤

第 6 ~ 9 步见 令牌端点、UserInfo 端点。

授权端点 ​

http
GET /api/oauth/authorize

浏览器跳转入口(公开)。校验通过后 302 跳转到用户同意页。

参数必填说明
response_type是固定 code
client_id是H5APP 的 AppKey
redirect_uri是回跳地址,须与登记一致
scope否空格分隔,默认 openid
state推荐随机串,回跳时原样返回,防 CSRF
code_challenge是PKCE 挑战值:base64url(sha256(code_verifier))
code_challenge_method是固定 S256(必须使用 PKCE)
nonce否OIDC 随机串,会写入 id_token

用户同意后回跳:

text
{redirect_uri}?code=授权码&state=原样返回

用户拒绝或出错回跳:

text
{redirect_uri}?error=access_denied&error_description=用户拒绝授权&state=...

INFO

授权码有效期 15 分钟,单次使用,换 token 后立即作废。

必须使用 PKCE

本方式下机密客户端和公开客户端都必须带 code_challenge(S256),换 token 时带对应的 code_verifier。机密客户端另外还要带 client_secret。

PKCE 前端示例(JS) ​

适用于公开客户端(SPA / native App,AppType=1)。机密客户端请在服务端生成 code_verifier 并完成换 token,见 服务端完整示例。

公开客户端的令牌

公开客户端刷新不需要 secret,refresh_token 被偷就能直接续,所以平台对它收紧了:

  • refresh_token 有效期 7 天(机密客户端 30 天),且有重放检测,旧票再出现会吊销整个授权。
  • refresh_token 不要放 localStorage:SPA 能不存就不存,过期就重新授权;native App 存 Keychain / Keystore。
  • id_token 用平台公钥(JWKS)验签,公开客户端也能验,不需要也不应该持有 AppSecret。
js
// 生成 code_verifier(随机串)与 code_challenge(S256)
function randHex(n) {
  const a = new Uint8Array(n);
  crypto.getRandomValues(a);
  return Array.from(a, (b) => ("0" + b.toString(16)).slice(-2)).join("");
}
function b64url(buf) {
  return btoa(String.fromCharCode(...new Uint8Array(buf)))
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
}
async function challengeOf(verifier) {
  const d = await crypto.subtle.digest(
    "SHA-256",
    new TextEncoder().encode(verifier),
  );
  return b64url(d);
}

// 第 1 步:发起授权
async function startAuthorize() {
  const verifier = randHex(48); // code_verifier
  const challenge = await challengeOf(verifier); // code_challenge
  const state = randHex(8);

  // 暂存 verifier / state,回调时使用
  sessionStorage.setItem("oauth_flow", JSON.stringify({ verifier, state }));

  const p = new URLSearchParams({
    response_type: "code",
    client_id: "YOUR_APP_KEY",
    redirect_uri: "https://your-site.com/callback",
    scope: "openid profile",
    state,
    code_challenge: challenge,
    code_challenge_method: "S256",
  });
  location.href = "https://kg.citv.cc/api/oauth/authorize?" + p.toString();
}

// 第 2 步:回调页,code 换 token
async function handleCallback() {
  const q = new URLSearchParams(location.search);
  const code = q.get("code");
  const flow = JSON.parse(sessionStorage.getItem("oauth_flow") || "{}");

  // 校验 state,防 CSRF
  if (q.get("state") !== flow.state) throw new Error("state 不匹配");

  const body = new URLSearchParams({
    grant_type: "authorization_code",
    code,
    redirect_uri: "https://your-site.com/callback",
    client_id: "YOUR_APP_KEY",
    code_verifier: flow.verifier,
    // 公开客户端(AppType=1)不传 client_secret;机密客户端不要走这里,改在服务端换取
  });
  const r = await fetch("https://kg.citv.cc/api/oauth/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body,
  });
  const tok = await r.json();

  // 第 3 步:拉用户信息
  const r2 = await fetch("https://kg.citv.cc/api/oauth/userinfo", {
    headers: { Authorization: "Bearer " + tok.access_token },
  });
  const userinfo = await r2.json();
  console.log("openId:", userinfo.sub);
}

安全提示

  • 机密客户端的 client_secret 只能放服务端,「code 换 token」应由你的后端完成,前端只回传 code。
  • 必须校验 state,防 CSRF。
  • 本方式必须使用 PKCE S256(服务端强制要求)。