客户端接入
使用官方网页组件接入绑定与确认流程,查看组件参数、事件和网站后台对接示例。
AuthLink 生产 API 为 https://api.getauthlink.com;网站业务后台地址仍填写你自己的 API,官网域名不用于接口调用。
JavaScript 快速接入
使用 @auth-link/web,浏览器访问你的网站业务后台;支持默认同源模式及显式配置的跨域模式。租户 API key 仅保存在后台。
npm install @auth-link/web
导入入口为 @auth-link/web、@auth-link/web/react、@auth-link/web/vue 和 @auth-link/web/styles.css。需显式导入样式;支持 React 18.2+ / 19、Vue 3.3+、ESM 和 TypeScript,工具链要求 Node 22.13+。
同源 Cookie / session 初始化
CSRF token 由网站后台为当前登录 session 生成并保存,使用现有框架机制或会话接口提供。下面的 csrfToken 是服务端模板变量,必须正确转义后渲染;不要在浏览器生成随机值代替,也不要放入租户密钥。
<meta name="csrf-token" content="{{ csrfToken }}">
import { createAuthLinkClient } from '@auth-link/web';
import '@auth-link/web/styles.css';
// Read the token rendered by YOUR backend for the current login session.
// This helper is application code, not an SDK export.
export function readCsrfToken(): string {
const token = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content;
if (!token) throw new Error('Missing session CSRF token');
return token;
}
export const client = createAuthLinkClient({
baseUrl: '/api/authlink',
csrfToken: readCsrfToken,
authLinkOrigin: 'https://api.getauthlink.com',
});
readCsrfToken 是示例中的网站自定义函数,不是 SDK 导出。创建和结果接口都必须验证 CSRF 请求头与登录 session;只发送请求头不等于完成校验。登录或会话更新后重新取得 token,含会话 token 的页面不得公开缓存。
在点击事件中绑定或确认
import { openAuthLinkBind, openAuthLinkConfirm, requestKey } from '@auth-link/web';
import type { AuthLinkModal, Mode } from '@auth-link/web';
import { client } from './authlink-client';
let modal: AuthLinkModal | undefined;
let creating = false;
let createKey: string | undefined;
let creation: AbortController | undefined;
// Call startAuthLink('bind') or startAuthLink('confirm') from a button handler.
export async function startAuthLink(mode: Mode) {
if (creating || modal) return;
creating = true;
creation = new AbortController();
try {
createKey ??= requestKey(); // Retain this key if creation fails.
const ticket = await client.create(mode, {
idempotencyKey: createKey, signal: creation.signal,
});
createKey = undefined;
const open = mode === 'bind' ? openAuthLinkBind : openAuthLinkConfirm;
modal = open({
ticket, client, locale: 'zh-CN', variant: 'A',
onConfirmed: result => {
modal = undefined;
console.log('Confirmed in UI:', result.requestId);
},
onRejected: result => { modal = undefined; console.log(result.code); },
onExpired: () => { modal?.destroy(); modal = undefined; },
onClose: () => { modal = undefined; },
onError: error => console.error(error.message),
});
} finally { creating = false; }
}
// Call on page/component teardown. Business authorization stays on the backend.
export function destroyAuthLink() { creation?.abort(); modal?.destroy(); modal = undefined; }
前后端分离与跨域接入
baseUrl 指向网站业务后台,不是 AuthLink 租户 API。跨域时提供绝对 baseUrl 和精确的 allowedBackendOrigins;来源只含协议、主机及端口,不含路径或通配符,也不能从用户输入取得。生产环境使用 HTTPS,本机开发可使用 HTTP 回环地址。authLinkOrigin 单独用于核对二维码中的 AuthLink 服务来源。
跨域 Cookie / session
GET /api/session 是接入网站自己实现的示例接口,需按当前登录 session 返回 csrfToken,设置 Cache-Control: no-store 并启用正确 CORS。请替换为网站实际会话接口。
import { createAuthLinkClient } from '@auth-link/web';
/** /api/session is an example endpoint implemented by your application backend. */
export async function createSessionBackendClient(backendOrigin: string) {
const response = await fetch(`${backendOrigin}/api/session`, {
credentials: 'include', cache: 'no-store', redirect: 'error',
});
if (!response.ok) throw new Error('Session initialization failed');
const session: unknown = await response.json();
if (!session || typeof session !== 'object' || !('csrfToken' in session) ||
typeof session.csrfToken !== 'string' || !session.csrfToken) {
throw new Error('The session endpoint must return { csrfToken: string }');
}
let currentCsrfToken = session.csrfToken;
return {
client: createAuthLinkClient({
baseUrl: `${backendOrigin}/api/authlink`,
allowedBackendOrigins: [backendOrigin],
credentials: 'include',
csrfToken: () => currentCsrfToken,
}),
// Call after your application's session/token refresh; recreate on account changes.
updateCsrfToken(value: string) { currentCsrfToken = value; },
};
}
登录后调用 createSessionBackendClient,向组件传入返回的 client;会话刷新时调用 updateCsrfToken。跨域 Cookie 需要 credentials: include;Cookie 的 SameSite、Secure、domain/path 及浏览器限制仍然生效。退出或切换账号时关闭旧弹窗并重新初始化。
跨域 Bearer / JWT
import { createAuthLinkClient } from '@auth-link/web';
/** auth is your website's existing login state, updated by its login/refresh flow. */
export function createBearerBackendClient(backendOrigin: string, auth: { accessToken: string }) {
return createAuthLinkClient({
baseUrl: `${backendOrigin}/api/authlink`,
allowedBackendOrigins: [backendOrigin],
credentials: 'omit',
accessToken: () => auth.accessToken,
});
}
auth 来自网站已有的登录状态;登录或刷新流程更新 auth.accessToken,回调在每次创建和轮询时读取最新值。此值是网站用户登录 token,绝不能使用 AuthLink 租户 API key。仅在后台只接受 Bearer、没有 Cookie 登录回退且 credentials 为 omit 时才可省略 CSRF;其他模式仍需 CSRF。
业务后台 CORS
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-CSRF-Token, Idempotency-Key, Authorization
Vary: Origin
网站业务后台需处理 OPTIONS 预检,并为会话接口、实际响应及错误响应配置 CORS。携带 Cookie 时不能使用通配符来源,先校验后台允许的来源再返回对应请求头;添加自定义请求头时同步更新允许列表。实际请求仍须验证登录身份、CSRF 和请求归属。SDK 不能代替后台开启 CORS,也不能通过 no-cors 读取结果。
错误码
| 错误码 | 处理方式 |
|---|---|
BACKEND_ORIGIN_NOT_ALLOWED | 目标业务后台不在允许来源中,请检查 allowedBackendOrigins。 |
INVALID_BACKEND_ORIGIN / INVALID_BASE_URL | 检查 baseUrl 是否为有效业务接口前缀;allowedBackendOrigins 中的来源不能含路径或通配符。 |
CSRF_REQUIRED / ACCESS_TOKEN_REQUIRED | 登录凭据或 CSRF token 缺失;重新取得有效会话。 |
RESERVED_HEADER | 不要通过 headers 覆盖 SDK 管理的鉴权及请求头。 |
客户端与弹窗参数
| createAuthLinkClient 参数 | 类型 / 默认 | 说明 |
|---|---|---|
baseUrl | string / /api/authlink | 网站业务后台前缀;跨域时提供完整 URL,不是 AuthLink API 域名。 |
allowedBackendOrigins | readonly string[] / [] | 额外允许的精确业务后台来源,同源仍默认允许。 |
credentials | RequestCredentials | 默认 same-origin;配置 accessToken 时默认 omit;跨域 Cookie 使用 include。 |
csrfToken | () => string | Promise<string> | 每次请求读取当前 session 的 token;Cookie 或混合认证模式必填。 |
csrfHeaderName | string / X-CSRF-Token | 与网站框架实际校验的 CSRF 请求头名称一致。 |
accessToken | () => string | Promise<string> | 每次读取网站用户登录 token,不含 Bearer 前缀,不能传租户密钥。 |
headers | HeadersInit | (() => HeadersInit | Promise<HeadersInit>) | 额外业务请求头或回调,不能覆盖 Accept、Content-Type、Authorization、Idempotency-Key 或 CSRF 头。 |
authLinkOrigin | string / optional | 用于核对 QR 中的 AuthLink 服务来源,与业务后台允许来源分开配置。 |
fetch | typeof fetch / globalThis.fetch | 可选自定义传输;仍执行客户端的目标来源校验。 |
| 弹窗 / 组件参数 | 类型 / 默认 | 说明 |
|---|---|---|
ticket / client
|
Ticket / AuthLinkClient,必填 | 后台创建的请求与网站查询客户端 |
locale
|
Locale / zh-CN | 支持 13 种语言;不支持的值回退简中 |
variant
|
A / B / C,默认 A | A 极简卡片;B 左右分栏;C 紧凑卡片 / 手机底部弹层 |
showQr
|
boolean / false | 确认弹窗是否额外显示 QR;绑定弹窗始终显示 |
intervalMs
|
number / 1500 | 查询间隔(毫秒),范围 250–10000;不延长请求期限 |
onRefresh
|
() => void / 可选 | 过期后由网站重新创建请求、更新 ticket |
onConfirmed / onRejected
|
(result) => void 或 Promise<void> | 确认 / 拒绝后自动关闭;不触发用户关闭事件 |
onExpired / onError
|
() => void;(Error) => void | 过期或查询 / 校验错误 |
onClose
|
() => void | 用户关闭时清理 ticket;destroy() 不触发此回调 |
支持语言:zh-CN, zh-TW, en, ja, bn, es, fil, hi, id, ms, pt, th, vi。取消 / 关闭只停止前端观察,不会替用户拒绝服务端请求。切换语言和布局不会重建请求。
React 与 Vue
复用上方 authlink-client.ts。两个示例均包含按钮、请求创建、结果提示和弹窗清理;mode 为 bind 时绑定,为 confirm 时请求手机确认。
React 完整示例
"use client";
import { useRef, useState } from 'react';
import { requestKey } from '@auth-link/web';
import type { Mode, Ticket } from '@auth-link/web';
import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/react';
import { client } from './authlink-client';
export function AuthLinkPanel({ mode }: { mode: Mode }) {
const [ticket, setTicket] = useState<Ticket | null>(null);
const [busy, setBusy] = useState(false);
const [message, setMessage] = useState('');
const key = useRef<string | null>(null);
const creating = useRef(false);
function finish(message: string) {
setTicket(null);
setMessage(message);
}
function showBound(requestId: string) {
// UI feedback only. The backend has already verified and saved the binding.
finish(`Phone bound (request ${requestId})`);
}
function showConfirmed(requestId: string) {
finish(`Confirmed (request ${requestId})`);
}
const start = async () => {
if (creating.current || ticket) return;
creating.current = true;
setBusy(true);
setMessage('');
try {
key.current ??= requestKey();
setTicket(await client.create(mode, { idempotencyKey: key.current }));
key.current = null;
} catch { setMessage('Request failed; retry the same creation.'); }
finally { creating.current = false; setBusy(false); }
};
const Component = mode === 'bind' ? AuthLinkBind : AuthLinkConfirm;
return <>
{!ticket && <button type="button" disabled={busy} onClick={() => void start()}>Start verification</button>}
{ticket && <Component ticket={ticket} client={client}
onConfirmed={result => result.purpose === 'bind' ? showBound(result.requestId) : showConfirmed(result.requestId)}
onClose={() => finish('Closed')}
onRejected={() => finish('Request rejected on the phone')}
onExpired={() => finish('Request expired; start again')}
onError={error => setMessage(error.message)} />}
<p role="status">{message}</p>
</>;
}
Vue 完整示例
<script setup lang="ts">
import { ref, shallowRef } from 'vue';
import { requestKey } from '@auth-link/web';
import type { Confirmation, Mode, Ticket } from '@auth-link/web';
import { AuthLinkBind, AuthLinkConfirm } from '@auth-link/web/vue';
import { client } from './authlink-client';
const props = defineProps<{ mode: Mode }>();
const ticket = shallowRef<Ticket | null>(null);
const busy = ref(false);
const message = ref('');
let key: string | null = null;
function finish(text: string) { ticket.value = null; message.value = text; }
function confirmed(result: Confirmation) {
finish(`${result.purpose === 'bind' ? 'Bound' : 'Confirmed'} (request ${result.requestId})`);
}
async function start() {
if (busy.value || ticket.value) return;
busy.value = true;
key ??= requestKey();
try {
ticket.value = await client.create(props.mode, { idempotencyKey: key });
key = null;
} catch { message.value = 'Request failed; retry the same creation.'; }
finally { busy.value = false; }
}
</script>
<template>
<button v-if="!ticket" type="button" :disabled="busy" @click="start">Start verification</button>
<AuthLinkBind v-else-if="mode === 'bind'" :ticket="ticket" :client="client"
@confirmed="confirmed" @close="finish('Closed')" @rejected="finish('Rejected on the phone')"
@expired="finish('Request expired; start again')" @error="message = 'Network error; retry this request.'" />
<AuthLinkConfirm v-else :ticket="ticket" :client="client"
@confirmed="confirmed" @close="finish('Closed')" @rejected="finish('Rejected on the phone')"
@expired="finish('Request expired; start again')" @error="message = 'Network error; retry this request.'" />
<p role="status">{{ message }}</p>
</template>
绑定和确认记录由后台验证、消费并保存后,才返回 confirmed。示例中的 showBound 和 finish 仅更新页面并清空 ticket;刷新页面后从后台读取真实绑定状态,不以组件 state 或 localStorage 放行业务。
创建失败保留同一幂等键,重试同一次创建;成功、拒绝、过期或关闭后清空 ticket。网络错误保留当前弹窗供重试。保持 client 和 ticket 稳定;组件卸载会清理轮询。useAuthLink 可用于自定义界面,Vue 的 snapshot 为 shallow ref。
网站后台接口与数据结构
以下接口由接入网站实现。验证 session 或网站 Bearer token,Cookie 模式还需 CSRF;同时验证请求归属与期限,不能作为匿名透传代理。
| 网站后台接口 | 输入 | 职责 |
|---|---|---|
POST /api/authlink/requests
|
body {"purpose":"bind"} 或 confirm;Idempotency-Key;session + CSRF 或网站 Bearer token |
后台从会话取得用户,保存请求上下文并调用平台创建接口,返回公开 ticket |
POST /api/authlink/requests/{id}/result
|
body {};session + CSRF 或网站 Bearer token |
检查所有权,查询 AuthLink;approved / consumed 后幂等消费、校验并持久保存,再返回 confirmed |
type Ticket = {
requestId: string; purpose: 'bind' | 'confirm';
qr: string; pairingCode: string; expiresAt: string;
displayCode?: {
value: string; serverTime: string; validUntil: string;
periodSeconds: 30;
};
};
// Pending: { requestId, status: 'new' | 'pending' | 'approved' |
// 'consumed' | 'rejected' | 'expired', displayCode? }
// Successful result after backend validation and persistence (minimum fields):
type Confirmation = {
status: 'confirmed'; requestId: string; verificationId: string;
purpose: 'bind' | 'confirm';
authenticationPolicy: string; expiresAt: string;
};
// Rejection event: { status:'rejected', code:'REJECTED', requestId, purpose }
原样返回 displayCode,过期数字应隐藏。确认结果由官方 SDK 校验,并与当前请求及网站会话对应。重新挂载组件不能替代后台保存的确认记录。
生命周期与界面规则
- 核对码每 30 秒变化,请求最长 120 秒;数字不是独立 TOTP 登录密码。
- 请求取消 / 组件卸载后清理观察器;成功和拒绝自动关闭,网站需要清空 ticket。
- 前端关闭不撤销请求,后台仍按原期限处理。不要为恢复未知结果不断创建新请求。
- 网页 CSP 需允许
img-src 'self' data:。QR 在浏览器本地生成,不发送第三方图片服务。 - App 删除绑定需显式永久停用确认,删除后网站账号永久不能重绑;覆盖更新不要卸载或清除数据。