JavaScript SDK

JavaScript SDK 集成指南,支持浏览器 OAuth 2.1 + PKCE 流程。

安装

UM JS SDK 是单文件 SDK,直接 <script> 引入即可:

<script src="https://um.yunjii.cn/sdk/um-sdk.js"></script>

初始化

<script>
UM.config.appid = 'your_appid';
UM.config.appkey = 'your_appkey';
UM.config.redirect_uri = 'https://your-app.com/callback.html';
</script>

浏览器端 appkey 会暴露在前端代码中。生产环境务必在 BFF 层(如 Next.js API Routes)调用需要签名的接口(token / refreshToken),前端只调无需签名的接口(authorizeUrl / qrcodeUrl)。

OAuth 2.1 + PKCE 流程(浏览器端)

1. 生成授权 URL + 跳转

async function login() {
  const state = 'state_' + Date.now() + '_' + Math.random().toString(36).slice(2);
  const verifier = UM.generateCodeVerifier();
  const challenge = await UM.generateCodeChallenge(verifier);

  sessionStorage.setItem('um_pkce_state', state);
  sessionStorage.setItem('um_pkce_verifier', verifier);

  window.location.href = UM.authorizeUrl(state, challenge);
}

2. 回调页处理(code 换 token)

推荐通过 BFF 后端换取 token(避免 appkey 暴露)。下方为前端直连示例,仅适合 demo / 内部工具。

// 回调页(URL 带 ?code=xxx&state=xxx 时自动执行)
async function handleCallback() {
  const params = new URLSearchParams(location.search);
  const code = params.get('code');
  const state = params.get('state');

  // 防 CSRF
  if (state !== sessionStorage.getItem('um_pkce_state')) {
    throw new Error('State mismatch');
  }

  // 用 code + code_verifier 换 token(⚠️ 建议走 BFF 后端)
  const verifier = sessionStorage.getItem('um_pkce_verifier');
  const tokenRes = await UM.token(code, verifier);

  if (tokenRes.code !== 0) {
    throw new Error('Token error: ' + tokenRes.msg);
  }

  // 存储 token
  localStorage.setItem('um_access_token', tokenRes.access_token);
  localStorage.setItem('um_refresh_token', tokenRes.refresh_token);

  // 清理 PKCE 临时数据
  sessionStorage.removeItem('um_pkce_state');
  sessionStorage.removeItem('um_pkce_verifier');

  // 跳转登录后页面
  location.href = '/dashboard';
}

if (location.search.includes('code=')) {
  handleCallback();
}

3. 获取用户信息

const accessToken = localStorage.getItem('um_access_token');
const user = await UM.userinfo(accessToken);
console.log(user);
// => { sub: 'um_xxx', name: '张三', nickname: '张三', picture: '...', login_type: 'wx', ... }

4. 刷新 Token

const refreshToken = localStorage.getItem('um_refresh_token');
const newToken = await UM.refreshToken(refreshToken);
// ⚠️ 旧 refresh_token 已失效,必须更新
localStorage.setItem('um_access_token', newToken.access_token);
localStorage.setItem('um_refresh_token', newToken.refresh_token);

扫码登录(二维码图片模式)

适合 PC 端,直接渲染二维码图片,无需跳转:

<div id="um-login"></div>
<script>
UM.init({
  mode: 'qrcode',
  appid: 'your_appid',
  appkey: 'your_appkey',
  element: '#um-login',
  type: 'wx',
  redirect_uri: 'https://your-app.com/callback.html',
  onLogin: function(res) {
    location.href = '/dashboard';
  }
});
</script>

支持的 modeqrcode(推荐)/ redirect / popup / embed(不推荐)。

Node.js / Next.js BFF(推荐)

生产环境应在后端处理 token 交换,避免 appkey 暴露:

// app/api/auth/callback/route.ts
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const code = searchParams.get('code')!;
  const verifier = cookies().get('um_pkce_verifier')?.value;

  // 服务端调用 UM token 端点(appkey 安全)
  const tokenRes = await fetch('https://um.yunjii.cn/oauth/token.php', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      client_id: process.env.UM_APPID!,
      client_secret: process.env.UM_APPKEY!,
      redirect_uri: process.env.UM_CALLBACK!,
      code_verifier: verifier,
    }),
  }).then(r => r.json());

  if (tokenRes.code !== 0) {
    return NextResponse.redirect(new URL('/login?error=token', request.url));
  }

  // 存储 token(httpOnly cookie)
  cookies().set('um_access_token', tokenRes.access_token, {
    httpOnly: true, secure: true, sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 7,
  });
  cookies().set('um_refresh_token', tokenRes.refresh_token, {
    httpOnly: true, secure: true, sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 30,
  });

  return NextResponse.redirect(new URL('/dashboard', request.url));
}

错误处理

try {
  const token = await UM.token(code, verifier);
  if (token.code !== 0) {
    console.error('errcode:', token.errcode, 'msg:', token.msg);
  }
} catch (err) {
  console.error('Network error:', err);
}

相关文档