5 分钟接入
本页给出 @tripo3d/auth v2 的最小可跑接入路径:从安装到「业务请求拦截器读 token」5 步走,整张表单 + OIDC 全走 showLogin 一键唤起。读完即可上手;细节继续看 架构总览 与各分章。
0. 前置依赖
- 浏览器环境(SSR 场景另见 Token 管理 的 SSR 段)
- 业务侧已在根上挂载
@tripo3d/design的<DialogOpener />(showLogin内部依赖) - 已完成 GitHub Packages 认证配置
1. 安装
pnpm add @tripo3d/auth1.5 宿主构建集成(必做)
@tripo3d/auth/ui 组件不自带 CSS,所有样式都是 UnoCSS 原子类,靠宿主 Vite + UnoCSS pipeline 在 dev/build 时扫描 node_modules/@tripo3d/auth/dist/**/*.uno.js 生成。
而 Vite 默认会用 esbuild 预打包 node_modules 依赖(optimizeDeps),auth 一旦被预打包就绕过 UnoCSS transform → 原子类生成不出来 → dev 模式登录 UI 全无样式。
Nuxt 使用 @tripo3d/auth/nuxt 模块:它会自动挂载 tripoAuth();Google GSI 默认由 SDK 在实际有 googleClientId 的登录容器按需加载,避免每个 SSR 页面都请求第三方脚本。只有首屏确定需要 One Tap/GSI 的站点才应显式开启 SSR 预热。纯 Vite 使用 @tripo3d/auth/vite 插件。两者都会把 auth 排除出预打包(走 transform → 被 UnoCSS 扫到),同时把 yup CJS 依赖链强制预打包。
export default defineNuxtConfig({
modules: ['@tripo3d/auth/nuxt'],
// GSI 默认在 SSR head 预热;确定用不到 Google 登录时置 false 关闭。
// Vite 集成不受此开关影响。
// tripoAuth: { preloadGoogleGsi: false },
});import { defineConfig } from 'vite';
import { tripoAuth } from '@tripo3d/auth/vite';
export default defineConfig({
plugins: [tripoAuth()],
});不挂这个插件的两个典型症状:
- dev 模式登录页 / 弹窗无样式(核心症状,根因:auth 被预打包后原子类没扫到)
- 若自己手写
optimizeDeps.exclude: ['@tripo3d/auth'],会报tiny-case does not provide an export named 'camelCase'—— 这是 yup CJS 依赖被一起带出预打包的副作用,正是tripoAuth()内部处理的事情
宿主不要再自己手写 optimizeDeps.exclude / include,全部交给插件。
2. 实例化 TripoAuth
业务侧把 TripoAuth 当成单例 —— 在 plugin / composable 里建一次,整个 SPA 复用。showLogin 第一参直接传实例;独立登录页用 :auth prop 显式传。无需 provide/inject。
import { TripoAuth } from '@tripo3d/auth';
// 整个 SPA 生命周期复用同一个 auth 实例
export const auth = new TripoAuth({
basePath: 'https://auth.tripo3d.ai',
// 可选:设备指纹,构造时传一次即自动随所有登录/注册/重置/改密请求上报(详见 §2.5)
deviceId: getDeviceId(),
});import { TripoAuth } from '@tripo3d/auth';
export default defineNuxtPlugin(() => {
// 服务端跳过实例化,避免 SSR 期间触发浏览器 API
if (import.meta.server) return;
const auth = new TripoAuth({
basePath: 'https://auth.tripo3d.ai',
deviceId: getDeviceId(), // 可选:设备指纹,见 §2.5
});
return { provide: { auth } };
});2.5 透传设备指纹 / 业务临时数据(transient_payload)
需要把 设备指纹 device_id 或风控等业务临时数据带给 Ory 后端 / webhook(用于设备绑定、风控判定)时,SDK 把它们统一注入 Ory 标准字段 transient_payload——仅本次提交对后端 / webhook 可见、不落库到 identity traits,正适合这类临时数据。
两个来源、两个时机:
| 来源 | 时机 | 形态 | 适用 |
|---|---|---|---|
deviceId | 构造期(new TripoAuth) | string | 设备级静态值,传一次后全局自动带上 |
options.transientPayload | 调用期(每个动作方法) | Record<string, unknown> | 某次调用按需附加的额外字段 |
二者在每次请求前合并为 transient_payload = { ...transientPayload, device_id: deviceId }(deviceId 优先),两者都不传则请求体不含该字段(向后兼容)。
覆盖范围:登录 / 注册 / 重置 / 改密链路的全部底层提交——login.sendCode / submitCode / submitPassword / startOidc、reset.sendCode / verifyCode / submitNewPassword、settings.sendVerifyCode / verifyCode / updatePassword。
// 构造期:device_id 全局自动带
const auth = new TripoAuth({ basePath, deviceId: getDeviceId() });
// 调用期:某次提交再附加业务字段(与 device_id 合并上报)
await auth.login.submitPassword(
{ email, password },
{ transientPayload: { invite_code: 'X8K2', scene: 'home_banner' } },
);
// 实际提交的 transient_payload = { invite_code, scene, device_id }验证:浏览器 DevTools Network 里看
updateLoginFlow等请求体的transient_payload字段;或在 Playground 顶部改deviceId、在「验证码登录」卡里填transientPayloadJSON,对照请求体观察合并结果。
3. 注册 onLogin handler
登录成功后 SDK 写一份 __tripo_login_signal__ cookie,业务侧用 onLogin(handler) 订阅;handler 拿到 { signal, context } payload 后通常做:拉新用户信息(SDK 在派发前已 await token.refresh() 兜底)、按 context.isRedirect 区分冷启路径:
import type { LoginPayload } from '@tripo3d/auth';
const unsubscribe = auth.signal.onLogin(async ({ signal, context }: LoginPayload) => {
// SDK 已在派发前确保 token 就绪;保留 refresh 作为容错(内部并发去重,不会重复打后端)
await auth.token.refresh();
await fetchCurrentUser();
console.log('logged in via', signal.provider, 'isRedirect=', context.isRedirect, signal.params);
});
// 组件卸载时调 unsubscribe();app 级订阅可以不卸handler 不需要担心被同一个信号触发多次——SDK 在 cookie 命中后立即调用 clearSignal,保证「一次信号一次处理」。
4. showLogin 唤起登录
业务任意位置一键弹出登录对话框,邮箱/密码 + OIDC 全在同一个 dialog 里完成:
<script setup lang="ts">
import { showLogin } from '@tripo3d/auth/ui';
import { auth } from '~/plugins/auth';
function onClickLogin() {
// showLogin 返回 void —— 登录完成统一靠 onLogin handler 收尾,dialog 内部错误自己 toast
showLogin(auth, {
completeUrl: 'https://www.tripo3d.ai/login/oidc', // 必填:OIDC 完成页绝对 URL
locale: useI18n().locale.value, // 可选:宿主当前语言;缺省回落 'en'
params: { fromBanner: 'home_v2' },
});
}
</script>
<template>
<button @click="onClickLogin">Sign in</button>
</template>showLogin 不返回任何东西——无论 dialog 自己关闭、用户取消,还是 redirect 整页跳走,登录完成的唯一消费路径都是业务方注册的 auth.signal.onLogin(handler)。务必先注册 handler 再唤起登录。
5. 业务请求拦截器读 token
每次发请求前调 auth.token.get(),过期前 60s SDK 自动续期,业务无感知:
import ofetch from 'ofetch';
import { auth } from '~/plugins/auth';
export const apiClient = ofetch.create({
baseURL: 'https://api.tripo3d.ai',
async onRequest({ options }) {
const token = await auth.token.get();
if (token) {
options.headers = new Headers(options.headers);
options.headers.set('Authorization', `Bearer ${token}`);
}
},
});同步判断「是否已登录」(不发请求)用 auth.token.peek():
const isLoggedIn = !!auth.token.peek();端到端最小示例
把上面 5 步串起来,得到一个端到端最小可跑示例:
import { TripoAuth } from '@tripo3d/auth';
import { showLogin } from '@tripo3d/auth/ui';
const auth = new TripoAuth({
basePath: 'https://auth.tripo3d.ai',
});
// 1. 全局 onLogin handler,处理所有登录路径的收尾
// SDK 已在派发前 await token.refresh() 兜底,handler 跑起来 token 直接可用
auth.signal.onLogin(async ({ signal, context }) => {
await fetchCurrentUser();
});
// 配套:订阅退出事件(由 auth.logout() 派发,token 已 clear)
auth.signal.onLogout(() => clearLocalUserStore());
// 2. 唤起登录(showLogin 返回 void,业务收尾全部交给上面的 onLogin handler)
function login() {
showLogin(auth, { completeUrl: 'https://www.tripo3d.ai/login/oidc' });
}
// 3. 退出登录
async function logout() {
await auth.logout(); // 内部清 signal cookie + token cache
router.push('/');
}
// 4. 全局请求 token
async function authHeader() {
const token = await auth.token.get();
return token ? { Authorization: `Bearer ${token}` } : {};
}下一步
- 架构总览 —
TripoAuth与 6 个命名空间全景 - 登录信号 (cookie) — 理解
onLogin的双通道与跨子域行为 - UI 子路径 /ui — 自定义登录页 / dialog
- Token 管理 — 自定义
fetchToken/ SSR 注入 - 错误处理 —
TripoAuthError5 类 name 分流模板 - Playground