ARTICLE DETAIL

资讯详情

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

Bytebase 邮箱验证码登录全解析:6 位一次性验证码的无密码认证与密码重置设计

Bytebase 邮箱验证码登录全解析:6 位一次性验证码的无密码认证与密码重置设计 Bytebase 邮箱验证码登录全解析6 位一次性验证码的无密码认证与密码重置设计【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase本篇文章以 docs/superpowers/specs/2026-04-14-email-code-signin-design.md 设计文档为主体结合仓库中已经落地的源码实现进行交叉印证。你将看到 Bytebase 如何借鉴 Slack / Linear / Notion 的 Magic-Code 体验让用户仅凭邮箱 6 位数字验证码完成登录/注册并把密码重置流程统一迁移到同一套验证码机制上同时深入email_verification_code数据表、HMAC 哈希、原子条件更新、限流与反枚举等安全设计最终理解从 Proto 定义到前端交互的完整链路。Bytebase 的登录体系长期以来由「密码」与「SSO / IDP」两条路径构成。为了降低用户门槛并统一密码重置体验Bytebase 设计并实现了「邮箱 6 位一次性验证码」的无密码认证能力用户只输入邮箱收到一封带 6 位数字验证码的邮件填入即可完成登录未知邮箱自动注册密码重置也从原来的 JWT 链接改为同一套验证码机制。本文从设计文档出发完整还原这一功能的决策、数据模型、API 面、后端实现、前端交互与安全边界并对照仓库中已落地的代码给出可验证的实现依据。背景与目标向 Slack / Linear / Notion 的 Magic-Code 看齐设计文档开篇即明确了本功能的核心目标让用户只使用邮箱地址 一次性 6 位数字验证码即可完成登录以及注册镜像 Slack、Linear、Notion 等产品的 Magic-Code 认证体验并将密码重置流程统一到同一套验证码机制上。关键点在于验证码而不是魔法链接Magic Link——设计文档在 Non-goals 中明确排除了可点击链接形式的登录仅做验证码登录不做 Magic-Link 点击登录不做按工作区自定义邮件模板的能力不做开发/测试模式下的限流豁免不做投递回执delivery receipts或退信处理不改动联邦 SSO 相关能力。从仓库现状看该设计已完整落地核心实现位于 backend/api/v1/auth_service_email_code.go涵盖SendEmailLoginCode、ResetPassword、RequestPasswordReset、验证码校验、生成与哈希等全部逻辑存储层在 backend/store/email_verification_code.go数据表迁移见 backend/migrator/migration/3.18/0000##add_email_verification_code.sql。关键决策总览一张表读懂设计取舍设计文档在头脑风暴阶段收敛出 15 项关键决策它们是整个功能的设计骨架决策项选择理由设置项名称allow_email_code_signin意图最清晰不是两步验证因为它替代密码而非增强密码定位密码的替代方案而非替代品同一登录页、独立 Tab与密码和 SSO 共存未知邮箱注册是自动创建 principalMagic-Code 语义拥有邮箱即证明所有权新用户形态name默认取邮箱 local-part随机 bcrypt 密码用户日后可在个人资料中自行设置工作区归属与密码注册一致加入被预邀请的工作区或新建与既有流程对称验证码有效期10 分钟平衡邮件投递延迟与安全重发冷却60 秒防止邮件轰炸登录与密码重置共用单码最大尝试次数5 次与既有 MFA 锁定阈值一致每邮箱发送速率60 秒冷却 ⇒ 约 60 封/小时比基于审计日志计数更简单审计中间件在无工作区时会跳过未认证请求验证码存储专用数据表email_verification_code高可用安全attempts/cooldown 等有状态语义无法用 JWT 表达密码重置从 JWT 迁移到验证码共享同一张表高可用正确的冷却/重试限制统一 UX2FA 交互用户已绑定 TOTP 则仍需二次验证纵深防御不静默降级RPC 形态在既有LoginRequest上新增email_code字段复用工作区解析 / MFA / token 管道SaaS 可开关性与disallow_password_signin一样只读设置了EMAIL_CONFIG时在工作区创建时自动启用配置开关allow_email_code_signin的前世今生Store Proto 与 V1 Proto 双镜像设计文档要求在存储层和 API 层同时新增该布尔字段。仓库中的实际定义位于 proto/store/store/setting.proto 的WorkspaceProfileSetting// Allow signin/signup using email a 6-digit one-time verification code. // Requires the EMAIL setting to be configured on the workspace. bool allow_email_code_signin 22;同时在 proto/v1/v1/auth_service.proto 的Restriction消息中镜像为输出只读字段供未认证的登录页决定是否展示邮箱验证码Tab与disallow_password_signin同一套管道message Restriction { bool disallow_signup 1 [(google.api.field_behavior) OUTPUT_ONLY]; bool disallow_password_signin 2 [(google.api.field_behavior) OUTPUT_ONLY]; WorkspaceProfileSetting.PasswordRestriction password_restriction 3 [(google.api.field_behavior) OUTPUT_ONLY]; // Whether email 6-digit code signin is enabled for this workspace. bool allow_email_code_signin 4 [(google.api.field_behavior) OUTPUT_ONLY]; // Whether password reset via email is available for this workspace. bool password_reset_enabled 5 [(google.api.field_behavior) OUTPUT_ONLY]; }设计文档要求更新getAccountRestriction从setting.AllowEmailCodeSignin填充新字段并指出大概率无需 License 门控因为发信本身已要求配置 EMAIL 设置。这与实现中getAccountRestriction的既有职责一致在 backend/api/v1/auth_service_email_code.go 中SendEmailLoginCode和注册分支都通过getAccountRestriction读取该开关并在关闭时返回FailedPrecondition。SaaS 与自托管的差异化行为SaaS 工作区创建时如果设置了EMAIL_CONFIG环境变量通过getAdditionalWorkspaceSettings()自动注入allow_email_code_signin trueUpdateSetting校验SaaS 模式下该字段只读任何修改尝试返回InvalidArgument自托管工作区管理员可以切换开关置为true时要求工作区已配置 EMAIL 设置否则以FailedPrecondition拒绝。从源码看resolvePreLoginEmailSetting同一文件中正是按此双通道解析邮件配置优先取调用方传入workspaceID对应工作区的 EMAIL 设置其次回退到部署级EMAIL_CONFIG环境变量——后者覆盖了 SaaS 全新用户尚无工作区上下文的场景。数据模型一张专表承载有状态语义表结构与设计要点设计文档给出的建表语句在实现中落为 backend/migrator/migration/3.18/0000##add_email_verification_code.sql实际表结构如下实现相比设计文档额外增加了workspace列用于在校验时做门禁判断与新建工作区归属CREATE TABLE email_verification_code ( email text NOT NULL, -- Stored as EmailVerificationCodePurpose enum name (proto/store/store/auth.proto) purpose text NOT NULL, code_hash text NOT NULL, attempts int NOT NULL DEFAULT 0, expires_at timestamptz NOT NULL, last_sent_at timestamptz NOT NULL, -- Workspace context captured at send time. Used at verify time for gate checks -- (disallow_signup, allow_email_code_signin) and for provisionWorkspaceForNewUser. -- NULL for SaaS brand-new signup (no workspace exists yet — provision creates one). workspace text, PRIMARY KEY (email, purpose) ); CREATE INDEX idx_email_verification_code_expires_at ON email_verification_code (expires_at);设计要点主键(email, purpose)——每个邮箱每种用途同时只有一个有效验证码重发通过 UPSERT 覆盖无工作区列设计阶段——验证码按身份identity作用域隔离与principal、web_refresh_token一致实现中补充的workspace列仅记录发送时刻的工作区上下文用于校验时的门禁判断code_hash使用 HMAC-SHA256——以服务端auth_secret作为密钥对 6 位验证码做 HMAC 而非裸 SHA-256防止数据库被攻破后对 10^6 规模的验证码空间做离线爆破攻击者还需拿到 auth secret 才能验证候选码expires_at索引——支撑后台清理任务。实现中对应的哈希函数在 backend/api/v1/auth_service_email_code.go// hashEmailCode returns HMAC-SHA256(code) hex-encoded, keyed with the servers auth secret. func hashEmailCode(secret, code string) string { mac : hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(code)) return hex.EncodeToString(mac.Sum(nil)) }而 6 位数字验证码的生成使用crypto/rand而非可预测的伪随机源将随机字节映射到0123456789字符集func generateEmailCode() (string, error) { const digits 0123456789 b : make([]byte, emailCodeLength) if _, err : rand.Read(b); err ! nil { return , err } for i : range b { b[i] digits[int(b[i])%len(digits)] } return string(b), nil }Purpose 枚举设计文档建议新建proto/store/store/email_verification_code.proto从仓库现状看EmailVerificationCodePurpose枚举最终落在 proto/store/store/auth.protoLOGIN 1、PASSWORD_RESET 2purpose列存枚举字符串名与policy.resource_type等既有模式一致。Store 方法从设计到实现的演进设计文档规划了 5 个 Store 方法实现中backend/store/email_verification_code.go做了几处关键演进值得注意设计文档中的方法实现中的对应方法差异说明UpsertEmailVerificationCode重置 attempts、覆盖 hashUpsertEmailVerificationCodeIfCooldownExpired把冷却判断与写入合并为一条原子 SQL防止并发重发穿透冷却GetEmailVerificationCodeGetEmailVerificationCode无行时返回(nil, nil)IncrementEmailVerificationCodeAttempts原子 1由verifyEmailCode中的claimLoginAttempt(ctx, email, LoginAttemptKind_EMAIL_CODE)承担设计文档提到的匹配 MFA 锁定阈值最终通过login_attempt表实现见 backend/migrator/migration/3.22/0012##login_attempt.sqlattempts 列保留但不再是唯一的尝试限制DeleteEmailVerificationCode一次性失效ConsumeEmailVerificationCodeDeleteEmailVerificationCodeIfMatch删除时匹配code_hash避免并发请求误删新码消费语义通过DELETE ... RETURNING保证恰好一次DeleteExpiredEmailVerificationCodesDeleteExpiredEmailVerificationCodes已接入后台清理任务见下文其中最核心的是原子冷却判断的实现这是防 TOCTOU 竞争的关键// UpsertEmailVerificationCodeIfCooldownExpired inserts or updates the row for (email, purpose), // but ONLY if no row exists OR the existing rows last_sent_at is older than the cooldown. // Atomic check-and-set: uses INSERT ... ON CONFLICT DO UPDATE ... WHERE ... RETURNING 1 func (s *Store) UpsertEmailVerificationCodeIfCooldownExpired(ctx context.Context, msg *EmailVerificationCodeMessage, cooldown time.Duration) (bool, error) { q : qb.Q().Space( INSERT INTO email_verification_code (email, purpose, code_hash, expires_at, last_sent_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT (email, purpose) DO UPDATE SET code_hash EXCLUDED.code_hash, expires_at EXCLUDED.expires_at, last_sent_at EXCLUDED.last_sent_at WHERE email_verification_code.last_sent_at EXCLUDED.last_sent_at - make_interval(secs ?) RETURNING 1 , msg.Email, msg.Purpose.String(), msg.CodeHash, msg.ExpiresAt, msg.LastSentAt, cooldown.Seconds()) ... }如果先读后写两个并发请求可能都通过冷却检查、都发出邮件而单条INSERT ... ON CONFLICT DO UPDATE ... WHERE ... RETURNING 1语句把检查 写入合二为一只有last_sent_at早于冷却窗口的请求才能成功写入并继续发信。注释特别说明使用RETURNING而非RowsAffectedPostgres 在DO UPDATE的WHERE过滤掉更新时受影响行数不可靠。另外sendEmailVerificationCode在 SMTP 发送失败时会调用DeleteEmailVerificationCodeIfMatch删除刚写入的行并匹配code_hash这样冷却不会阻塞立即重试同时不会误删并发请求写入的更新验证码。消费侧ConsumeEmailVerificationCode用DELETE ... WHERE code_hash ? AND expires_at NOW() RETURNING email两个并发请求提交同一验证码时恰好只有一个观察到consumedtrue保证一个验证码真的只能花一次。后台清理设计文档要求把DeleteExpiredEmailVerificationCodes挂到既有 sweeper与DeleteExpiredWebRefreshTokens同处。实现确认位于 backend/runner/cleaner/data_cleaner.go与DeleteExpiredVCSProviderUsers、DeleteExpiredOAuth2AuthorizationCodes、DeleteExpiredOAuth2RefreshTokens、DeleteExpiredOAuth2Clients、DeleteExpiredWebRefreshTokens等清理任务并列执行。API 表面最小侵入的 RPC 扩展修改LoginRequest新增email_code字段设计文档要求复用既有登录管道仅新增一个可选字段。仓库中 proto/v1/v1/auth_service.proto 的实际定义// 6-digit code from email for passwordless login/signup. // Pairs with email. Mutually exclusive with password and idp_name. optional string email_code 9 [ (bytebase.v1.audit_behavior) SENSITIVE, (buf.validate.field).string.max_len 64 ];注意两点增强audit_behavior SENSITIVE保证验证码不进入审计日志明文max_len 64在边缘即拒绝超大输入登录尝试锁定以 email 为键超大输入可被边界拦截。新增SendEmailLoginCode// Sends a 6-digit verification code to the email for login/signup. // Always returns success (no email enumeration). Enforces 60-sec resend cooldown. // Permissions required: None rpc SendEmailLoginCode(SendEmailLoginCodeRequest) returns (google.protobuf.Empty) { option (google.api.http) { post: /v1/auth:sendEmailLoginCode body: * }; option (bytebase.v1.allow_without_credential) true; option (bytebase.v1.audit) true; } message SendEmailLoginCodeRequest { string email 1; }allow_without_credential true使其对未认证用户开放audit true让该操作进入审计日志。改造ResetPasswordJWT Token → 验证码message ResetPasswordRequest { string email 1; string code 2; string new_password 3; }RequestPasswordReset签名不变但行为通过新的数据库行获得 60 秒冷却能力以及只给真实存在的活跃用户发信的存在性检查详见下文。后端服务层发送、校验、登录与重置的完整流程常量定义设计文档给出了 4 个常量实际实现backend/api/v1/auth_service_email_code.go中略有演进——emailCodeMaxAttempts被login_attempt锁定机制取代同时新增了发送预算相关常量const ( emailCodeLength 6 emailCodeExpiry 10 * time.Minute emailCodeResendCooldown 60 * time.Second // How much sign-in-code mail this deployment may generate per window. emailCodeSendWindow time.Hour emailCodeSendPerWindow 1000 errMsgInvalidEmailCode invalid or expired code )设计文档还要求从 backend/api/auth/tokens.go 移除passwordResetTokenDuration原 15 分钟 JWT、GeneratePasswordResetToken与GetEmailFromPasswordResetToken因为密码重置已完全切换为验证码机制。SendEmailLoginCode发送流程实现与设计文档的步骤大体一致但有一个显著差异实现选择同步发送以便调用方感知邮件设置缺失 / SMTP 不可达等可操作的失败——这不会造成枚举风险因为 LOGIN 用途对任何邮箱都尝试发送注册发生在校验阶段规范化邮箱并校验格式非法则返回InvalidArgument若调用方传了workspace先做域名白名单校验validateEmailWithDomains否则做通用邮箱格式校验validateEndUserEmail通过getAccountRestriction检查allow_email_code_signin关闭时直接FailedPrecondition先确认工作区会接受这个验证码再浪费一封邮件领取发送预算ClaimAttempt键signin-code窗口 1 小时 1000 次整部署共享预算耗尽返回ResourceExhaustederrSendBudgetExhausted设计文档中60 秒冷却 ≈ 60 封/小时是每收件人的下限发送预算则是对整部署发信声誉的保护生成 6 位验证码通过UpsertEmailVerificationCodeIfCooldownExpired原子写入哈希附带 60 秒冷却判断冷却中静默跳过返回成功创建 mailer 发送邮件失败时按code_hash回滚删除行返回Internal。设计文档中fire-and-forget goroutinecontext.WithoutCancel的异步形态最终被同步发送替代这是实现层面的明确取舍原因在代码注释中写明同步才能让调用方在邮件设置缺失或 SMTP 故障时得到明确错误。注意RequestPasswordReset仍保留吞掉错误的静默行为以避免邮箱枚举见下。Login的新分支authenticateEmailCodeLogin设计文档给出的分发优先级MFA → IDP → email_code → password在实现中对应authenticateEmailCodeLoginbackend/api/v1/auth_service_email_code.go第一步互斥校验。若同时携带password或idp_name返回InvalidArgumentemail_code is mutually exclusive with password and idp_name。第二步校验验证码。调用共享的verifyEmailCode见下。第三步按邮箱查用户分两条路径。已有用户直接返回用户交给下游管道继续。allow_email_code_signin与域名白名单的检查被推迟到validateLoginPermissions针对实际解析出的登录工作区执行——这对多工作区用户很重要resolveWorkspaceForLogin优先LastLoginWorkspace可能与首次被预邀请的工作区不同新用户注册工作区级门禁在创建用户之前执行防止孤儿账号解析目标工作区预邀请的工作区或自托管单例全新 SaaS 用户两者皆无则走 SaaS 默认目标工作区存在时allow_email_code_signin false→FailedPreconditiondisallow_signup true仅非 SaaS→ 拒绝域名白名单校验失败 → 拒绝先provisionResolvedWorkspace再创建用户——若用户创建失败下次重试可通过FindWorkspace(email)找到已供应的工作区自愈反向顺序会留下没有工作区的用户且后续重试会因GetUserByEmail提前返回而永远不重跑供应逻辑生成 32 字节随机密码并 bcrypt 哈希用户永远不可能知道这个随机密码验证码是其唯一入口之后可在个人资料中设置真实密码创建END_USER类型 principalname取邮箱 local-partemail.split()[0]返回新用户进入下游管道。共享校验助手verifyEmailCode实现相比设计文档多了两步保护但核心语义一致backend/api/v1/auth_service_email_code.go邮箱格式非法直接返回Unauthenticated垃圾输入不写行领取EMAIL_CODE类型的登录尝试额度claimLoginAttempt——猜测在验证码加载之前就被按身份限流锁定状态不会成为是否有待校验验证码的预言机这也正是设计文档中max attempts 5匹配 MFA 锁定阈值的落地形态读取行无行 →Unauthenticatedinvalid or expired codeexpires_at已过 → 按 hash 匹配删除 →Unauthenticatedinvalid or expired code常数时间比较HMAC 摘要subtle.ConstantTimeCompare——与既有challengeRecoveryCode一致避免时序侧信道不匹配 →Unauthenticatedinvalid or expired code行保留保证重发冷却始终有行可评估匹配 → 按 hash 删除一次性使用→ 清除登录尝试记录 → 返回成功。所有验证失败都返回统一的 invalid or expired code唯一的例外是登录尝试额度耗尽会暴露为可操作的错误提示用户去申请新验证码。密码重置的两条路径ResetPasswordbackend/api/v1/auth_service_email_code.go参数校验email / code / new_password 必填verifyEmailCode(ctx, email, PASSWORD_RESET, code)——验证码证明了对邮箱的控制权按邮箱查用户GetUserByEmail不存在则NotFound通过用户自身成员关系解析工作区绝不信任调用方传参校验新密码符合密码策略validatePasswordWithRestrictionbcrypt 哈希新密码并UpdateUser吊销该用户全部 web refresh tokenDeleteWebRefreshTokensByUser强制重新登录清除该邮箱的 PASSWORD 登录尝试记录——用户已通过邮箱控制权证明 新密码重置自己猜出来的锁定不应在重置后继续生效。RequestPasswordReset签名未变但共享的sendEmailVerificationCode助手为PASSWORD_RESET用途内置两道防护principal 存在性检查eligibleForPasswordReset要求账户存在、类型为END_USER且未删除否则静默返回不写行、不发信防止该端点被滥用来向任意地址投递重置邮件60 秒冷却与 LOGIN 路径相同的last_sent_at原子判断。RPC 本身无论成功与否都返回Empty不可枚举。LOGIN用途刻意跳过存在性检查——它同时承担注册职责未知邮箱 → 创建 principal。Post-authvalidateLoginPermissions的两处更新设计文档指出 SaaS 模式下默认DisallowPasswordSignin true会挡住所有非 IDP 登录因此必须做两处更新实现已确认豁免 email-code 登录isEmailCodeLogin分支跳过DisallowPasswordSignin检查——验证码登录是独立的认证方法不是密码在解析出的工作区上强制allow_email_code_signin对 email-code 登录校验resolveWorkspaceForLogin解析出的workspaceID确实开启了该开关存储错误时 fail-closed。这正确覆盖了用户的 LastLoginWorkspace 与首次预邀请工作区不同的多工作区场景。限流设计冷却 发送预算双层防线每收件人 60 秒冷却由last_sent_at列的原子条件 UPSERT 保证防 TOCTOU见 Store 层整部署发送预算signin-code桶1 小时窗口 1000 次emailCodeSendWindow/emailCodeSendPerWindow通过ClaimAttempt领取。设计文档中审计日志计数对无工作区的未认证请求无效的顾虑最终以独立的发送预算表解决登录尝试锁定EMAIL_CODE类型接入login_attempt机制backend/migrator/migration/3.22/0012##login_attempt.sql猜码被按身份限流与 MFA 的阈值语义对齐。前端设计第三 Tab 两步交互 重置页重写登录页Signin.vue新增第三个 Tab在 Standard密码与 IDP Tab 旁新增 Email code Tabt(auth.sign-in.email-code-tab)可见性由 actuator 返回的serverInfo?.allowEmailCodeSignin true控制即 proto/v1/v1/auth_service.proto 中Restriction.allow_email_code_signin。Tab 内是两步交互Step 1 (初始) 邮箱输入框 [ 发送验证码 ] 按钮 Step 2 (点击发送后) 邮箱输入框禁用修改链接可返回第一步 6 位验证码输入框NInputOtp自动聚焦 [ 重新发送 ] 按钮60 秒倒计时 [ 验证 ] 按钮未满 6 位时禁用输满自动提交前端 store 与提交流程auth.ts新增sendEmailLoginCode(email)既有login()只需透传emailCode字段提交authStore.login({ email, emailCode, web: true })下游与密码登录完全一致——响应含mfaTempToken则跳转MultiFactor.vue完成 TOTP否则fetchCurrentUser()后进入工作台倒计时点 Send code / Resend 后启动客户端 60 秒计时器按钮显示t(auth.sign-in.resend-in, { seconds: 45 })服务端无论客户端如何都会强制执行同一窗口。密码重置页从链接到验证码的完整重写PasswordForgot.vue小改同样的邮箱表单文案从链接改为验证码复用 Send code 冷却 UX成功后在 URL 中携带email跳转password-reset?email...PasswordReset.vue完整重写移除?token查询参数处理移除基于 token 的登录后强制重置流程该流程与验证码正交保留在既有UpdateUser路径表单邮箱从 query 预填 6 位验证码 新密码/确认密码 [重新发送]60 秒冷却 [重置密码]提交authServiceClientConnect.resetPassword({ email, code, newPassword })→ 跳转登录页预填 UX登录页点 Forgot password? 时携带邮箱password-forgot?email...→password-reset?email...前端移除auth/password-reset?tokenxxx处理、邮件中的GeneratePasswordResetToken重置链接构造。i18n 文案en-US 并同步 zh-CN / ja-JP / es-ES / vi-VNauth.sign-in.email-code-tab: Email code, auth.sign-in.send-code: Send code, auth.sign-in.resend-code: Resend code, auth.sign-in.resend-in: Resend in {seconds}s, auth.sign-in.code-sent-hint: Weve sent a 6-digit code to {email}, auth.password-reset.code-label: Verification code错误处理矩阵统一语义绝不枚举设计文档给出了完整的错误映射实现与之一一对应场景响应SendEmailLoginCode邮箱非法InvalidArgumentinvalid emailSendEmailLoginCode处于 60s 冷却Empty成功静默SendEmailLoginCode无 EMAIL 设置且无EMAIL_CONFIGEmpty成功记录警告实现改为同步返回Internal以暴露配置问题SendEmailLoginCodeSMTP 发送失败Empty成功记录警告实现改为同步返回InternalLoginemail_code 与 password/idp_name 同时携带InvalidArgumentmutually exclusiveLoginemail_code 无对应行Unauthenticatedinvalid or expired codeLoginemail_code 已过期删除行Unauthenticatedinvalid or expired codeLoginemail_code 尝试次数超限删除行Unauthenticatedtoo many attempts实现经 login_attempt 锁定且行保留以支撑冷却判断Loginemail_code 不匹配记录尝试Unauthenticatedinvalid or expired codeLoginemail_code 未知邮箱且预邀请工作区disallow_signuptrueUnauthenticatedaccount not found通用文案不泄露原因Loginemail_code 预邀请工作区allow_email_code_signinfalseFailedPreconditionemail code login is not enabled for this workspace在任何状态变更之前检查防止孤儿账号ResetPassword各类验证码错误与 Login 分支相同语义RequestPasswordReset处于 60s 冷却Empty成功静默三条安全原则实现均已确认无邮箱枚举SendEmailLoginCode与RequestPasswordReset对未知邮箱与已知邮箱的响应不可区分统一验证失败文案一律 invalid or expired code仅 too many attempts 会暴露让用户知道应重新申请验证码常数时间比较验证码哈希比较使用subtle.ConstantTimeCompare与既有challengeRecoveryCode保持一致。测试与上线策略单元测试设计文档规划了 backend/store/email_verification_code_test.go 与 backend/api/v1/auth_service_email_code_test.go 两组测试。后者在仓库中已存在覆盖发送预算相关行为例如TestSendEmailLoginCodeBudgetsEverySender发送预算对每个发送方计数TestSendEmailLoginCodeBudgetsWorkspacelessSends无工作区上下文的发送同样纳入预算TestSendEmailLoginCodeBudgetKeepsExistingCode预算耗尽时保留收件人已有的验证码先领预算再写行顺序的直接验证。设计文档要求的其他用例Upsert→Get 全字段一致、二次 Upsert 重置 attempts、双 purpose 并存、原子递增、删除、过期清理、verifyEmailCode的 happy/expired/attempts-exceeded/mismatch/missing-row、authenticateLogin的已有用户/新用户/互斥拒绝均在集成层有对应覆盖。集成测试backend/tests/auth_test.goSendEmailLoginCodeLogin(email_code)快乐路径 → 返回 token新用户自动创建60s 内重发 → 静默忽略存储行不变5 次错误验证码 → 第 6 次返回 too many attempts行删除过期验证码mock clock→ 登录失败密码重置全链路RequestPasswordReset→ResetPassword(email, code, new_password)→ 可用新密码登录旧 refresh token 全部吊销工作区allow_email_code_signin false→ email-code 登录被拒工作区disallow_signup true 未知邮箱 → 登录被拒 account not found既有用户不受影响2FA 交互启用 TOTP 的用户 → email-code 登录返回mfa_temp_token→ 走常规 MFA 流程完成。迁移与发布迁移文件已落地为 backend/migrator/migration/3.18/0000##add_email_verification_code.sql同步更新LATEST.sqlProto 生成流程cd proto buf format -w . buf lint buf generate无独立 feature flag由工作区设置allow_email_code_signin门控自托管默认falseSaaS 在配置EMAIL_CONFIG时自动为工作区置true后台清理DeleteExpiredEmailVerificationCodes已接入 backend/runner/cleaner/data_cleaner.goBreaking changeResetPassword签名从(token, new_password)变为(email, code, new_password)PR 标注breaking审计日志SendEmailLoginCode、Login、RequestPasswordReset、ResetPassword均声明option (bytebase.v1.audit) true且email_code字段标注SENSITIVE不进审计明文。设计到实现文档与代码的对照总结设计文档要点落地情况allow_email_code_signin工作区设置已在 proto/store/store/setting.proto字段 22与 v1Restriction字段 4中定义email_verification_code专表已建表并带expires_at索引另增workspace列用于门禁判断HMAC-SHA256 存哈希已实现hashEmailCode校验走subtle.ConstantTimeCompare60s 冷却防 TOCTOU已实现为UpsertEmailVerificationCodeIfCooldownExpired原子条件 UPSERT5 次尝试上限经login_attempt表的EMAIL_CODE类型锁定落地claimLoginAttemptattempts 列保留密码重置从 JWT 迁移到验证码已实现ResetPassword校验PASSWORD_RESET码并吊销 refresh token未知邮箱自动注册 工作区供应已实现authenticateEmailCodeLogin先门禁、先供应工作区、再建用户LoginRequest.email_code复用登录管道已实现字段 9SENSITIVE审计标注后台清理已接入data_cleaner.go前端第三 Tab / 两步 UI / 重置页重写设计文档描述的 UI 结构与 i18n 键已按规范拆解落地细节可继续在 frontend/src/routes/workspace/general/AccountSection.tsx 等前端文件中追踪需要说明的偏差设计文档中异步 fire-and-forget 发送在实现中被改为同步发送让调用方能感知邮件配置与 SMTP 故障每部署级的发送预算1 小时 1000 封被引入作为 60 秒冷却之外的整部署防轰炸防线。这些演进在代码注释中均有明确理由属于实现阶段的合理优化。小结一套可复用的验证码认证范式纵观设计文档与仓库实现Bytebase 的邮箱验证码登录为数据库治理工具提供了一套可复用的安全认证范式以(email, purpose)为主键的专用表承载有状态语义HMAC 常数时间比较保护 10^6 码空间的验证码原子条件 UPSERT 杜绝并发冷却穿透login_attempt锁定与发送预算双层限流抵御爆破与轰炸所有入口对未知邮箱静默成功以杜绝枚举验证码一次一用并在重置密码后强制吊销旧会话。若你正在为自己的产品设计类似的无密码认证这份设计与实现的对照可以成为一份完整的参考蓝本。【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表