ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

TREK 两步验证(2FA)实战指南:TOTP 配置、备份码、登录流程与管理员强制策略

TREK 两步验证(2FA)实战指南:TOTP 配置、备份码、登录流程与管理员强制策略 TREK 两步验证2FA实战指南TOTP 配置、备份码、登录流程与管理员强制策略【免费下载链接】TREKA self-hosted travel/trip planner with real-time collaboration, interactive maps, PWA support, SSO, budgets, packing lists, and more.项目地址: https://gitcode.com/GitHub_Trending/nomad22/TREKTREK 是一款自托管的旅行规划应用支持实时协作、交互式地图与 PWA 离线使用。为保证账号安全TREK 内置了基于时间的一次性密码TOTP两步验证无论你是普通用户想为自己的账号加一道保险还是管理员需要在全站强制开启 2FA本文都能提供从界面操作到服务端实现原理的完整指引。读完本文你将掌握 TREK 2FA 的完整生命周期——启用、登录、禁用、策略强制——并理解其背后的加密存储、令牌设计与限流机制。什么是 TREK 的 2FA标准 TOTP 实现TREK 采用Time-based One-Time PasswordTOTP基于时间的一次性密码作为第二重身份验证。这一实现完全兼容业界通用的 TOTP 标准因此你可以使用以下任意一款验证器应用Google AuthenticatorAuthy1Password任何支持 TOTP 的标准验证器应用当 2FA 处于启用状态时每次登录在输入邮箱与密码之后TREK 还会要求你输入 6 位动态验证码或一张备份码只有两步全部通过才会建立会话。从服务端实现看TOTP 的核心校验由otplib库承担见 authService.ts。其中一处容易被忽略但值得注意的配置是authenticator.options { window: 1 };window: 1表示验证器在时间上允许 ±1 步即前后各 30 秒的容差可以容忍验证器与服务器之间的轻微时钟偏差同时不会过度放宽安全性。设置 2FA从扫码到保存备份码进入Settings设置→ Account账户点击Set up two-factor authentication设置两步验证即可开启整个流程。操作步骤扫描二维码页面会同时展示一个 QR 码和一个文本形式的密钥secret。用你的验证器应用扫描二维码或手动输入文本密钥完成绑定。注意设置会话的有效期只有15 分钟。如果在此窗口内没有完成设置需要重新发起设置流程。输入验证码在验证器应用中查看当前 6 位动态码输入后点击Confirm确认。保存备份码系统会展示10 张备份码backup codes。这些是一次性single-use的救援码且仅展示这一次——请将它们妥善保存到密码管理器或打印纸质副本。每张备份码的格式为XXXX-XXXX8 位大写十六进制字符中间以连字符分隔。完成2FA 即在此账号上生效后续登录将强制进行两步验证。源码视角设置流程的后端实现设置流程涉及三个接口均定义于 auth.controller.tsPOST /api/auth/mfa/setup生成随机密钥、构造otpauth://URI 并渲染 SVG 二维码POST /api/auth/mfa/enable校验用户输入的 6 位码通过后启用 2FA 并返回备份码POST /api/auth/mfa/disable关闭 2FA见下文禁用 2FA后端实现细节见 authService.ts 的setupMfa与enableMfaconst MFA_SETUP_TTL_MS 15 * 60 * 1000; // 15 分钟 const mfaSetupPending new Mapnumber, { secret: string; exp: number }(); const MFA_BACKUP_CODE_COUNT 10;15 分钟窗口生成的密钥并不会立即写入数据库而是先存进内存中的mfaSetupPendingMap附带 15 分钟过期时间getPendingMfaSecret在读取时会校验是否过期并自动清理。这就是文档中设置会话 15 分钟过期的底层来源。密钥生成与二维码authenticator.generateSecret()生成随机密钥authenticator.keyuri(userEmail, TREK, secret)构造标准的otpauth://URIissuer 为TREK再由qrcode库渲染成 SVG 供前端展示。启用校验enableMfa用authenticator.verify({ token, secret })校验用户输入的验证码校验通过后才真正启用并生成 10 张备份码。关键安全设计TOTP 密钥加密存储TREK 并没有把 TOTP 密钥明文存放在 SQLite 数据库中。在 mfaCrypto.ts 中密钥在落库前会经过AES-256-GCM加密function getKey(): Buffer { return crypto.createHash(sha256).update(${ENCRYPTION_KEY}:mfa:v1).digest(); } export function encryptMfaSecret(plain: string): string { const iv crypto.randomBytes(12); const cipher crypto.createCipheriv(aes-256-gcm, getKey(), iv); const enc Buffer.concat([cipher.update(plain, utf8), cipher.final()]); const tag cipher.getAuthTag(); return Buffer.concat([iv, tag, enc]).toString(base64); }加密密钥由服务端的ENCRYPTION_KEY环境变量派生每次加密都使用随机 12 字节 IV并附带 GCM 认证标签用于防篡改。这意味着即使数据库文件被拖走攻击者也无法直接获得可用于伪造 TOTP 码的明文密钥。数据库侧的字段定义见 schema.tsmfa_enabled INTEGER DEFAULT 0, mfa_secret TEXT, mfa_backup_codes TEXT,备份码的存储方式bcrypt 哈希备份码不是以明文存储的。enableMfa中调用generateBackupCodes()生成 10 张形如XXXX-XXXX的码随后经hashBackupCodeBcryptbcryptcost 10哈希后才写入mfa_backup_codes字段const backupCodes generateBackupCodes(); const backupHashes backupCodes.map(hashBackupCodeBcrypt); db.prepare(UPDATE users SET mfa_enabled 1, mfa_secret ?, mfa_backup_codes ?, ...).run( enc, JSON.stringify(backupHashes), userId );源码注释解释了为什么不用简单的 SHA-256备份码只有约 40 位熵8 位十六进制字符一旦数据库泄露明文 SHA-256 彩虹表可以在几分钟内破解改用 bcrypt 中等 cost 后破解成本提升约 34 个数量级。同时代码保留了旧版 SHA-256 哈希的兼容校验路径matchBackupCode会同时识别 bcrypt 与 legacy SHA-256 格式旧版本启用过 2FA 的用户无需重新绑定验证器。使用 2FA 登录两步验证与 5 分钟会话令牌启用 2FA 后登录流程变为两步输入邮箱与密码提交登录。服务端验证密码通过后不会立即签发会话而是返回mfa_required: true与一个短期的mfa_token前端随即展示第二个输入框。在此输入框填写以下任一内容验证器应用中当前的6 位动态码一张备份码格式XXXX-XXXX。每张备份码只能使用一次使用后即被消耗。第二步必须在5 分钟内完成否则中间会话令牌过期需要重新输入密码。这是因为mfa_token在签发时被显式设置为 5 分钟有效期const mfa_token jwt.sign( { id: Number(user.id), purpose: mfa_login, pv }, JWT_SECRET, { expiresIn: 5m, algorithm: HS256 } ); return { mfa_required: true, mfa_token };登录流程源码解析完整的第二步校验在verifyMfaLoginauthService.ts中实现对应接口POST /api/auth/mfa/verify-loginauth-public.controller.ts。其核心逻辑是校验中间令牌jwt.verify(mfa_token)并检查purpose mfa_login防止令牌被挪作他用。校验 TOTP 或备份码先尝试 TOTP 验证失败则遍历备份码哈希列表用matchBackupCode做常量时间比较crypto.timingSafeEqual匹配后从列表中删除该码——这就是备份码只能使用一次的实现方式。签发正式会话通过后更新last_login与login_count生成常规 JWT 会话令牌并写入审计日志action: user.login, details: { mfa: true }。密码重置场景下的 2FA值得特别说明的是即使走忘记密码流程MFA 也不会被绕过。resetPassword中设置了 MFA 门槛——如果目标账号已启用 2FA用户必须同时提供有效的 TOTP 码或备份码才能完成重置若未提供接口返回mfa_required: true。这意味着攻击者即使拿到重置邮件链接也无法仅凭邮件接管一个受 2FA 保护的账号。重置成功后password_version递增所有旧会话被吊销且本次使用的备份码会同步消耗。禁用 2FA密码 动态码双重校验关闭 2FA 同样在Settings → Account页面点击Disable two-factor authentication禁用两步验证需要同时提供两项凭据当前账号的密码验证器应用中的一张有效TOTP 动态码disableMfaauthService.ts的实现正是依次校验bcrypt.compareSync(password, user.password_hash)与authenticator.verify({ token, secret })两者都通过才会清除mfa_enabled、mfa_secret、mfa_backup_codes三个字段。注意如果管理员已在全站强制 2FA见下文普通用户无法自行禁用2FA——服务端会直接返回403Two-factor authentication cannot be disabled while it is required for all users。这防止了用户通过先关 2FA 再操作账号的方式绕过安全策略。管理员强制 2FArequire_mfa 全局策略管理员可以在管理面板中开启require_mfa全站强制 2FA设置要求所有用户都启用 2FA 才能使用应用。启用前置条件管理员必须先自证安全开启强制策略前管理员自己必须先启用 2FA或绑定用户验证型 passkey服务端会拒绝不符合条件的变更请求。这一检查位于 authService.ts 的updateAppSettingsif (require_mfa true || require_mfa true) { const adminMfa db.prepare(SELECT mfa_enabled FROM users WHERE id ?).get(userId); const adminHasPasskey !!db.prepare(SELECT 1 FROM webauthn_credentials WHERE user_id ? LIMIT 1).get(userId); if (!(adminMfa?.mfa_enabled 1) !adminHasPasskey) { return { error: Secure your own account with two-factor authentication or a passkey before requiring it for all users., status: 400, }; } }也就是说管理员可以**先用 TOTP 保护自己的账号也可以注册一枚用户验证型 passkeyWebAuthn**来满足前置条件。从源码注释看passkey 之所以被认可是因为用户验证型 passkey 本身具有抗钓鱼能力天然构成双因素与 TOTP 在策略上等价。策略实施登录后 403 MFA_REQUIRED当require_mfa生效且某账号未启用 2FA 时该账号登录后的任何 API 请求都会被中间件拦截并返回HTTP 403响应体为{ error: Two-factor authentication is required. Complete setup in Settings., code: MFA_REQUIRED }客户端检测到MFA_REQUIRED后会重定向到Settings → Account引导用户完成 2FA 设置在设置完成前该账号无法使用应用的其他任何功能。这套拦截逻辑实现在 mfaPolicy.ts 的enforceGlobalMfaPolicy中间件中。它遵循以下豁免规则公开路径isPublicApiPath直接放行如GET /api/health、GET /api/auth/app-config、POST /api/auth/login、POST /api/auth/register、POST /api/auth/mfa/verify-login等MFA 设置相关路径isMfaSetupExemptPath放行如GET /api/auth/me、POST /api/auth/mfa/setup、POST /api/auth/mfa/enable以及 passkey 注册/校验相关的端点——确保未启用 2FA 的用户可以顺利完成设置已有 TOTP 或 passkey 的用户放行中间件同时检查mfa_enabled字段与webauthn_credentials表两者任一满足即视为已满足策略Demo 模式下的演示账号放行DEMO_MODE环境下。同时该中间件兼容两种认证载体httpOnly 会话 Cookie常规 SPA 用户与Authorization请求头MCP / API 客户端并且复用了统一的 JWT 校验包含password_version门槛避免绕过。管理员善后能力如果有用户被 2FA 锁在门外例如丢失了验证器与备份码管理员可以从管理面板为其重置 2FA详见 Admin-Users-and-Invites。限流保护IP 维度的暴力破解防御为防止攻击者暴力尝试动态码或密码TREK 对相关接口实施了基于 IP 的限流超出限制即返回HTTP 429需等待窗口重置后重试接口限制登录POST /api/auth/login每 15 分钟 10 次尝试MFA 验证码校验POST /api/auth/mfa/verify-login每 15 分钟 5 次尝试实现层面限流由RateLimitService与固定 15 分钟窗口完成见 auth-public.controller.tsconst WINDOW 15 * 60 * 1000; ... this.limit(login, req, 10); // 登录10 次/15 分钟 this.limit(mfa, req, 5); // MFA 校验5 次/15 分钟此外TREK 还在登录响应上加入了最小延迟填充LOGIN_MIN_LATENCY_MS 350——即使校验失败也会等待补齐 350ms 再返回以拉平成功与失败请求的时间差异进一步抵御基于响应时间的账号枚举。Demo 用户限制在 Demo 模式DEMO_MODEtrue下演示账号不能启用或禁用 2FA。服务端在setupMfa、disableMfa、changePassword等关键操作前都会检查isDemoEmail(userEmail)并返回403。同时enforceGlobalMfaPolicy中间件对 Demo 邮箱也做了豁免避免演示账号因全局 2FA 策略被锁定。这一设计保证了任何人无需注册即可体验 TREK 的核心功能同时保护演示环境的稳定性。结语与相关文档TREK 的 2FA 实现兼顾了易用性标准 TOTP、主流验证器全兼容、备份码救援与安全性AES-256-GCM 加密密钥、bcrypt 哈希备份码、5 分钟中间令牌、严格限流、管理端强制策略。从设置界面到中间件拦截再到管理面板的全局开关每一环都能在 authService.ts、mfaPolicy.ts、mfaCrypto.ts 中找到对应实现前端行为也由 AccountTab.test.tsx 中的测试用例FE-COMP-ACCOUNT-022 至 031覆盖验证。如需深入了解登录与注册的整体流程、管理端权限体系或账户设置可继续阅读Login-and-RegistrationAdmin-PermissionsAdmin-Users-and-InvitesUser-Settings【免费下载链接】TREKA self-hosted travel/trip planner with real-time collaboration, interactive maps, PWA support, SSO, budgets, packing lists, and more.项目地址: https://gitcode.com/GitHub_Trending/nomad22/TREK创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表