数据库灾备【免费下载链接】databasusPostgreSQL backup tool with Point-In-Time-Recovery and restore verification项目地址https://gitcode.com/gh_mirrors/po/databasus点击查看免费下载导读本文基于开源仓库 openspec/specs/two-factor-authentication/spec.md 中的规格说明系统讲解 Databasus 的邮件双因子认证Two-Factor Authentication设计与实现。该功能允许实例所有者要求所有账号在密码登录之外再提供一个邮件验证码同时通过开启前前置条件校验验证码失效与限流主机控制台逃生命令等机制避免管理员把自己锁在门外。读完本文你将掌握该功能的完整开关逻辑、两步登录协议、验证码生命周期管理以及邮件服务器故障时的恢复路径并了解其在 backend 源码中的具体落点。一、功能定位实例级全局开关默认关闭双因子认证在 Databasus 中不是每个账号各自启用/关闭的选项而是一个实例级instance-wide全局开关实例首次部署并被认领claimed时开关默认处于关闭状态此时登录仅需邮箱地址 密码开关一旦被管理员打开对实例上每一个账号生效包括初始化时记录的引导管理员bootstrap administrator只有管理员可以修改该设置普通成员提交的修改会被拒绝且设置保持不变打开开关不会中断已有会话开关生效前签发的访问令牌access token在过期或被改密失效之前持续可用。在数据模型层面该开关对应users_settings表中的is_two_factor_auth_required布尔列由迁移 backend/migrations/20260920215925_add_two_factor_auth.sql 以NOT NULL DEFAULT FALSE方式加入保证任何已部署实例在迁移后立即处于默认关闭的安全初始状态。模型定义见 backend/internal/features/users/models/users_settings.go。权限校验位于设置服务 settings_service.go 的UpdateSettings!updatedBy.CanUpdateSettings()时直接拒绝只有能更新设置的管理员才能翻转该开关。二、开启前置条件没有可达的收件人就拒绝开启开启双因子认证是可能把整个实例锁死的动作因此规格要求只有当以下两个条件同时成立时管理员才能打开开关否则请求被拒绝且拒绝信息必须明确点出失败的是哪个条件实例已配置邮件服务器mail server每一个处于激活状态的active管理员账号都持有语法上合法的邮箱地址syntactically valid email address。如果某管理员地址不合法拒绝信息必须指名是哪一个账号。拒绝提示还会附带如何配置邮件服务器的文档链接且设置界面是否展示该选项、是否提示不可用必须与后端拒绝逻辑使用同一个判断依据即实例自身的IsEmailConfigured答案避免界面与后端对同一实例得出不同结论。该逻辑在SettingsService.checkCodesCanReachEveryAdmin中实现settings_service.go先检查s.IsEmailConfigured()底层为emailSender ! nil emailSender.IsConfigured()再枚举所有管理员GetAdmins跳过非激活账号用validator.Var(admin.Email, required,email)校验地址——规格特别点名的场景是早期实例创建的登录名admin非邮箱值会导致开启被拒并点名该账号。一个值得注意的细节是关不受限制当开关已开启而邮件服务器后来故障时仍处于登录状态的管理员可以随时在设置界面把开关关掉因为只有打开才需要前置条件校验request.IsTwoFactorAuthRequired !existingSettings.IsTwoFactorAuthRequired分支。这保证了开启是唯一被防守的锁门动作。三、两步登录协议密码正确 ≠ 拿到令牌当开关开启时一次成功的登录被拆成两个 HTTP 步骤路由注册见 backend/internal/features/users/controllers/user_controller.go步骤端点请求载荷成功响应1. 密码步骤POST /users/signin邮箱 密码待处理登录标识pendingSignInId不返回令牌2. 验证步骤POST /users/verify-signin-codependingSignInId 六位码访问令牌在服务层UserService.SignInuser_services.go依次完成查邮箱 → 校验账号状态邀请中/停用均拒绝→ 确认账号有密码哈希纯 OAuth 账号无密码按密码错误同样拒绝避免空指针→ bcrypt 比对密码 → 读取实例设置。只有密码通过且settings.IsTwoFactorAuthRequired true时才调用StartTwoFactorSignIn进入发码流程否则走原有单步登录直接签发令牌。3.1 验证码的生成与存储验证码的生成逻辑在 backend/internal/features/users/services/two_factor_auth.go 的generateSixDigitCode从crypto/rand密码学安全随机源读取 4 字节binary.BigEndian.Uint32 % 1000000后按%06d格式化为六位数字明文六位码从不落库仅用 bcryptbcrypt.GenerateFromPassword默认 cost保存HashedCode数据库字段本身在 GORM 模型中标为json:-不外泄见 backend/internal/features/users/models/two_factor_code.go每条待处理登录在two_factor_codes表留一行记录字段包括user_id、password_creation_time、expires_at、is_used、failed_attempt_count、created_at并通过外键ON DELETE CASCADE关联用户表。3.2 邮件内容邮件由 two_factor_email.go 渲染主题为Sign-In Code正文是一封 HTML 邮件其中必须包含规格要求的三个要素六位验证码大字、等宽、字母间距 8px 突出显示This code stops working10 minutesafter it was sent10 分钟后失效Requesting another code replaces this one, so only the newest code works重新请求会替换旧码。邮件还会提醒如果你没有尝试登录请修改密码有人知道了你的密码。3.3 密码未验证前绝不发码规格明确必须先验证密码才能生成或发送任何验证码。未知地址或错误密码的登录尝试必须满足三不不发送邮件、不创建待处理登录、响应与开关关闭时完全一致——从而不泄露该地址是否存在或双因子是否开启。这一点由上述SignIn的调用顺序天然保证所有密码校验第 144-184 行都发生在读取设置与调用发码逻辑第 186 行之后之前。四、验证码生命周期过期、猜测上限、重发与小时配额这是整个规格最细致也最值得展开的部分全部常量定义在 two_factor_auth.go规则取值常量/实现点验证码有效期10 分钟twoFactorCodeLifetime 10 * time.Minute错误码上限5 次达到即销毁待处理登录MaxTwoFactorCodeAttempts 5two_factor_code.go重发间隔同一账号每分钟最多 1 次twoFactorResendWindow time.Minute限流 scopesignin-code-resend小时发码配额同一账号每小时最多 5 个码maxTwoFactorCodesPerHour int64 5记录保留1 小时与配额统计窗口一致twoFactorCodeRetention time.Hour4.1 一次性使用与并发安全规格对一次性的要求非常严格同一验证码完成过一次登录后再次提交必须被拒绝两个携带同一正确验证码的并发请求同时到达时恰好只有一个能拿到令牌。实现上VerifyTwoFactorCodetwo_factor_auth.go先通过ClaimAttempt原子占用一次尝试额度并发请求中只有一次能成功 claim再比对 bcrypt 哈希最后通过SpendCode原子花掉该码——SpendCode返回 false已被花掉即拒绝从而保证恰好一个成功。4.2 错误码与并发猜测错误码上限同样具备并发安全性refuseWrongTwoFactorCode在attemptCount达到 5 时销毁待处理登录并写入审计日志Two-factor sign-in abandoned after too many incorrect codes。规格要求并发到达的错误码共享同一个 5 次上限绝不超过 5 个码被实际检查——这由ClaimAttempt的原子占用保证超出额度的请求连码都不会被比对。4.3 重发resendResendTwoFactorCodetwo_factor_auth.go的逻辑非常讲究顺序先加载当前可用的待处理登录不存在/已用/已销毁/过期则拒绝检查每分钟 1 次的重发限流超限直接拒绝且不发邮件调用issueTwoFactorCode生成新码并尝试入库受小时配额约束只有新码确实发出/落库后才把旧码MarkCodeAsUsed标记失效。代码注释明确说明这样设计的原因The previous code is invalidated only once the replacement is on its way, so a refused resend leaves the user with the code they already have——被拒绝的重发不会把用户手上还能用的旧码弄失效。响应体中携带的是新待处理登录的pendingSignInId。4.4 小时配额与回到密码页的友好语义规格包含一个反直觉但很贴心的设计重复执行密码步骤不会白费小时配额。当账号已存在一个存活的待处理登录未过期、未使用、未被错误码销毁、且是当前密码创建的时再次提交正确密码会原样返回同一个 pendingSignInId且不发送第二封邮件。实现上这是issueTwoFactorCode的shouldReuseLiveCodetrue分支CreateCodeUnlessLive发现存活记录则直接返回它。两个并发密码步骤之间也保证只产生一条记录、一封邮件。但有两类情况会打破复用密码已改变待处理登录记录了创建时的PasswordCreationTime用户改密后用新密码登录会得到一个全新待处理登录规格场景改密后再登录。这正是模型注释所说的The password the first step accepted, so the second step can refuse a pending sign-in whose account has changed its password since。小时配额耗尽CreateCodeWithinHourlyCap在配额耗尽时返回ErrHourlyCodeCapReached最终映射为ErrTooManySignInCodes响应明确告知请求了太多验证码而不是把用户引向一个永远不会到达邮件的验证码页面。4.5 过期记录清扫SweepPendingSignInsPastRetention通过DeleteCodesCreatedBefore(now - 1h)清理超过保留窗口的记录。规格要求待处理登录保留的时长与其配额统计的窗口一致且不再保留之后——表上同时建有idx_two_factor_codes_user_id与idx_two_factor_codes_created_at索引见迁移文件后者正是供小时配额统计与定时清扫两条路径共同读取created_at使用。注意销毁destroyed的记录行会保留到清扫为止因为小时配额仍要统计它。五、验证步骤的二次复核签发令牌前重新检查规格要求在签发访问令牌之前实例必须重新确认待处理登录背后的账号现在仍然可以进入账号仍然存在账号仍然处于激活状态active密码自待处理登录创建以来没有改变。任一条件不满足无论提交什么验证码都拒绝。这些检查集中在loadUsablePendingSignIntwo_factor_auth.goif pendingCode nil || !pendingCode.IsValid() { /* 拒绝 */ } user, err : s.userRepository.GetUserByID(ctx, pendingCode.UserID) // 账号仍存在 if !user.IsActiveUser() { /* 拒绝账号被停用 */ } if !user.PasswordCreationTime.Truncate(time.Microsecond).Equal( pendingCode.PasswordCreationTime.Truncate(time.Microsecond)) { /* 拒绝密码已变 */ }注释点明了设计意图The pending sign-in proves that a password was correct minutes ago, not that the account may be let in now。同时IsTwoFactorAuthRequired故意不参与二次复核待处理登录在开关开启时创建、但验证发生在开关被关闭之后仍然可以完成——不能让关闭开关把已经收到验证码的用户晾在半路。该行为由测试Test_VerifySignInCode_AfterTheSettingWasSwitchedOff_StillCompletes锁定。六、第二道门同样受防滥用保护验证码端点与密码登录、密码重置请求享有同等强度的自动化滥用防护验证与重发请求在执行任何验证码校验、发送任何邮件之前先经过与登录一致的防护检查包括实例配置了人机验证挑战如 Cloudflare Turnstile 时的挑战校验请求必须以实例最近一次返回的pendingSignInId来指名待处理登录绝不接受邮箱地址作为指名方式——因此仅知道别人的邮箱地址无法消耗该账号的尝试次数、重发额度或小时配额。这正是规格中用地址指名账号场景并发场景下无法消耗对方额度被拒绝的原因。实现上VerifySignInCodeRequestDTO与ResendSignInCodeRequestDTO均以binding:required强约束pendingSignInId字段见 backend/internal/features/users/dto/dto.go而验证码校验逻辑只从GetCodeByID(pendingSignInID)出发全程不接触地址这一维度的输入。错误响应还带有稳定的错误码供前端翻译见respondToSignInErroruser_controller.go错误码HTTP 状态含义sign_in_code_incorrect400验证码错误pending_sign_in_not_usable410 Gone待处理登录已用/已销毁/已过期/账号不再可用sign_in_code_not_sent503验证码未能发送邮件服务器故障too_many_sign_in_codes429超过小时配额sign_in_code_resent_too_soon429一分钟内重复重发七、失败即关闭Fail Closed发不出码就不放行规格要求如果实例无法发送验证码登录必须失败响应明确说明验证码无法发送code could not be sent且不签发访问令牌。这意味着开关开启期间如果邮件服务器停止工作实例在密码登录路径上就不会放任何人进入。issueTwoFactorCode的兜底逻辑two_factor_auth.go分两层实现入口检查s.emailSender nil || !s.emailSender.IsConfigured()时直接返回ErrSignInCodeNotSent在生成验证码之前就拒绝发送失败兜底SendEmail返回错误时把刚创建的待处理登录MarkCodeAsUsed标记为不可用再返回ErrSignInCodeNotSent——保证用户永远不会看到一个对应邮件不会到达的验证码页面。注释精确描述了这一设计Nothing is created before the instance has said it can deliver, and a send that fails leaves the row it counted behind while making it unusable。这一关闭则锁死的行为由Test_SignIn_WhenTheMailServerIsMissing_FailsClosed与Test_SignIn_WhenTheSendFails_FailsClosed两个测试用例锁定。八、明确边界外部身份提供商不受双因子约束规格明确通过 Google 或 GitHub 的第三方登录不受邮件双因子约束无论开关是否开启都直接签发访问令牌、不需要邮件验证码。这是有意为之的例外——这些提供商自身执行各自的多因子校验允许它们接入的实例等于接受双因子只守护密码登录。相关接口与文档均不得声称双因子覆盖外部身份提供商。路由注册中可见OAuth 回调POST /auth/github/callback、POST /auth/google/callback是独立端点不经过验证码流程且SignIn中账号没有密码哈希则按密码错误拒绝的分支正好覆盖了纯 OAuth 账号无法走密码双因子路径的情况。九、主机逃生通道单命令关闭开关当邮件服务器彻底不可用、管理员又收不到任何验证码时规格要求实例所有者仅凭一条命令就能在主机上关闭全局开关无需手工编辑数据库./backend --disable-2fa对应实现位于 backend/cmd/main.go 与disableTwoFactorAuthIfRequestedbackend/cmd/main.go命令行标志名刻意写作disable-2fa注释说明这是被死邮件服务器锁在门外的主人会搜索的术语The name carries a digit, unlike every other flag here命令调用SettingsService.DisableTwoFactorAuthsettings_service.go设置已关闭时返回false并打印 already off. Nothing changed.幂等、无副作用真正翻转时打印 Two-factor authentication is now off. 并向审计日志写入一条记录isTwoFactorAuthRequired: true - false (switched off from the host console)——让从主机移动安全开关这一动作无法被无声完成命令的信任级别与--new-password密码重置、--list-admins管理员列表一致只有已经能在运行实例内执行命令的人才能使用不额外要求登录态。这正好构成一条完整的恢复路径实例死锁 → 主机执行--disable-2fa→ 审计日志留下痕迹 → 下一次密码登录无需验证码。十、配套文档与测试佐证规格的最后一条要求是把双因子作为产品安全叙事的一部分仓库 README 与官网安全页、首页安全问答需要在所有发布语言中声明登录可要求邮件验证码且不得声称覆盖外部身份提供商--disable-2fa命令需要与其他恢复命令密码重置等并列文档化让被死邮件服务器锁定的主人能在找密码恢复的地方找到它。实现与行为的可信度由一套针对性测试保障核心用例集中在 backend/internal/features/users/controllers/two_factor_controller_test.go覆盖规格中的关键场景两步登录完成、验证码仅以哈希存储、邮件内容要素Test_SignIn_WhenTheSecondFactorIsOn_...、Test_SignIn_CodeMessage_CarriesTheCodeAndTheRulesItFollows一次性使用、过期拒绝、跨待处理登录的码无效、5 次错误后正确码也失效Test_VerifySignInCode_...系列一分钟内重发被拒、重发后旧码失效、存活待处理登录复用不发第二封邮件、小时配额耗尽提前拒绝Test_SignIn_WhileAPendingSignInIsStillLive_ReturnsItAndSendsNothing等账号停用/改密后拒绝、并发同码仅一令牌、并发错误码不超过上限、设置关闭后验证仍可完成、拒绝错误码可被界面翻译。总结Databasus 的邮件双因子认证在安全增强与防锁死之间做了精心权衡实例级开关默认关闭、开启前校验邮件服务器与管理员邮箱可达性、密码验证通过后才发码、10 分钟有效期的六位 bcrypt 哈希码、5 次错误上限与并发安全的一次性消费、1 分钟重发限流与 5 码/小时配额、签发令牌前的二次复核、发码失败即拒绝登录外加外部 OAuth 明确豁免与主机--disable-2fa逃生命令。这套机制从规格openspec/specs/two-factor-authentication/spec.md到实现two_factor_auth.go、settings_service.go、20260920215925_add_two_factor_auth.sql再到测试层层对应适合作为理解该功能行为契约与运维恢复路径的一手参考。赞分享数据库灾备【免费下载链接】databasusPostgreSQL backup tool with Point-In-Time-Recovery and restore verification项目地址https://gitcode.com/gh_mirrors/po/databasus点击查看免费下载相关推荐Databasus 邮件双因素认证Email 2FA设计解析从失败关闭到控制台逃生通道Databasus 邮件双因素认证Email 2FA设计解析从失败关闭到控制台逃生通道 导读 本文基于 Databasus 仓库中 openspec/ch数据库灾备Databasus 邮件二次认证Email 2FA完整实现指南从全局开关到两步登录与主机逃生通道Databasus 邮件二次认证Email 2FA完整实现指南从全局开关到两步登录与主机逃生通道 本文基于仓库中 openspec/changes/arc数据库灾备File structure of working directory {{folder}}File structure of working directory {{folder}} this is filtered overview not ful数据库灾备上一篇Kubernetes Handbook 视角下的 CNCF 2020 年度报告解读从云原生生态扩张到安全认证升级下一篇ant-design-mobile Footer 页脚组件完全指南属性、事件与 CSS 变量定制创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考