ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Epic Stack 邮箱验证双通道方案:邮件验证码与验证链接的设计决策与源码实现

Epic Stack 邮箱验证双通道方案:邮件验证码与验证链接的设计决策与源码实现 Epic Stack 邮箱验证双通道方案邮件验证码与验证链接的设计决策与源码实现【免费下载链接】epic-stackThis is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea.项目地址: https://gitcode.com/GitHub_Trending/ep/epic-stack本指南围绕 Epic Stack 中的决策记录 013-email-code.md 展开剖析该方案从仅发送验证链接演进为验证码 验证链接双通道的动机、决策与代价并结合仓库源码逐层还原 TOTP 验证码的生成、持久化、邮件投递与/verify校验的完整链路。读完本文你将掌握在 React RouterRemix全栈应用中搭建验证码可选、链接可点邮箱验证体系的具体实现方式并理解如何将该机制复用到找回密码、修改邮箱与双因素认证等敏感操作。决策背景为什么要验证邮箱当新用户注册时应用必须收集其邮箱地址以便在用户忘记密码时发送重置链接应用的其他功能如通知、订阅也可能依赖邮箱。无论用途如何收集邮箱之后都需要对其进行验证以降低垃圾注册spam风险、减少用户填错邮箱导致的错误。在采用本决策之前Epic Stack 的做法是在邮件中附带一个链接用户点击链接后进入引导onboarding流程。这一方案功能上可用但在真实使用中存在明显的体验缺陷用户在注册页提交后会留下一个没有后续动作的旧标签页属于典型的 dead-end 页面令人困惑尤其是在移动端邮件客户端往往会在另一个浏览器中打开链接用户被迫在多个应用间来回切换流程割裂。决策内容验证码与链接双通道并行针对上述问题决策记录给出的结论非常明确——两种方式都支持We will support both options. The email will include a code and a link, giving the user the option between the two so they can select the one that works best for them in the situation.也就是说验证邮件中同时包含一段验证码和一个验证链接偏好便捷的用户可以直接点击链接延续一键进入 onboarding的老路径更看重停留在当前页继续操作的用户可以留在原标签页把邮件里的验证码手动输入到应用中。这种双通道设计的好处在于把选择权交给用户同时为后续功能铺路一旦验证码体系就位把它复用到密码重置等敏感操作上就会变得非常容易决策记录原文明确指出 this paves the way for other features in the future。决策代价多一点实现工作换更好的体验决策记录在 Consequences 中坦率地承认双通道比单一链接需要更多实现工作——既要生成和校验验证码又要保证链接仍然可用还要处理两种方式的会话衔接。但从结果看这是值得的一方面带来了更好的用户体验另一方面为未来功能密码重置验证、改邮箱验证、2FA 等打下了统一的基础设施。而仓库的实际代码恰好印证了这一点Epic Stack 最终把验证码机制做成了一套统一、可复用的验证设施onboarding、reset-password、change-email、2fa四种验证场景共享同一套生成与校验管线。源码实现验证码的生成、存储与邮件发送验证码生成prepareVerification所有验证码的生成都收敛在 verify.server.ts 的prepareVerification函数中其核心调用链如下const { otp, ...verificationConfig } await generateTOTP({ algorithm: SHA-256, // Leaving off 0, O, and I on purpose to avoid confusing users. charSet: ABCDEFGHJKLMNPQRSTUVWXYZ123456789, period, })关键参数说明algorithm: SHA-256TOTP 使用的 HMAC 哈希算法采用 SHA-256 而非常见的 SHA-1安全性更高charSet: ABCDEFGHJKLMNPQRSTUVWXYZ123456789验证码字符集。注意源码注释明确指出刻意去掉了 0、O、I 三个字符避免用户在看不清时把数字 0 与字母 O、字母 I 与数字 1 混淆period验证码的有效期秒由调用方传入Epic Stack 在注册与找回密码场景中均传入10 * 60即 10 分钟generateTOTP来自epic-web/totp库totp.server.ts 以.server.ts后缀仅做服务端导出避免把 Crypto polyfill 打进客户端包。生成 OTP 后函数会将验证配置以(target, type)为唯一键 upsert 进数据库并计算过期时间const verificationData { type, target, ...verificationConfig, expiresAt: new Date(Date.now() verificationConfig.period * 1000), } await prisma.verification.upsert({ where: { target_type: { target, type } }, create: verificationData, update: verificationData, })对应的数据模型定义在 schema.prisma字段说明type验证类型例如 email 或 phoneEpic Stack 中实际取值为onboarding/reset-password/change-email/2fatarget被验证的对象例如用户的邮箱地址secret用于生成 OTP 的密钥algorithm生成 OTP 使用的算法SHA-256digitsOTP 位数periodOTP 有效秒数charSetOTP 可用字符集expiresAt过期时间由period推算双通道的核心把 OTP 同时塞进链接prepareVerification返回三个值otp、redirectTo和verifyUrl其中verifyUrl是双通道实现的关键——验证码被直接以查询参数拼进验证链接const verifyUrl getRedirectToUrl({ request, type, target }) // add the otp to the url well email the user. verifyUrl.searchParams.set(codeQueryParam, otp)getRedirectToUrlverify.server.ts构造的 URL 形如https://your-domain.com/verify?typeonboardingtargetuserexample.comcodeXXXXXX这样邮件里既展示纯文本验证码又提供携带同一验证码的链接点击链接的用户直接带着code参数访问/verify页面手动输入的用户则在表单里填写相同的验证码。两条路径最终都走到同一套校验逻辑。邮件内容代码与链接共存以注册场景为例signup.tsx 的 action 中调用prepareVerification后通过sendEmail发送邮件并使用 React Email 组件渲染邮件正文const { verifyUrl, redirectTo, otp } await prepareVerification({ period: 10 * 60, request, type: onboarding, target: email, }) const response await sendEmail({ to: email, subject: Welcome to Epic Notes!, react: SignupEmail onboardingUrl{verifyUrl.toString()} otp{otp} /, })SignupEmail组件同文件 signup.tsx同时渲染两段内容Heres your verification code:{otp}Or click the link to get started:{onboardingUrl}sendEmail本身定义在 email.server.ts通过 Resend API 投递并做了两层防护若未配置RESEND_API_KEY且未开启MOCKS环境变量邮件不会真实发送而是把内容打印到控制台并返回 mock 成功若 Resend 返回错误则按 Zod schema 解析为结构化错误返回给表单层。校验流程/verify 路由与 TOTP 验证验证页面与表单用户拿到验证码后无论在哪个标签页最终都会落到/verify页面。verify.tsx 中定义了四个查询参数与校验 schemaexport const codeQueryParam code export const targetQueryParam target export const typeQueryParam type export const redirectToQueryParam redirectTo const types [onboarding, reset-password, change-email, 2fa] as const export const VerifySchema z.object({ [codeQueryParam]: z.string().min(6).max(6), [typeQueryParam]: VerificationTypeSchema, [targetQueryParam]: z.string(), [redirectToQueryParam]: z.string().optional(), })页面使用 Conform Zod 做表单校验验证码输入框带autoComplete: one-time-code与autoFocus并隐藏type、target、redirectTo三个字段——也就是说验证码固定为 6 位min(6).max(6)与 TOTP 的digits位数保持一致。点击链接进入的用户表单的code默认值会直接从 URL 查询参数中预填verify.tsx因此甚至可以免输入直接提交。校验与分发validateRequest表单提交后进入 verify.server.ts 的validateRequest其核心校验逻辑是isCodeValidconst verification await prisma.verification.findUnique({ where: { target_type: { target, type }, OR: [{ expiresAt: { gt: new Date() } }, { expiresAt: null }], }, select: { algorithm: true, secret: true, period: true, charSet: true }, }) if (!verification) return false const result await verifyTOTP({ otp: code, ...verification })要点以(target, type)复合唯一键从数据库取回验证记录同时用expiresAt now或expiresAt为空过滤已过期记录使用记录中保存的secret、algorithm、period、charSet调用verifyTOTP重新计算并比对 OTP因此校验完全不依赖双方共享时钟之外的状态数据库里存什么参数就用什么参数验证验证失败时通过VerifySchema.superRefine在code字段上注入Invalid code错误返回 400。校验通过后validateRequest依据type参数分发到不同场景的处理函数verify.server.tstype处理函数后续流程reset-passwordhandleResetPasswordVerificationreset-password.server.ts删除验证记录按 email/username 查找用户将 username 写入验证会话重定向到/reset-passwordonboardinghandleOnboardingVerificationonboarding/index.server.ts删除验证记录将已验证的 email 写入验证会话重定向到/onboardingchange-emailhandleChangeEmailVerificationchange-email.server.tsx删除验证记录完成邮箱更换2fahandleLoginTwoFactorVerificationlogin.server.ts处理登录双因素校验2FA 类型不删除验证记录因为同一 TOTP 需要支持多窗口比对验证会话10 分钟安全窗口验证通过后已验证身份如何传递到下一步答案是专用的 cookie session。verification.server.ts 定义了verifySessionStorageexport const verifySessionStorage createCookieSessionStorage({ cookie: { name: en_verification, sameSite: lax, // CSRF protection is advised if changing to none path: /, httpOnly: true, maxAge: 60 * 10, // 10 minutes secrets: process.env.SESSION_SECRET.split(,), secure: process.env.NODE_ENV production, }, })要点maxAge: 60 * 10验证会话与验证码一样限定 10 分钟有效期避免验一次、永不过期httpOnly: truecookie 对 JS 不可见防 XSS 窃取sameSite: lax默认限制跨站携带源码注释特别提醒若改成none必须自行补上 CSRF 防护secure在生产环境强制 HTTPS 传输。以 onboarding 为例校验通过后把目标 email 写入会话并重定向onboarding/index.server.tsonboarding 页面的 loader 再通过requireOnboardingEmailonboarding/index.tsx从会话中取出 email拿不到则直接重定向回/signup。由此形成验证码 → 会话 → 表单 → 创建账号的闭环。复用到其他敏感操作决策记录中为未来功能铺路的判断在仓库中已经兑现验证码机制被复用在四个场景全部复用prepareVerification/validateRequest这套管线注册引导onboarding/signup提交邮箱后发送验证邮件signup.tsx 以type: onboarding调用找回密码reset-password/forgot-password提交用户名或邮箱后forgot-password.tsx 以type: reset-password调用邮件同样同时包含验证码与链接修改邮箱change-email设置页修改邮箱时使用type: change-email见 change-email.tsx双因素认证2fa登录时若需二次验证verify.server.ts 的requireRecentVerification会通过shouldRequestTwoFA判断并把用户引导到/verify?type2fa页面文案也会切换为 Check your 2FA appverify.tsx。值得注意的是找回密码场景对用户枚举的防护handleResetPasswordVerification在找不到用户时并不直接报用户不存在而是返回与验证码错误相同的Invalid code提示reset-password.server.ts源码注释明确指出这是为了防止攻击者探测某个邮箱是否已注册。安全与体验的配套设计除了上文提到的字符集、有效期与会话 cookie这套体系还有几处值得借鉴的安全细节蜜罐防护Honeypot/signup、/forgot-password、/verify等表单 action 均先调用checkHoneypot(formData)如 signup.tsx配合 honeypot.server.ts 拦截自动化灌入表单的机器人防枚举 防重放验证记录以(target, type)唯一键 upsert同一目标同一类型只会有一条有效记录旧验证码被新生成的覆盖校验成功后大多立即deleteVerification删除记录验证码一次性有效验证码与链接的内容一致链接里的code参数与邮件正文的otp是同一个值两条路径共享同一套 TOTP 校验不存在链接有效但验证码无效的不一致状态开发者本地体验未配置RESEND_API_KEY时email.server.ts 会把邮件内容打印到控制台并返回 mock 成功配合MOCKS环境变量见 tests/mocks/index.ts本地开发无需真实邮件服务即可走通完整验证流程。总结013-email-code这份决策记录看似简短却定义了一套贯穿 Epic Stack 全局的验证基础设施邮件同时携带验证码与链接把选择权交给用户并以同一套 TOTP 校验逻辑支撑注册、找回密码、改邮箱与 2FA 四个场景。从仓库源码可以清楚看到该决策带来的额外工作量最终沉淀为prepareVerificationvalidateRequest两个可复用函数配合 PrismaVerification模型、en_verification会话 cookie 与 Resend 邮件通道构成了完整、安全且可扩展的邮箱验证体系。若想进一步深入可以继续阅读 TOTP 实现决策 与 邮件服务说明了解验证码底层算法选型与邮件通道的完整配置。【免费下载链接】epic-stackThis is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea.项目地址: https://gitcode.com/GitHub_Trending/ep/epic-stack创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表