Token 管理
auth.token 命名空间负责 JWT 内存缓存与自动续期。v2 与 v1 最大的差异:
- 不再自动 refresh:v1 在
submitCode/submitPassword/ OIDC 成功路径都会内部调一次token.refresh;v2 完全交给业务方在onLoginhandler 里主动调 - 网络抖动不丢 cache:v1
refresh失败会清掉旧 token;v2 保留旧 cache 后再 rethrow,让业务侧决定降级 - 默认 fetchToken 走 ory toSession:业务方可自定义覆盖到自家 BFF 网关
5 个 API
| 方法 | 签名 | 说明 |
|---|---|---|
auth.token.peek | () => string | undefined | 同步读,不发请求;SSR / hydrate / 已知新鲜场景用;缓存空返回 undefined |
auth.token.get | () => Promise<string | undefined> | HTTP 拦截器主入口;过期前 60s 阈值时自动 refresh;新鲜则直接返回 cache |
auth.token.refresh | () => Promise<string | undefined> | 强制拉一次并写 cache;fetchToken 返回 undefined → 清 cache;拿回非法 JWT → 保留旧 cache 返 undefined;网络抛错 → 保留旧 cache 再 rethrow |
auth.token.set | (jwt: string) => void | 直接把外部拿到的 token 写入 cache;解 exp;非法 JWT 静默忽略保留旧 cache(不抛错) |
auth.token.clear | () => void | 清缓存;logout 自动调,业务通常不必 |
续期策略
cache 维护两个值:cached: string | undefined 与 exp: number | undefined(JWT exp 字段,秒级 unix timestamp)。
get() 判断是否新鲜:
function isFresh(): boolean {
if (!cached || exp === undefined) return false;
// 过期前 60s 视作需续,避免请求发出去时 token 刚好过期
return Date.now() < (exp - REFRESH_LEEWAY_SECONDS) * 1000;
}REFRESH_LEEWAY_SECONDS = 60。新鲜则直接返回 cache;不新鲜则调 refresh()。
refresh() 的三种结果:
fetchToken 返回 | refresh 行为 |
|---|---|
| 合法 JWT 字符串 | jwtDecode 解 exp → 写 cache → 返回 token |
undefined(未登录语义) | 清空 cache 与 exp → 返回 undefined |
非法 JWT(无 exp 字段 / decode 抛错) | 保留旧 cache → 返回 undefined |
| 网络抛错 | 保留旧 cache → rethrow |
set(jwt) 的两种结果:
| 输入 | set 行为 |
|---|---|
| 合法 JWT | 写 cache + 解 exp |
| 非法 JWT | 保留旧 cache,不抛错(业务侧塞错 token 时保留可用旧 cache 比清空更安全) |
默认 fetchToken
业务方不传 fetchToken 时 SDK 走默认实现(createDefaultFetchToken):调 ory toSession({ tokenizeAs: 'default_jwt' }),从 session.tokenized 拿 JWT 字符串。
// 等价的伪代码
async function defaultFetchToken(): Promise<string | undefined> {
try {
const session = await api.toSession({ tokenizeAs: 'default_jwt' });
return session.tokenized;
} catch (err) {
// session_inactive / 401 → 视作未登录返回 undefined(不抛错)
if (err.code === 'session_inactive' || /unauthorized|no active session/i.test(err.message)) {
return undefined;
}
throw err;
}
}适用:业务直接吃 ory cookie 的场景(典型:登录后所有 API 都通过 ory cookie 认证 + BFF 透传 JWT)。
自定义 fetchToken
业务有自家 BFF 网关(比如要注入团队 ID / 调试 header / 走非 ory 的鉴权链)时传 fetchToken 覆盖:
const auth = new TripoAuth({
basePath,
fetchToken: async (fetcher) => {
// fetcher 是 SDK 自带 credentials/json header 的 FetchAPI
const res = await fetcher(`${apiUrl}/whoami?tokenizeAs=default_jwt`, {
method: 'GET',
headers: { 'x-team-id': currentTeamId.value },
});
if (!res.ok) {
if (res.status === 401) return undefined; // 未登录
throw new Error(`whoami failed: ${res.status}`);
}
const body = await res.json();
return body.tokenized;
},
});要点:
- 返回
undefined→ SDK 清 cache,视作未登录 - 抛错 → SDK 保留旧 cache,rethrow 给上层
- 必须返回合法 JWT(有
exp字段);不合法时 SDK 保留旧 cache 返回 undefined fetcher参数自带credentials: 'include'+ JSON header;想要更轻量也可以无视fetcher直接用全局fetch
HTTP 拦截器主入口(get())
每次发请求前调一次 auth.token.get():
import ofetch from 'ofetch';
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}`);
}
},
});行为:
- token 新鲜 → 同步 resolve(实际上是 microtask,因为
get签名是 Promise) - token 临期 → 一次 refresh + 写 cache + resolve
- 未登录 → resolve 出 undefined;业务侧应当跳登录页
并发请求场景:N 个请求同时 get(),SDK 内置 in-flight 合并——同一时刻只发起 1 次 fetchToken,所有并发调用共享同一份 Promise 结果。业务侧无需在拦截器外再手工去重。
成功 / 抛错都在 finally 清掉 in-flight 标记;下一波并发 get() 仍可正常发起新的 refresh。
同步判断「是否已登录」(peek())
peek() 不发请求,直接读 cache:
const isLoggedIn = !!auth.token.peek();
if (!isLoggedIn) router.push('/login');适用:路由 guard / SSR hydrate / 同步 UI 状态切换。
注意:peek() 只反映「cache 里有没有 token」。如果业务想要「确认 ory session 仍然活跃」,必须调 refresh()——peek 不知道 cookie 是否已过期、是否被业务清掉。
团队切换示例
业务方切换团队时通常想拿一份「新 team 视角」的 JWT;可以走 refresh()(如果 fetchToken 已经感知 team)或 set(newJwt)(如果业务侧自己已经拿到新 token):
// 模式 A:fetchToken 自动感知 team(如 header x-team-id)
async function switchTeam(teamId: string) {
currentTeamId.value = teamId;
await auth.token.refresh(); // 触发 fetchToken;新 team 的 JWT 写入 cache
}
// 模式 B:业务侧拿到新 JWT 后直接注入
async function switchTeam(teamId: string) {
const res = await api.post('/teams/switch', { teamId });
const newToken: string = res.tokenized;
auth.token.set(newToken); // 同步写 cache,无需再 refresh
}SSR 场景
服务端实例化 TripoAuth 不会有 token:
peek()返回undefinedget()会触发fetchToken调 ory;服务端通常没有 cookie 上下文,会拿到undefined- 即便要做 SSR 透传,建议走
set(jwt)注入:
// Nuxt server middleware:从请求 cookie 拿 ory session → 调 ory toSession 拿 JWT
export default defineEventHandler(async (event) => {
const cookie = getRequestHeader(event, 'cookie');
if (!cookie) return;
// 走 ory 拿 JWT(独立于 SDK,避免共享 fetcher 上的状态)
const jwt = await fetchSessionToken({ basePath, fetchApi: makeForwardingFetch(cookie) });
if (jwt) event.context.tripoToken = jwt;
});
// 客户端 hydrate
const ssrToken = window.__NUXT__.tripoToken;
if (ssrToken) auth.token.set(ssrToken);fetchSessionToken 是 core 顶层导出的工具(不是 auth.token.*):
import { fetchSessionToken } from '@tripo3d/auth';
const jwt = await fetchSessionToken({ basePath, fetchApi: customFetch });
// jwt: string | undefined(未登录时 undefined,不抛错)它内部构造一个临时 FrontendApi 调 toSession({ tokenizeAs: 'default_jwt' });与业务的 TripoAuth 实例完全独立,不共享状态——适合 SSR 一次性取 JWT 的场景。
与 v1 的差异速查
| 行为 | v1 | v2 |
|---|---|---|
| 登录成功后自动续 token | ✓ SDK 内部自动 | ✗ 业务在 onLogin handler 里主动调 |
refresh 网络抖动 | 清旧 cache(业务侧 token 抖没) | 保留旧 cache 后 rethrow |
set 非法 JWT | 抛错 | 静默忽略,保留旧 cache |
构造参数 token: string | 支持 SSR 直接注入 | 已删;统一走 token.set(jwt) 注入 |
fetchSessionToken | 不存在 | 顶层工具,独立于 TripoAuth 实例 |
错误处理
auth.token.* 各方法的失败行为:
peek()— 不抛错;缓存空返回undefinedget()— 新鲜路径不抛错;refresh 路径透传 fetchToken 抛的错(含TripoAuthError(name: 'FLOW' | 'NETWORK'));保留旧 cacherefresh()— 同get()的 refresh 路径;保留旧 cacheset(jwt)— 不抛错;非法 JWT 静默忽略clear()— 不抛错
业务通常在拦截器层兜:
try {
const token = await auth.token.get();
// ...
} catch (err) {
// 网络异常路径:保留旧 cache,业务可继续用 peek() 拿到的旧 token 跑请求
const fallback = auth.token.peek();
if (fallback) return fallback;
// 真的没 token 了:跳登录
router.push('/login');
}下一步
- 架构总览
- 登录信号 (cookie) —
onLoginhandler 是 v2 主动调refresh的入口 - 错误处理 —
TripoAuthError完整分类