Wasp 邮箱认证完整实战指南:从注册登录到邮件验证与密码重置
【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/wasp
本指南以 Wasp 全栈框架(当前仓库 waspc 与 web 目录对应的开源实现)的邮箱(Email)认证能力为主线,完整讲解如何在 Wasp 应用中启用邮箱注册登录、邮件验证(Email Verification)与"忘记密码"(Password Reset)流程,并深入剖析这些流程在生成代码层面的底层实现与安全机制。读完本文,你将掌握在main.wasp中声明邮箱认证、配置邮件发送器、定制验证邮件内容、扩展注册字段,以及用内置 Auth UI 或手动调用 auth action 两种方式实现完整认证链路的实战方法。
邮箱认证能开箱即用地提供什么
Wasp 对邮箱认证提供了开箱即用的完整支持,包括服务端实现和邮件模板。围绕邮箱认证,框架帮你封装好了以下四类能力:
- 注册与登录(Signup / Login):基于邮箱 + 密码的账号体系,密码由服务端自动哈希存储。
- 邮件验证(Email Verification):注册后向用户邮箱发送验证链接,默认只有在验证通过后才允许登录。
- 忘记密码(Forgot Password / Password Reset):用户可请求重置密码邮件,通过带 token 的链接设置新密码。
- 内置 Auth UI 组件:生成
LoginForm、SignupForm、VerifyEmailForm、ForgotPasswordForm、ResetPasswordForm等现成组件,直接拼装进页面即可使用。
同时要说明一个当前限制:从 version-0.19/auth/_multiple-identities-warning.md 的说明来看,目前 Wasp 尚不支持一个用户绑定多个认证身份,例如同一个用户不能同时拥有邮箱身份和 Google 身份,账号合并(Account Merging)功能仍在规划中。
设置邮箱认证的五个步骤
完整的设置流程分为五步,最终的main.wasp文件结构大致如下:
// Configuring e-mail authentication app myApp { auth: { ... }, emailSender: { ... } } // Defining routes and pages route SignupRoute { ... } page SignupPage { ... } // ...下面按步骤展开(配置代码以 version-0.19 文档 为准)。
1. 在 main.wasp 中启用邮箱认证
app myApp { wasp: { version: "{latestWaspVersion}" }, title: "My App", auth: { // 1. 指定用户实体(下一步会定义它) userEntity: User, methods: { // 2. 启用邮箱认证 email: { // 3. 指定发件人字段 fromField: { name: "My App Postman", email: "hello@itsme.com" }, // 4. 指定邮件验证与密码重置选项(后面会细讲) emailVerification: { clientRoute: EmailVerificationRoute, }, passwordReset: { clientRoute: PasswordResetRoute, }, }, }, onAuthFailedRedirectTo: "/login", onAuthSucceededRedirectTo: "/" }, }这段声明是整套邮箱认证的核心,各字段含义如下:
auth.userEntity:指向你定义的User实体,Wasp 会把业务用户与认证数据关联起来。auth.methods.email:启用邮箱认证方式,email与usernameAndPassword二选一。auth.methods.email.fromField:发送验证邮件 / 重置邮件时的发件人姓名与地址。auth.methods.email.emailVerification.clientRoute:验证邮件的跳转路由,即用户点击邮件链接后进入的前端路由。auth.methods.email.passwordReset.clientRoute:重置密码邮件的跳转路由。auth.onAuthFailedRedirectTo:未认证用户访问受保护页面(authRequired: true)时被重定向到的路由。auth.onAuthSucceededRedirectTo:登录 / 注册成功后跳转的路由,默认值为"/";该自动跳转仅在启用 Wasp 内置 Auth UI 时生效。
2. 添加 User 实体
User实体可以精简到只有id字段:
// 5. 定义用户实体 model User { // highlight-next-line id Int @id @default(autoincrement()) // 在此下方添加你自己的字段 // ... }关于User实体有两个要点:
id字段是必需的,它可以是任意类型,但必须用@id标记(见 _user-fields.md)。- 除
id外你可以自由添加业务字段;如果这些字段需要在注册时写入,还需同步配置userSignupFields(见后文"扩展注册字段")。
User实体如何与整个认证系统关联、如何读取用户数据,可进一步阅读 认证实体文档。
3. 添加认证相关的路由与页面
在main.wasp中声明 5 条路由与页面,分别对应登录、注册、请求重置密码、重置密码、邮件验证:
// ... route LoginRoute { path: "/login", to: LoginPage } page LoginPage { component: import { Login } from "@src/pages/auth" } route SignupRoute { path: "/signup", to: SignupPage } page SignupPage { component: import { Signup } from "@src/pages/auth" } route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage } page RequestPasswordResetPage { component: import { RequestPasswordReset } from "@src/pages/auth", } route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage } page PasswordResetPage { component: import { PasswordReset } from "@src/pages/auth", } route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage } page EmailVerificationPage { component: import { EmailVerification } from "@src/pages/auth", }这些组件的 React 实现将写在src/pages/auth.{jsx,tsx}中。
4. 创建客户端页面并使用 Auth UI 组件
在src/pages下创建auth.{jsx,tsx},引入 Wasp 生成的 Auth UI 组件(页面样式使用 Tailwind CSS,相关引入方式可参考 project/css-frameworks):
import { LoginForm, SignupForm, VerifyEmailForm, ForgotPasswordForm, ResetPasswordForm, } from 'wasp/client/auth' import { Link } from 'react-router-dom' export function Login() { return ( <Layout> <LoginForm /> <br /> <span className="text-sm font-medium text-gray-900"> Don't have an account yet? <Link to="/signup">go to signup</Link>. </span> <br /> <span className="text-sm font-medium text-gray-900"> Forgot your password? <Link to="/request-password-reset">reset it</Link>. </span> </Layout> ) } export function Signup() { return ( <Layout> <SignupForm /> <br /> <span className="text-sm font-medium text-gray-900"> I already have an account (<Link to="/login">go to login</Link>). </span> </Layout> ) } export function EmailVerification() { return ( <Layout> <VerifyEmailForm /> <br /> <span className="text-sm font-medium text-gray-900"> If everything is okay, <Link to="/login">go to login</Link> </span> </Layout> ) } export function RequestPasswordReset() { return ( <Layout> <ForgotPasswordForm /> </Layout> ) } export function PasswordReset() { return ( <Layout> <ResetPasswordForm /> <br /> <span className="text-sm font-medium text-gray-900"> If everything is okay, <Link to="/login">go to login</Link> </span> </Layout> ) } // 用于居中内容的布局组件 export function Layout({ children }: { children: React.ReactNode }) { return ( <div className="h-full w-full bg-white"> <div className="flex min-h-[75vh] min-w-full items-center justify-center"> <div className="h-full w-full max-w-sm bg-white p-5"> <div>{children}</div> </div> </div> </div> ) }通过这种方式,邮件验证、请求重置密码、重置密码等流程中"从 URL 读取 token 并发送给服务端"的繁琐工作全部由 Auth UI 组件代劳。如果想完全自定义登录 / 注册界面,可以改用 邮箱认证自定义 UI 的方式手动调用认证 action。更多 Auth UI 组件的用法参见 Auth UI 文档。
5. 配置邮件发送器(Email Sender)
验证邮件与重置密码邮件都需要一个邮件发送器。Wasp 开箱支持多个邮件服务商:Dummy(仅开发环境)、Mailgun、SendGrid、Resend以及通用SMTP(详见 高级邮件文档)。
为快速跑通流程,先用Dummy提供商——它不会真正发信,而是把邮件内容打印到控制台:
app myApp { // ... // 7. 设置邮件发送器 emailSender: { provider: Dummy, } }需要特别注意的是,Dummy提供商仅限开发环境使用。从 _dummy-provider-note.md 的说明可知,如果用Dummy提供商执行生产构建,构建会直接失败。
收尾:迁移数据库并启动
完成上述配置后,依次运行:
wasp db migrate-dev wasp start即可得到一个带邮箱认证的可运行应用。想为某些页面开启登录保护,只需在页面声明中加上authRequired: true,未登录用户会被重定向到onAuthFailedRedirectTo指定的路由,详见 认证总览文档。
登录与注册流程的内置防护行为
使用邮箱认证后,登录和注册流程默认带有以下几项安全防护:
- 注册限流(Rate limiting):同一邮箱地址的注册请求被限制为每分钟 1 次,用于防止垃圾注册。
- 防止邮箱枚举(Preventing user email leaks):如果有人用一个已存在且已验证的邮箱注册,服务端会"假装"注册成功,而不是提示邮箱已被占用,从而避免泄露已有用户的邮箱地址。
- 允许未验证邮箱重复注册(Allowing registration for unverified emails):如果用户用一个已存在但未验证的邮箱注册,Wasp 会允许其重新注册。这是为了防止恶意用户抢先占用他人邮箱、永久阻止邮箱主人注册。
- 密码校验(Password validation):默认要求密码非空、长度至少 8 位且包含数字。校验规则与覆盖方式见 认证总览中的默认校验。
源码视角:注册防护是怎么实现的
从生成代码可以印证上述行为。在 signup.ts 模板 中,注册路由的处理逻辑如下:
- 对已存在且已验证的邮箱身份,调用
doFakeWork()模拟耗时后直接返回{ success: true },刻意与真实注册行为保持一致,防止通过响应差异枚举邮箱; - 对已存在但未验证的邮箱身份,检查
isEmailResendAllowed(providerData, 'emailVerificationSentAt')判断距离上次发送验证邮件是否满足时间间隔,不满足则抛出400 Please wait X secs before trying again.,满足则删除旧用户并重新创建; - 参数校验由
ensureValidEmail、ensurePasswordIsPresent、ensureValidPassword完成(见 validation.ts 模板:邮箱必须非空且格式合法,密码必须非空、长度 ≥ 8 且包含数字); - 密码通过
sanitizeAndSerializeProviderData序列化时自动哈希,绝不会以明文落库; - 创建成功后调用
createEmailVerificationLink(email, clientRoute)生成带 JWT token 的验证链接,并通过sendEmailVerificationEmail发送。
开发模式下跳过邮件验证
默认情况下,Wasp 要求邮箱验证通过后才允许登录。但在开发阶段每次注册都走一遍邮件验证很繁琐,也影响自动化测试的编写。为此可以在.env.server中设置环境变量:
SKIP_EMAIL_VERIFICATION_IN_DEV=true该变量的底层逻辑在 config/email.ts 模板 中:仅在isDevelopment为真时,isEmailAutoVerified才会读取env.SKIP_EMAIL_VERIFICATION_IN_DEV,进而由 signup.ts 模板 将isEmailVerified直接置为true并跳过发信;生产构建中该值恒为false。
邮件验证流程(Email Verification)
默认注册完成后,Wasp 会向用户邮箱发送验证邮件。邮件中的链接指向emailVerification.clientRoute指定的路由(本例即EmailVerificationRoute,路径为/email-verification):
// ... emailVerification: { clientRoute: EmailVerificationRoute, }用户点击链接进入验证页后,页面需要从 URL 取出 token 并交给服务端验证。如果你用了 Auth UI 的VerifyEmailForm,这一步已自动完成;手动实现时则调用verifyEmailaction:
import { verifyEmail } from 'wasp/client/auth' // ... await verifyEmail({ token });源码视角:verifyEmail 的实现
在 verifyEmail.ts 模板 中,服务端处理流程为:
- 用
validateJWT校验并解析 token 中的email,token 非法则抛出400 Email verification failed, invalid token; - 通过
findAuthIdentity(createProviderId('email', email))查找邮箱身份,找不到同样报错(防止枚举); - 将
providerData.isEmailVerified更新为true; - 触发
onAfterEmailVerifiedHook钩子,供业务方在验证完成后执行自定义逻辑(如欢迎邮件、积分发放等,详见 auth-hooks.md)。
定制验证邮件内容
默认验证邮件内容由生成代码提供(见 config/email.ts 模板 中未定义getEmailContentFn时的兜底实现),主题为 "Verify your email",正文包含验证链接。你可以通过getEmailContentFn字段完全自定义:
app myApp { // ... auth: { methods: { email: { // ... emailVerification: { clientRoute: EmailVerificationRoute, getEmailContentFn: import { getVerificationEmailContent } from "@src/auth/email", }, }, }, }, }对应的实现文件(注意 TypeScript 类型GetVerificationEmailContentFn从wasp/server/auth导入,函数接收verificationLink并返回subject/text/html三部分):
import { GetVerificationEmailContentFn } from 'wasp/server/auth' export const getVerificationEmailContent: GetVerificationEmailContentFn = ({ verificationLink, }) => ({ subject: 'Verify your email', text: `Click the link below to verify your email: ${verificationLink}`, html: ` <p>Click the link below to verify your email</p> <a href="${verificationLink}">Verify email</a> `, })密码重置流程(Password Reset)
用户可以在/request-password-reset页面输入邮箱发起重置请求,随后收到一封带重置链接的邮件;链接指向passwordReset.clientRoute指定的路由(本例即PasswordResetRoute,路径为/password-reset),用户在那里输入新密码完成重置:
// ... passwordReset: { clientRoute: PasswordResetRoute, }该流程同样内置了两项安全防护:
- 限流:同一邮箱的密码重置请求同样限制为每分钟 1 次。
- 防止信息泄露:如果请求重置的邮箱不存在,服务端会返回与"重置成功"完全一致的响应,避免攻击者通过响应差异判断邮箱是否注册过。
手动实现时,两个关键 action 分别是requestPasswordReset与resetPassword:
import { requestPasswordReset } from 'wasp/client/auth' // ... await requestPasswordReset({ email });import { resetPassword } from 'wasp/client/auth' // ... await resetPassword({ password, token })源码视角:请求重置与重置的实现
在 requestPasswordReset.ts 模板 中:
- 先用
ensureValidEmail校验邮箱; - 邮箱身份不存在时执行
doFakeWork()模拟耗时再返回成功,从响应时间上增加邮箱枚举难度(源码注释明确说明了这一设计意图); - 身份存在时通过
isEmailResendAllowed(providerData, 'passwordResetSentAt')做限流,通过后调用createPasswordResetLink生成链接并发送邮件。
在 resetPassword.ts 模板 中:
- 先校验 token、再校验密码(源码注释说明:这是为了让持有无效 token 的未认证调用者无法探测部署环境的密码策略);
- token 解析失败返回
400 Password reset failed, invalid token; - 更新
hashedPassword时自动重新哈希,同时把isEmailVerified置为true——即成功重置密码即视为邮箱已验证; - 调用
invalidateAllSessionsForAuthId使该用户所有现存会话失效,防止会话被他人继续使用。
定制重置密码邮件内容
与验证邮件类似,通过passwordReset.getEmailContentFn定制:
app myApp { // ... auth: { methods: { email: { // ... passwordReset: { clientRoute: PasswordResetRoute, getEmailContentFn: import { getPasswordResetEmailContent } from "@src/auth/email", }, }, }, }, }import { GetPasswordResetEmailContentFn } from 'wasp/server/auth' export const getPasswordResetEmailContent: GetPasswordResetEmailContentFn = ({ passwordResetLink, }) => ({ subject: 'Password reset', text: `Click the link below to reset your password: ${passwordResetLink}`, html: ` <p>Click the link below to reset your password</p> <a href="${passwordResetLink}">Reset password</a> `, })密码相关的校验辅助函数
Wasp 在wasp/server/auth(对应生成模板 validation.ts)中提供了两个可直接复用的密码校验函数:
ensurePasswordIsPresent(args):检查密码是否存在,缺失则抛出校验错误。ensureValidPassword(args):检查密码是否合法(长度 ≥ 8 且包含数字),不合法则抛出校验错误;具体规则见 认证总览的默认校验。
扩展注册字段(userSignupFields)
如果需要保存邮箱、密码之外的额外注册字段(如address、phone),需要做两件事。
第一步:服务端声明字段
在main.wasp中给email方法加上userSignupFields引用:
app myApp { // ... auth: { userEntity: User, methods: { email: { // ... userSignupFields: import { userSignupFields } from "@src/auth", // ... }, }, }, }然后在src/auth.{js,ts}中定义字段处理函数。userSignupFields是一个对象:键是字段名(必须与User实体上的字段一一对应),值是接收客户端提交数据的函数,函数返回要写入数据库的值,数据非法时抛错:
import { defineUserSignupFields } from 'wasp/server/auth' export const userSignupFields = defineUserSignupFields({ address: async (data) => { const address = data.address if (typeof address !== 'string') { throw new Error('Address is required') } if (address.length < 5) { throw new Error('Address must be at least 5 characters long') } return address }, })两点提醒:
- 不要把
password放进userSignupFields,密码由 Wasp 认证后端单独处理(自动哈希),防止以明文落库; - 也可以在字段函数里使用任意校验库,例如
zod的safeParse来做更复杂的校验(示例见 认证总览的注册字段定制)。
从 signup.ts 模板 可以看到,userSignupFields传入validateAndGetUserFields后被用于校验与写库;同时该模板也证明了这些字段处理函数会在onBeforeSignupHook之后执行,因此钩子可以先行否决(通过抛错)整个注册流程。
第二步:在 SignupForm 中展示字段
使用 Auth UI 时,通过SignupForm的additionalFieldsprop 添加额外字段,它可以是对象列表或渲染函数(二者可混用):
import { SignupForm, FormError, FormInput, FormItemGroup, FormLabel, } from 'wasp/client/auth' export const SignupPage = () => { return ( <SignupForm additionalFields={[ /* address 用对象定义 */ { name: 'address', label: 'Address', type: 'input', validations: { required: 'Address is required', }, }, /* phoneNumber 用渲染函数定义 */ (form, state) => { return ( <FormItemGroup> <FormLabel>Phone Number</FormLabel> <FormInput {...form.register('phoneNumber', { required: 'Phone number is required', })} disabled={state.isLoading} /> {form.formState.errors.phoneNumber && ( <FormError> {form.formState.errors.phoneNumber.message} </FormError> )} </FormItemGroup> ) }, ]} /> ) }对象形式的字段支持name、label(必填)、type(可选input/textarea)与validations(校验规则对象,键为校验名、值为错误提示,规则体系与react-hook-form的register一致)。渲染函数签名如下:
type AdditionalSignupFieldRenderFn = ( hookForm: UseFormReturn, formState: FormState ) => React.ReactNode其中form是react-hook-form对象(需要用form.register注册字段),state是表单状态(含isLoading: boolean,表示是否正在提交)。如果你不使用 Auth UI 而是自定义注册界面,则只需在自定义表单中提交这些额外字段即可,无需配置additionalFields。
读取用户的邮箱认证数据
拿到user对象后(客户端通过useAuth()或受保护页面的userprop,服务端通过context.user,具体见 认证总览的访问登录用户),可以通过user.identities.email访问邮箱认证相关的全部数据(字段说明来自 entities/_email-data.md):
const emailIdentity = user.identities.email // 用户注册时使用的邮箱地址,例如 "fluffyllama@app.com" emailIdentity.id // 邮箱是否已验证,true 表示已验证 emailIdentity.isEmailVerified // 最后一次发送验证邮件的时间 emailIdentity.emailVerificationSentAt // 最后一次发送密码重置邮件的时间 emailIdentity.passwordResetSentAt关于认证数据的整体模型(User、Auth、AuthIdentity如何关联),可继续阅读 认证实体文档 中的 "Accessing the Auth Fields" 章节。
email 字典完整字段速查
以下是auth.methods.email支持的全部配置项(对应文档 API Reference 章节):
app myApp { title: "My app", // ... auth: { userEntity: User, methods: { email: { userSignupFields: import { userSignupFields } from "@src/auth", fromField: { name: "My App", email: "hello@itsme.com" }, emailVerification: { clientRoute: EmailVerificationRoute, getEmailContentFn: import { getVerificationEmailContent } from "@src/auth/email", }, passwordReset: { clientRoute: PasswordResetRoute, getEmailContentFn: import { getPasswordResetEmailContent } from "@src/auth/email", }, }, }, onAuthFailedRedirectTo: "/someRoute" }, // ... }| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
userSignupFields | ExtImport | 否 | 注册时写入User的额外字段定义(见上文"扩展注册字段") |
fromField | EmailFromField | 是 | 发件人信息;name为发件人名称,email为发件人邮箱(email必填) |
emailVerification | EmailVerificationConfig | 是 | 邮件验证配置;clientRoute必填,指向处理验证 token 的客户端路由;getEmailContentFn可选,自定义验证邮件内容 |
passwordReset | PasswordResetConfig | 是 | 密码重置配置;clientRoute必填,指向处理重置 token 与新密码的客户端路由;getEmailContentFn可选,自定义重置邮件内容 |
emailVerification.clientRoute指定的页面需要完成"从 URL 取 token 并交给服务端"的工作(可用verifyEmailaction),passwordReset.clientRoute指定的页面则需要完成"读取 token、收集新密码并提交"的工作(可用requestPasswordReset/resetPasswordaction)——使用 Auth UI 时这些都由生成组件代劳。
从声明到路由:生成代码如何串起整个链路
最后从生成器模板层面梳理邮箱认证的完整服务端链路。在 config/email.ts 模板 中,Wasp 会为邮箱认证生成一个 Express Router,注册如下路由:
POST /login→getLoginRoute(登录)POST /signup→getSignupRoute(注册,含验证邮件发送)POST /request-password-reset→getRequestPasswordResetRoute(请求重置邮件)POST /reset-password→resetPassword(重置密码并作废全部会话)POST /verify-email→verifyEmail(验证邮箱)
模板还演示了配置注入方式:fromField、emailVerificationClientRoute、passwordResetClientRoute由声明生成,getEmailContentFn未定义时使用内置默认邮件模板,SKIP_EMAIL_VERIFICATION_IN_DEV仅在开发环境生效。这些模板位于 waspc/data/Generator/templates/server/src/auth/providers/email,是理解邮箱认证内部机制的绝佳起点;对应的客户端 auth action(requestPasswordReset、resetPassword、verifyEmail等)则生成在 waspc/data/Generator/templates/sdk/wasp/auth/email。
总结
在 Wasp 中启用邮箱认证只需五步:在main.wasp声明auth.methods.email、定义User实体、添加五条认证路由与页面、用 Auth UI 组件拼装页面、配置emailSender。之后 Wasp 会自动为你交付完整的注册 / 登录、邮件验证、密码重置服务端实现,并内置注册限流、邮箱枚举防护、未验证邮箱重注册、密码重置防枚举与 token 校验等安全机制。更进一步,你可以通过userSignupFields扩展注册字段、通过getEmailContentFn定制两类邮件内容、通过SKIP_EMAIL_VERIFICATION_IN_DEV加速开发调试,而生成模板 waspc/data/Generator/templates/server/src/auth/providers/email 则为你理解底层原理提供了完整参考。
【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/wasp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考