验证码流程
验证码(OTP)是 password 之外的另一条统一路径,适用于:
- 登录 — 已存在用户用一次性验证码替代密码
- 注册 — 新用户(尤其是手机号)用验证码完成注册
- 验证 — 已登录用户校验新绑定的邮箱 / 手机
底层都是 Kratos method: 'code',SDK 把它拆成两步对称的子方法:
send(traits)— 让 Kratos 把验证码发到 email / phonecode(traits + code)或verify(code)— 提交验证码完成本次 flow
三类 flow 的子方法名略有差异:登录 / 注册用
code(),verification 用verify()。下面分别说明。
登录(验证码)
适合「记不住密码」或「无密码登录」场景。先发码,再用同一个 loginFlow 提交验证码:
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 已可用手机号同样支持:
await loginFlow.send({ phone: '+8613800138000' });
await loginFlow.code({ phone: '+8613800138000', code: '123456' });send 与 code 必须用同一个 loginFlow 实例——它内部持有 flowId 与 csrf_token,跨方法共享。重新 createLoginFlow() 会得到新的上下文,验证码就对不上了。
注册(验证码)
新用户走 createSignupFlow(),签名与登录对称。手机号注册必须走这条路径(密码方式被后端拒,见密码流程):
const signupFlow = await auth.createSignupFlow();
// 1. 发码到手机号
await signupFlow.send({ phone: '+8613800138000' });
// 2. 提交验证码完成注册;traits 写入新 identity
await signupFlow.code({
phone: '+8613800138000',
code: '123456',
});
// 注册即登录,token 已写入缓存邮箱注册同理:
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:
// 前置:用户已登录,且通过 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 内部会从响应里提取最新的 flowId 与 csrf_token,就地更新到工厂闭包持有的 state 上。所以同一个 flow 实例可以反复重试,无需重建:
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 即可:
// 同一个 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' })
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' },
);注意:send 与 code 各自独立调用,两次的 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 字段
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
email | string | — | 邮箱,与 phone 二选一 |
phone | string | — | E.164 手机号,与 email 二选一 |
code | string | — | 用户输入的 6 位验证码;仅 code()/verify() 需要 |
transient_payload | Record<string, any> | — | 可选,转发到 webhook,不写入 traits |
常见错误
验证码错误
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() 重建。
async function resend() {
await loginFlow.send({ email }); // 复用同一个 flow
startCountdown(60);
}频率限制
Kratos 后端配置了发码频率(默认 30s 一次)。前端短时间连续 send 会拿到 Response 错误:
try {
await loginFlow.send({ email });
}
catch (error) {
const e = error as Error;
if (e.name === 'Response' && e.message.includes('rate')) {
showToast('发送过于频繁,请稍后再试');
}
}建议 UI 上显示倒计时按钮,前端先卡一道,避免无效请求。