网页(Web)接入

网页应用有两种:有自己后端的站点(机密客户端,密钥只放服务端)和 纯前端 SPA(公开客户端,无密钥,必须用 PKCE)。两者都用标准授权码模式,用户在 open.jintuzi.com 的授权页上登录并同意。

1. 两种模式怎么选

你的形态客户端类型换 token 的位置PKCE
有后端(PHP / Java / Node / Go…)confidential 机密后端(带 client_secret)可选,建议开
纯前端(Vue / React / 静态站点)public 公开浏览器(无密钥)强制

2. 回调地址要求

  • 必须是 https://,且与后台登记的回调地址逐字符一致(不会做前缀或通配匹配);
  • 本地联调可用 http://localhost:端口/oauth/callback,上线前替换成正式域名;
  • 回调只用来接 code,不要在里面做业务写入。

3. 授权(跳转,不要用 iframe)

平台页面禁止被第三方 iframe 嵌入(frame-ancestors 'self'),请用整页跳转或弹窗:

const params = new URLSearchParams({
  response_type: 'code',
  client_id: 'YOUR_CLIENT_ID',
  redirect_uri: 'https://your-site.com/oauth/callback',
  scope: 'user:avatar user:nickname',
  state: crypto.randomUUID(),
});
// 机密客户端:无需 PKCE;SPA:再带上 code_challenge / code_challenge_method=S256
location.href = `https://open.jintuzi.com/oauth/authorize?${params}`;

4. 用 code 换 token

有后端(机密客户端,示例 Node/Express)

// 服务端:code 只在这里出现,密钥不下发到浏览器
const r = await fetch('https://open.jintuzi.com/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: req.query.code,
    redirect_uri: 'https://your-site.com/oauth/callback',
    client_id: process.env.JZ_CLIENT_ID,
    client_secret: process.env.JZ_CLIENT_SECRET,
  }),
});
const { access_token, refresh_token } = await r.json();

纯前端 SPA(公开客户端,PKCE)

// 1) 生成 verifier,存 sessionStorage(不要 localStorage)
const verifier = [...crypto.getRandomValues(new Uint8Array(32))]
  .map(b => b.toString(16).padStart(2, '0')).join('');
sessionStorage.setItem('pkce_verifier', verifier);
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
const challenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
  .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');

// 2) 授权时带上 code_challenge / code_challenge_method=S256
//    回调页取出 code 后:
const r = await fetch('https://open.jintuzi.com/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code,
    redirect_uri: 'https://your-site.com/oauth/callback',
    client_id: 'YOUR_CLIENT_ID',
    code_verifier: sessionStorage.getItem('pkce_verifier'),
  }),
});
const { access_token } = await r.json();   // 该接口已开 CORS,可直接跨域调用

5. 取用户资料

const me = await fetch('https://open.jintuzi.com/oauth/userinfo', {
  headers: { Authorization: `Bearer ${access_token}` },
}).then(r => r.json());
// { openid, nickname, avatar, jintuzi_no, ... } —— 只返回用户授权过的字段

6. 跨域(CORS)

以下接口已开放跨域调用(Access-Control-Allow-Origin: *,不使用 Cookie,凭证走 Authorization 头),第三方网页可直接 fetch:

  • /oauth/token、/oauth/revoke、/oauth/introspect
  • /oauth/userinfo
  • /.well-known/oauth-authorization-server

授权页 /oauth/authorize 不开跨域(它依赖平台登录 Cookie),只能整页跳转打开。

7. 前端安全清单

  • SPA 只放 client_id,绝不能在前端出现 client_secret;
  • code_verifier 放 sessionStorage,用完即删;
  • access_token 优先放内存,页面刷新后用 refresh_token 换新;
  • 回调页校验 state 与发起时一致,防 CSRF;
  • 不要把 token 拼进 URL、不要通过 postMessage 发给任意来源。

8. 服务端校验(可选)

curl -X POST https://open.jintuzi.com/oauth/introspect \
  -d "token=ACCESS_TOKEN" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"