Skip to content

验证码流程 ​

验证码(OTP)是 Tripo 业务侧最推荐的登录入口——一套表单同时覆盖已注册用户登录与新用户注册。v2 SDK 把它收敛成两个对称方法 + 一个透传字段:

  • auth.login.sendCode({ email }) / auth.login.sendCode({ phone }) — 发码 + 自动分流(identifier-first)
  • auth.login.submitCode({ code, email }, { params? }) / auth.login.submitCode({ code, phone }, { params? }) — 提交验证码完成登录或注册
  • LoginParams — 可选透传字段;submitCode 成功后写入登录信号 cookie,onLogin handler 拿到

sendCode / submitCode 通过重载同时支持邮箱与手机号两条并列签名:传 { email } 走邮箱、传 { phone } 走手机号,二选一即可,调用方无需额外开关。手机号为完整号码串(如 +8613800138000),区号由调用方拼接,SDK 国家中立——详见下文 手机号验证码。

底层走 ory method: 'code',SDK 内部维护「当前活跃的验证码会话」串起 sendCode 与 submitCode。业务侧不需要手动管 flowId / csrf_token,也不需要为登录/注册各搞一套表单。

最短链路 ​

ts
import { TripoAuth, TripoAuthError } from '@tripo3d/auth';

const auth = new TripoAuth({
  basePath: 'https://auth.tripo3d.ai',
});

// 1. 发码:SDK 内部 checkIdentifier 决定走 login 还是 registration,并创建对应 ory flow + 发码
await auth.login.sendCode({ email: 'user@example.com' });

// 2. 提交验证码:复用 sendCode 缓存的 flow id + csrf;params 透传到 onLogin handler
await auth.login.submitCode(
  { code: '123456', email: 'user@example.com' },
  { params: { fromBanner: 'home_v2' } },
);

// 3. 成功 → SDK 写 __tripo_login_signal__ cookie;同根域的一个实例认领并触发 onLogin

注:v2 的 sendCode 不再返回 kind——「当前 identifier 走 login 还是 registration」属于 SDK 内部细节,业务方按统一 UI 文案处理即可(即便要切换 UI,建议改成 UI 层独立调 /idp-ext/auth/check-identifier,避免双重职责)。

设计要点 ​

1. sendCode → submitCode 是有状态序列 ​

sendCode 内部创建 ory login 或 registration flow,把 { flow, csrf_token, kind } 写入实例级闭包(CodeSessionHolder)。submitCode 复用同一份缓存提交。

[sendCode]
  probeFlowType(email)  ─►  'login' | 'registration'
  ory createBrowser{Login|Registration}Flow → flow id + ui.nodes
  ory updateFlow(method='code', identifier)  → 邮件下发
  holder.set({ flow, csrf_token, kind })
[submitCode]
  holder.get()  ─►  cached
  ory updateFlow(method='code', identifier, code)  → 通过
  writeSignal({ provider: 'code', ts, params })
  holder.clear()

注意:

  • 两次调用必须在同一个 TripoAuth 实例上完成,跨实例 holder 不通
  • 提交成功后 SDK 自动清掉 holder;再发码只需再调一次 sendCode
  • 多实例 / SSR 并发场景下 holder 互不串扰

2. 邮箱变更要重新 sendCode ​

业务 UI 通常监听邮箱输入框,一旦值变更就清掉「已发码」状态,并 disable 提交按钮直到用户重新发码:

ts
import { ref, watch } from 'vue';

const email = ref('');
const codeSent = ref(false);

watch(email, () => {
  codeSent.value = false;   // 标记需要重新发码
});

async function onSendCode() {
  await auth.login.sendCode({ email: email.value });
  codeSent.value = true;
  startCountdown(60);   // 业务 UI 自己的发码冷却
}

如果用户改完邮箱直接调 submitCode,SDK 仍会用旧 flow 提交——ory 会以「identifier 不匹配」拒绝并抛 TripoAuthError(name: 'FLOW')。

3. params 透传到 onLogin handler ​

submitCode 第二参的 params 字段以 LoginParams(即 unknown)类型透传:JSON 化后塞进登录信号 cookie 的 payload,onLogin handler 拿到的 LoginSignal.params 即原值:

ts
// 触发端
await auth.login.submitCode(
  { code, email },
  { params: { source: 'landing-v3', utm_campaign: 'spring-sale' } },
);

// 订阅端(可能是同 tab,也可能是其他子域 tab)
auth.signal.onLogin(({ signal, context }) => {
  console.log(signal.provider);   // 'code'
  console.log(signal.params);     // { source: 'landing-v3', utm_campaign: 'spring-sale' }
  console.log(context.isRedirect); // 业务页冷启路径 = true,popup/dialog 路径 = false
});

params 不会写入 ory traits、不会被 SDK 解析;纯透传,不要依赖它做安全决策。编码失败(非 JSON-serializable 输入)会被静默丢弃,信号仍会发出去。

4. 自动分流的判定与降级 ​

sendCode 第一步是调业务端 ${basePath}/idp-ext/auth/check-identifier:

ts
POST /idp-ext/auth/check-identifier
Body: { identifier: 'user@example.com' }
Response: { success: true, data: { flow_type: 'login' | 'registration' } }

fetch 失败 → TripoAuthError(name: 'NETWORK');HTTP 非 2xx 或返回 success !== true → TripoAuthError(name: 'FLOW')。业务侧通常按 error.name 分流(详见 错误处理)。

手机号验证码 ​

sendCode / submitCode 是重载方法,把入参从 { email } 换成 { phone } 即切到手机号链路,其余行为(自动分流、会话复用、params 透传、错误模型)与邮箱完全一致:

ts
// 区号由调用方拼接,SDK 收到的是完整号码串(国家中立)
const fullPhone = `+86${'13800138000'}`;

// 1. 发码:内部 check-identifier 用手机号判定 login / registration
await auth.login.sendCode({ phone: fullPhone });

// 2. 提交验证码:登录走 identifier、注册走 traits.phone(SDK 自动处理)
await auth.login.submitCode(
  { code: '123456', phone: fullPhone },
  { params: { source: 'phone-login' } },
);

// 3. 成功 → 同样写 __tripo_login_signal__ cookie(provider 仍为 'code');onLogin 被触发

要点:

  • 区号在调用方拼:SDK 不内置任何国家码逻辑,{ phone } 必须是完整号码串。UI 套件固定 +86(仅中国大陆),自建表单可自行决定。
  • 手机号只有验证码链路:没有 submitPassword({ phone });密码登录仅邮箱。
  • 后端前置:ory identity schema 需含 phone trait 且开放 code method,/idp-ext/auth/check-identifier 需接受手机号。
  • 邮箱与手机号是互斥的两条入口;不要在同一次 sendCode → submitCode 序列里混用(identifier 不一致 ory 会拒绝)。

重载 vs 联合

{ email } 调用方完全不受影响(签名向后兼容)。注意 TypeScript 重载不接受「联合实参一次命中不同重载」,所以调用时要按身份用具体形状分别调用(sendCode({ phone }) 而非 sendCode(emailOrPhone)),这也正好让两条链路在调用点一一对照。

重试与重发 ​

验证码错误 ​

submitCode 失败时不自动清缓存——业务直接让用户改输入再调一次:

ts
try {
  await auth.login.submitCode({ code: input, email });
} catch (error) {
  if (error instanceof TripoAuthError && error.name === 'FLOW') {
    toast.error(error.message);
    // 用户改输入后再调 submitCode 即可,无需 sendCode
  }
}

验证码过期 ​

Ory 默认 5 分钟过期;过期后 submitCode 会抛 TripoAuthError(name: 'FLOW'),业务侧需要重新 sendCode:

ts
async function resend() {
  await auth.login.sendCode({ email });
  startCountdown(60);
}

没先发码就提交 ​

ts
await auth.login.submitCode({ code, email });
// TripoAuthError(name: 'VALIDATION', message: 'submitCode requires a prior sendCode call in the same session')

业务侧通常用 UI 状态卡住「未发码就 disable 提交按钮」,触发这条错误说明 UI 状态出 bug 了。

频率限制 ​

Ory 后端配置了发码频率(默认 30s 一次)。前端短时间连续 sendCode 会拿到 TripoAuthError(name: 'FLOW'),error.message 透传 ory 的限流文案:

ts
try {
  await auth.login.sendCode({ email });
} catch (error) {
  if (error instanceof TripoAuthError && /too many|rate/i.test(error.message)) {
    toast.error('发送过于频繁,请稍后再试');
  }
}

建议 UI 上显示倒计时按钮,前端先卡一道,避免无效请求。

端到端示例(Vue + UI 层) ​

下面这段不依赖 UI 子路径,业务方自己渲染表单 + 调 core API:

vue
<script setup lang="ts">
import { ref } from 'vue';
import { TripoAuthError } from '@tripo3d/auth';
import { auth } from '~/plugins/auth';

const email = ref('');
const code = ref('');
const codeSent = ref(false);
const cooldown = ref(0);
const submitting = ref(false);

// onLogin handler 在 app 启动时已统一注册;本组件只负责表单
async function onSendCode() {
  try {
    await auth.login.sendCode({ email: email.value });
    codeSent.value = true;
    startCooldown(60);
  } catch (err) {
    handleAuthError(err);
  }
}

async function onSubmit() {
  if (!codeSent.value) return;
  submitting.value = true;
  try {
    await auth.login.submitCode(
      { code: code.value, email: email.value },
      { params: { source: 'inline-form-v1' } },
    );
    // 不需要在这里跳转:onLogin handler 已经在 app 级别统一处理 token.refresh + initUser + 关 dialog
  } catch (err) {
    handleAuthError(err);
  } finally {
    submitting.value = false;
  }
}

function handleAuthError(err: unknown) {
  if (err instanceof TripoAuthError) {
    toast.error(err.message);
    return;
  }
  toast.error('网络异常,请稍后重试');
}

function startCooldown(seconds: number) {
  cooldown.value = seconds;
  const t = setInterval(() => {
    cooldown.value -= 1;
    if (cooldown.value <= 0) clearInterval(t);
  }, 1000);
}
</script>

<template>
  <form @submit.prevent="onSubmit">
    <input v-model="email" type="email" placeholder="邮箱" required>
    <div>
      <input v-model="code" type="text" inputmode="numeric" placeholder="验证码" maxlength="6">
      <button type="button" :disabled="cooldown > 0 || !email" @click="onSendCode">
        {{ cooldown > 0 ? `${cooldown}s` : '发送验证码' }}
      </button>
    </div>
    <button type="submit" :disabled="!codeSent || submitting">
      登录
    </button>
  </form>
</template>

API 参考 ​

方法签名说明
auth.login.sendCode(args: { email } | { phone }) => Promise<void>(重载)发码 + 自动分流;内部先调 /idp-ext/auth/check-identifier 决定走 login 还是 registration。phone 为完整号码串(区号调用方拼)
auth.login.submitCode(args: { code, email } | { code, phone }, options?: { params? }) => Promise<void>(重载)提交验证码完成登录或注册;成功后写 __tripo_login_signal__ cookie(provider=code)

submitCode 的 options ​

字段类型说明
paramsLoginParams (= unknown)透传到 onLogin handler;JSON-serialize 后写入信号 cookie payload;编码失败静默丢弃

常见错误 ​

完整错误分类见 错误处理。验证码流程常见的 error.name:

  • FLOW — 验证码错 / 过期 / identifier 不匹配 / 频控(业务直接 toast error.message)
  • NETWORK — check-identifier 或 ory flow 调用 fetch 抛错
  • VALIDATION — submitCode 在没先 sendCode 的情况下被调用
  • INTERNAL — flow ui 节点里取不到 csrf_token(理论上 ory 不应出现)

下一步 ​

基于 MIT 协议发布(内部使用)