keygen标签及challenge属性已被所有现代浏览器彻底移除,其设计本身不可信;迁移必须重构认证链路,由服务端生成时效性challenge并纳入签名验证流程。

keygen 标签及其 challenge 属性已从所有现代浏览器中彻底移除——不是“不推荐”,而是根本不存在。Chrome 120+、Firefox 115+、Safari 17+、Edge 120+ 均不渲染该标签,也不触发任何密钥生成行为。你写的 <keygen challenge="abc"> 在 DOM 中只会是一个空节点,challenge 字符串不会被读取、校验或参与任何密码学流程。
为什么 challenge 不能被“替代”而必须重写逻辑
challenge 属性在 keygen 中的作用是:将一段字符串随公钥一起提交,供服务端验证该公钥是否对应本次会话(防重放)。但它从未提供可编程访问、不可篡改或绑定上下文的能力——前端可随意伪造,后端也无法验证其来源真实性。所以它不是“功能缺失”,而是设计上就不可信。
-
challenge不加密、不签名、不绑定用户身份或设备指纹,仅作表单字段提交 - 现代标准要求挑战值(challenge)必须由服务端生成、带时效性(如 JWT exp ≤ 60s)、且需在签名时被纳入哈希输入(例如作为
clientDataJSON的一部分) - WebAuthn 的
challenge是 ArrayBuffer 类型,必须用TextEncoder编码后传入navigator.credentials.create(),与旧keygen的字符串字段无任何兼容路径
实际迁移中必须处理的三个关键点
你无法“保留 challenge 字段 + 换个标签”,必须重构整个认证链路:
- 服务端生成 challenge:用安全随机数(如
crypto.randomBytes(32))生成,并存入 session 或 Redis,设置 60 秒 TTL - 前端传 challenge 给 Web Crypto:不能直接拼接字符串,要构造标准
clientDataJSON,例如:{ "challenge": base64urlEncode(challengeArrayBuffer), "origin": "https://your.app", "typ": "webauthn.create" } - 签名后连同 challenge 一并提交:服务端收到 assertion 后,先解码 challenge,查库确认未过期且未使用过,再调用
verifyAssertionResponse()验证签名
如果坚持用 Web Crypto 手动实现(非 WebAuthn)
这适用于需要自定义密钥用途(如加密而非认证)的场景,但你要自己补全 challenge 的语义:
立即学习“前端免费学习笔记(深入)”;
- 生成密钥对时,不要只导出公钥,而是用
crypto.subtle.sign()对 challenge 做一次签名:const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", privateKey, new TextEncoder().encode(challenge)) - 提交时发三样东西:base64 编码的公钥(SPKI)、signature、原始 challenge 字符串
- 服务端必须用 OpenSSL 或 WebCrypto 兼容库验证 signature 是否能被该公钥正确验签,否则等同于没做 challenge 校验
- 注意:私钥不能导出(
extractable: false),否则 challenge 签名失去意义
真正难的不是生成密钥,而是让 challenge 在端到端流程中具备抗重放、可验证、不可绕过的约束力——这要求前后端都放弃“表单字段思维”,转为基于签名断言(assertion)和状态化 challenge 管理的协议模型。硬套旧逻辑,只会把漏洞从前端搬到后端。



















