广告位 · 页头(后台-模块管理-新建模块后替换此区域)

如何基于Rodauth实现TOTP与U2F硬件密钥双因素认证?

Rodauth 作为 Ruby 生态中的认证库,采用 feature 机制组织认证能力。双因素认证的入口是 two_factor_base,它本身不实现算法,而是维护一次登录过程中的 2FA 状态。要支持 TOTP 和 U2F 硬件密钥,需要分别启用 otp 与 webauthn。两者可以叠加使用,也可以单独部署。下面从模块配置、TOTP 细节、U2F 流程和组合策略四个部分展开。

如何基于Rodauth实现TOTP与U2F硬件密钥双因素认证?

一、模块组成与最小配置

Rodauth 的插件系统允许在初始化认证模块时按需启用 feature。双因素认证部分必须先打开 two_factor_base,否则 otp 和 webauthn 的流程缺少统一的会话状态入口。最小配置通常会同时启用 two_factor_base、otp 和 webauthn,如果需要备份码,还可以加上 recovery_codes。启用后 Rodauth 会自动注册 /otp-setup、/otp-auth、/webauthn-setup、/webauthn-auth 等路由,不需要为这些路径手写控制器。

以下是一个基础配置示例,展示了如何在 Roda 应用中挂载 Rodauth 并打开相关 feature。

plugin :rodauth do
  enable :login, :logout, :two_factor_base, :otp, :webauthn
  two_factor_confirm_password true
  webauthn_origin "https://app.ipipp.com"
  webauthn_rp_name "My Application"
end

数据库层面,这些 feature 需要额外的列来保存状态和凭证。可以在迁移中为账号表添加 two_factor_status、otp_secret、otp_last_use、webauthn_id、webauthn_public_key 和 webauthn_sign_count。其中 otp_secret 保存 TOTP 共享密钥,webauthn_sign_count 用来记录硬件密钥的签名计数器,防止设备被克隆。下面是一个 Sequel 迁移片段。

Sequel.migration do
  change do
    alter_table(:accounts) do
      add_column :two_factor_status, Integer, default: 0
      add_column :otp_secret, String, text: true
      add_column :otp_last_use, Integer
      add_column :webauthn_id, String, text: true
      add_column :webauthn_public_key, String, text: true
      add_column :webauthn_sign_count, Integer
    end
  end
end

二、TOTP 的实现细节与验证逻辑

TOTP 的全称是基于时间的一次性密码,算法定义在 RFC 6238。它把当前 Unix 时间按 30 秒一个窗口切分,将窗口序号作为计数器,和 base32 编码的共享密钥一起送入 HMAC-SHA1,再从哈希结果中通过动态截断得到 6 位数字。只要客户端和服务端时钟偏差不大,双方就会生成相同的动态码。Rodauth 的 otp feature 并不重新实现这套算法,而是依赖 ROTP gem 完成生成与验证。

启用 otp 后,用户进入 /otp-setup 页面时,Rodauth 首先生成一个 base32 密钥,再构造 provisioning URI,通常以二维码形式展示给用户。URI 形如 otpauth://totp/App:user@ipipp.com,包含密钥和 issuer 参数。保存二维码时还需要用户填写一次当前验证码,以确认密钥已经正确写入认证器。Rodauth 的 otp_setup 和 otp_auth 两个 route 分别处理设置和登录验证。

require "rotp"
secret = ROTP::Base32.random
totp = ROTP::TOTP.new(secret, issuer: "MyApp")
uri = totp.provisioning_uri("user@ipipp.com")
puts uri

验证环节中,Rodauth 会调用 ROTP 的 verify 方法,返回值可能是时间偏移,也可能是表示失败的 nil。同一个验证码不能重复使用,否则会有重放风险。Rodauth 内部通过 otp_last_use 字段记录上次成功使用的时间,如果某次验证的时间小于等于该记录,则直接拒绝。ROTP 默认允许一定范围的时钟漂移,可以在配置中调整。建议与恢复码一起启用,防止手机或认证器丢失后无法进入账号。

三、U2F 硬件密钥的注册与断言

U2F 与 TOTP 的凭证模型差异很大。TOTP 是软件生成的对称凭证,密钥同时存在于服务端和认证器。U2F 则依赖硬件设备中的非对称密钥对。注册时设备生成公私钥,私钥永远不离开硬件,公钥和 key handle 发送给服务器保存。登录时服务器发送一条随机 challenge,硬件用私钥签名,服务器再用公钥验签。由于签名中还包含 origin 信息,伪造域名无法通过校验,因此 U2F 天然抗钓鱼。

Rodauth 的 webauthn feature 使用 WebAuthn gem 封装标准流程,支持 U2F 与 FIDO2 设备。注册入口 /webauthn-setup 先由服务器生成 challenge,再由浏览器 JavaScript 调用 navigator.credentials.create 引导用户触碰硬件。完成注册后返回的 credential 数据被提交回服务器解析并保存。验证时前端调用 navigator.credentials.get,服务器校验签名和来源。

async function registerWebauthn() {
  const options = await fetch('/webauthn-setup', { method: 'POST' }).then(r => r.json());
  const credential = await navigator.credentials.create({ publicKey: options });
  await fetch('/webauthn-setup', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(credential)
  });
}

服务端会保存 webauthn_id、webauthn_public_key 和 webauthn_sign_count。每次断言除了校验签名,还会检查来源和计数器。计数器递增可以检测克隆设备,来源校验则进一步阻止钓鱼站点。配置时必须把 webauthn_origin 设置成实际域名,例如 https://app.ipipp.com,并且不要用 localhost 做跨站测试,因为 U2F 的 origin 必须与浏览器中的来源完全一致。

四、TOTP 与 U2F 的组合策略

双因素认证的重点不只是开启 feature,还要处理登录状态。账号密码验证通过后,two_factor_base 会创建一个待验证的会话,用户此时不能直接进入应用。只有 TOTP 或 U2F 验证成功后,Rodauth 才会完成会话建立并记住 2FA 状态。登录后的设置页面也需要再次验证当前会话,避免 CSRF 或本地残留会话被滥用。

如果同时启用 TOTP 和 U2F,通常将硬件密钥作为强认证,TOTP 保持可用作为备用和过渡方案。Rodauth 允许用户在设置页注册多个硬件密钥,也可以只启用 TOTP。为了降低设备丢失风险,建议同时启用 recovery_codes,生成一组一次性备份码。备份码不能明文存储,需要哈希后保存,使用时只校验哈希。

enable :two_factor_base, :otp, :webauthn, :recovery_codes
two_factor_confirm_password true
webauthn_origin "https://app.ipipp.com"

敏感操作即使已经通过 2FA 登录,也不应完全依赖旧的登录会话。修改邮箱、密码或执行资金操作时,可以要求用户重新验证 TOTP 或硬件密钥。应用控制器中可以结合 Rodauth 提供的辅助方法检查当前 2FA 状态。测试时应覆盖首次设置、常规登录、设备丢失后恢复三个场景,只有把设置、验证、恢复都跑通,双因素认证才算真正落地。

广告位 · 文章底部(后台-模块管理替换此区域)