为什么 WebAuthn 看起来很容易——直到你尝试交付它

发布: (2026年1月8日 GMT+8 21:14)
5 min read
原文: Dev.to

Source: Dev.to

每个 WebAuthn 演示都能正常工作。真正的生产环境才是问题悄然出现的地方。

WebAuthn 演示具有危险的说服力。你按照教程操作,注册一个通行密钥,成功认证,一切似乎… 已经解决。

没有密码。没有一次性验证码。没有摩擦。

然后你将其上线。瞬间:

  • 用户在不该认证的情况下进行认证
  • 计数器表现异常
  • 不同浏览器之间出现分歧
  • 企业客户提出你的演示从未准备过的问题

这种差距——介于 “它能工作”“它能在生产环境中存活” 之间——正是大多数 WebAuthn 实现失败的地方。

在观察团队多年重复同样错误后,三个差距一次又一次地显现

Gap #1: 演示级别的数据库访问

大多数 WebAuthn 演示都会包含以下这种代码:

async function getUser(username) {
  const query = `SELECT * FROM users WHERE username = '${username}'`;
  return db.query(query);
}

在开发、测试,甚至预发布环境中它可以正常工作——直到真实的用户名中出现引号,或有人尝试 ' OR '1'='1

这不是 WebAuthn 的问题,而是演示忽视的 生产工程 问题。生产系统会对所有查询进行参数化,包括动态的 IN 子句:

function buildInClause(values) {
  if (!Array.isArray(values) || values.length === 0) {
    return { clause: '(NULL)', params: [] };
  }

  const placeholders = values.map(() => '?').join(',');
  return {
    clause: `(${placeholders})`,
    params: Array.from(values)
  };
}

区别不在于代码的优雅——而在于 生存能力

差距 #2:“一次调用”验证幻觉

演示验证通常是这样的:

await fido2.verify(response);

简洁、最小,但完全不足。在生产环境中,这一次调用隐藏了多个攻击面:

  • 重放攻击
  • 计数器回滚(克隆的认证器)
  • 来源伪造
  • 跨会话的挑战重用
  • 原生应用来源的边缘情况

真实的验证逻辑会对每一层进行校验:

if (counter <= prevCounter && counterSupported) {
  throw new Error("counter rollback detected");
}
if (origin !== expectedOrigin) {
  throw new Error("origin mismatch");
}

计数器、来源、编码、挑战绑定、RP ID——这些在生产环境中都不是可选的。如果跳过其中任何一步,认证仍然“能工作”——直到它不再工作。

Gap #3: “单域” WebAuthn 的神话

演示假设:

  • 一个 RP ID
  • 一个域名
  • 一个策略

生产环境并非如此。真实系统需要处理:

  • 多个子域名
  • 通配符 RP ID
  • 企业级认证器白名单(基于 AAGUID)
  • 每个用户的设备数量限制
  • 按租户的超时设置
  • 设备绑定

配置不再是常量,而是 数据

{
  "domain": ".example.com",
  "device_limit": 2,
  "registration_session_timeout": 999
}

这并非为了制造复杂性,而是支持真实组织的最低要求。

“Production‑Ready” 实际含义

领域演示现实生产现实
数据库String queriesParameterized everywhere
验证Single functionMulti‑layer validation
域名localhostWildcards & subdomains
计数器IgnoredRollback detection
策略HardcodedPer‑tenant configuration

这些失败在演示中并不明显。它们全部会在用户信任你的系统后显现。

关于 WebAuthn 的严酷真相

WebAuthn 很容易演示。却很难安全运营。问题不在于标准本身——而在于把一次成功的演示等同于可交付系统的错觉。

如果你计划在原型之外部署 Passkey,请把演示当作教育工具,而不是架构参考。在身份验证领域,失败往往不像错误那样显现;它表现为成功——但却是错误的用户。

我从事需要在真实流量、浏览器以及企业约束下生存的 WebAuthn 系统工作。上面的大多数经验教训,都是在修复那些“运行良好”——直到它们不再正常的情况时得到的。

Back to Blog

相关文章

阅读更多 »