Skip to content

验证码流程

验证码(OTP)是 password 之外的另一条统一路径,适用于:

  • 登录 — 已存在用户用一次性验证码替代密码
  • 注册 — 新用户(尤其是手机号)用验证码完成注册
  • 验证 — 已登录用户校验新绑定的邮箱 / 手机

底层都是 Kratos method: 'code',SDK 把它拆成两步对称的子方法:

  • send(traits) — 让 Kratos 把验证码发到 email / phone
  • code(traits + code)verify(code) — 提交验证码完成本次 flow

三类 flow 的子方法名略有差异:登录 / 注册用 code(),verification 用 verify()。下面分别说明。

登录(验证码)

适合「记不住密码」或「无密码登录」场景。先发码,再用同一个 loginFlow 提交验证码:

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

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

const loginFlow = await auth.createLoginFlow();

// 1. 发码:Kratos 校验 identifier 已存在,然后下发邮件 / 短信
await loginFlow.send({ email: 'a@b.c' });

// 2. 用户输入收到的验证码
await loginFlow.code({ email: 'a@b.c', code: '123456' });
// 成功后 SDK 自动 initTokenized(),token 已可用

手机号同样支持:

ts
await loginFlow.send({ phone: '+8613800138000' });
await loginFlow.code({ phone: '+8613800138000', code: '123456' });

sendcode 必须用同一个 loginFlow 实例——它内部持有 flowIdcsrf_token,跨方法共享。重新 createLoginFlow() 会得到新的上下文,验证码就对不上了。

注册(验证码)

新用户走 createSignupFlow(),签名与登录对称。手机号注册必须走这条路径(密码方式被后端拒,见密码流程):

ts
const signupFlow = await auth.createSignupFlow();

// 1. 发码到手机号
await signupFlow.send({ phone: '+8613800138000' });

// 2. 提交验证码完成注册;traits 写入新 identity
await signupFlow.code({
  phone: '+8613800138000',
  code: '123456',
});
// 注册即登录,token 已写入缓存

邮箱注册同理:

ts
await signupFlow.send({ email: 'newuser@b.c' });
await signupFlow.code({ email: 'newuser@b.c', code: '123456' });

与登录的关键区别

名称 类型 默认值 说明
identity新建 / 已存在signup 要求 identifier 不存在,login 要求 identifier 存在;Kratos 会在 send 阶段校验
traits 写入send + code 双重传入signup 的 send 第一参数和 code 第一参数都要带 email/phone,Kratos 用它确定要为谁建账号
后续 session同样自动建立signup 完成自动 login,与密码注册一致

已登录态校验邮箱 / 手机

用户在 settings 页改了邮箱后,Kratos 通常要求重新验证新邮箱。这个动作不属于 login / signup,而走单独的 verification flow:

ts
// 前置:用户已登录,且通过 settings flow 把邮箱改成了 new@b.c
const verificationFlow = await auth.createVerificationFlow();

// 1. 发码到新邮箱
await verificationFlow.send({ email: 'new@b.c' });

// 2. 提交验证码;注意子方法叫 verify 而不是 code
await verificationFlow.verify({ email: 'new@b.c', code: '123456' });
// SDK 自动 initTokenized() 刷新 session(traits 状态变化)

何时使用

名称 类型 默认值 说明
改邮箱后场景通过 settings 修改 traits.email 后,新邮箱默认未验证,需 verification flow 标记为已验证
改手机号后场景同上,phone 字段独立,改了就要重新发码验证
账号绑定双因子场景某些功能要求 verified=true 才能开启

createVerificationFlow() 不要求用户调过登录——只要 Kratos session cookie 在,Kratos 就允许创建。

重试与失败后再发

handleFlow 内部会从响应里提取最新的 flowIdcsrf_token,就地更新到工厂闭包持有的 state 上。所以同一个 flow 实例可以反复重试,无需重建:

ts
const loginFlow = await auth.createLoginFlow();
await loginFlow.send({ email });

try {
  await loginFlow.code({ email, code: '000000' });
}
catch (error) {
  // 验证码错,提示用户重新输入
  const e = error as Error;
  console.warn(e.message); // 「The provided code is invalid」

  // 直接再调即可,无需 createLoginFlow()
  await loginFlow.code({ email, code: realCode });
}

如果用户希望重新发码(原码失效或没收到),也只调 send 即可:

ts
// 同一个 loginFlow,直接再次发码
await loginFlow.send({ email });

Kratos 会在 server 端控制频率,前端不必额外节流。但建议在 UI 上加倒计时,避免用户连点。

transient_payload 用法

所有 send / code / verify / password 方法都接受可选第二参数 transient_payload——一个普通对象,Kratos 会把它原样转发到配置的 webhook,不写入 identity traits。常用于:

  • 注册来源({ source: 'landing-page-a' })
  • A/B 实验分桶({ experiment: 'pricing-v2' })
  • 推荐人 ID({ referrer: 'uid_xxx' })
ts
const signupFlow = await auth.createSignupFlow();

await signupFlow.send(
  { email: 'newuser@b.c' },
  { source: 'landing-v3', utm_campaign: 'spring-sale' },
);

await signupFlow.code(
  { email: 'newuser@b.c', code: '123456' },
  { source: 'landing-v3', utm_campaign: 'spring-sale' },
);

注意:sendcode 各自独立调用,两次的 transient_payload 互不继承。如果业务需要 webhook 在两次都能拿到同一份元数据,两次都要传

API 参考

方法签名说明
loginFlow.send(params: { email } | { phone }, transient_payload?) => Promise<void>让 Kratos 把验证码发给已存在的 identifier;不会自动登录
loginFlow.code(params: { email, code } | { phone, code }, transient_payload?) => Promise<void>提交验证码;成功后自动 initTokenized()
signupFlow.send(traits: { email } | { phone }, transient_payload?) => Promise<void>为新 identifier 发码;Kratos 会校验该 identifier 不存在
signupFlow.code(params: { email, code } | { phone, code }, transient_payload?) => Promise<void>提交验证码完成注册;traits 写入新 identity;自动登录
verificationFlow.send(params: { email } | { phone }, transient_payload?) => Promise<void>为已登录用户的新 traits 发码
verificationFlow.verify(params: { email, code } | { phone, code }, transient_payload?) => Promise<void>提交验证码,标记 trait 为 verified;自动 initTokenized() 刷新 session

params 字段

名称 类型 默认值 说明
emailstring邮箱,与 phone 二选一
phonestringE.164 手机号,与 email 二选一
codestring用户输入的 6 位验证码;仅 code()/verify() 需要
transient_payloadRecord<string, any>可选,转发到 webhook,不写入 traits

常见错误

验证码错误

ts
try {
  await loginFlow.code({ email, code: '000000' });
}
catch (error) {
  const e = error as Error;
  if (e.name === 'Flow') {
    // e.message 形如「The provided code is invalid, check for typos or try again」
    showToast('验证码错误,请重试');
  }
}

flow 状态自动更新,用户改一下输入直接重试即可,不用重建。

验证码过期

Kratos 默认 5 分钟过期。过期后 code() 会以 Flow 错误返回 'The session expired' 或类似消息。处理方式:让用户点「重新发送」,即再调一次 send(),不需要 createLoginFlow() 重建

ts
async function resend() {
  await loginFlow.send({ email }); // 复用同一个 flow
  startCountdown(60);
}

频率限制

Kratos 后端配置了发码频率(默认 30s 一次)。前端短时间连续 send 会拿到 Response 错误:

ts
try {
  await loginFlow.send({ email });
}
catch (error) {
  const e = error as Error;
  if (e.name === 'Response' && e.message.includes('rate')) {
    showToast('发送过于频繁,请稍后再试');
  }
}

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

下一步

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