本文永久链接 – https://tonybai.com/2026/08/07/go-crypto-passkey-proposal-explained
大家好,我是Tony Bai。
【导读】
密码正在被淘汰,这不是口号,而是浏览器厂商、操作系统厂商联手推进了近十年的既定方向。2024 年,我曾用写过一篇 Passkey 入门文章(使用Go语言作为示例);两年后的今天,前 Go 密码学负责人 Filippo Valsorda(GitHub 网名 FiloSottile)亲自提交了一份重磅提案——为标准库新增 crypto/passkey 包,试图用“无状态、无回调、无接口”的极简设计,一次性把中小型网站接入 Passkey 的门槛降到最低。这会是 Go 密码学工具箱的下一块重要拼图吗?
【文章要点】
- 官方重磅提案:前 Go 密码学负责人 Filippo Valsorda(FiloSottile)提交 Issue #80663 提案,拟在 Go 标准库中新增
crypto/passkey包,打造“无状态、无回调、无接口”的极简服务端 API。 - 凭据存储标准化:引入
passkey-record规范,将复杂的 WebAuthn 认证数据压缩为类似于密码哈希(PHC)的单行不透明字符串,支持跨语言、跨数据库无缝存储与迁移。 - 反直觉的安全简化:打破行业惯例,证明数据库只需按
user_id建立索引,无需对credential_id设唯一校验,从根本上规避跨账号主键碰撞攻击。 - 流式无状态工作流:清晰拆解
NewRegistration/Register注册流与NewLogin/ParseResponse/Login登录流,显式引入UnauthenticatedUserID标志,杜绝鉴权前的数据信任漏洞。 - 务实的设计取舍:吸取 GO-2024-3321 漏洞教训摒弃回调机制,强制单 Origin 配置以抵御跨站断言重放,并放弃不实用的 attestation 和签名计数器校验,专门为 95% 的消费级 Web 应用降维减负。

先补个课:Passkey 到底是什么
在进入正题之前,有必要先花几分钟,把 Passkey 这件事讲明白。毕竟不是所有读者都天天泡在 WebAuthn 的世界里。
密码到底哪里不好用
密码的问题,其实每个人都亲身体验过:要记住、要够复杂、还不能重复使用。于是现实中大多数人的做法是——同一套密码走天下,或者干脆把密码存进浏览器。这就给了撞库攻击、钓鱼网站可乘之机。
据行业报告,网络钓鱼的举报率近年来持续攀升,凭证泄露和漏洞利用也在同步增长,而这些几乎都和“密码”这一认证方式脱不开关系。
短信验证码、TOTP、邮件魔法链接,本质上都是在给密码“打补丁”,并没有解决根本问题:只要认证的核心还是一串“可以被记住、被输入、被偷走”的字符串,攻击面就一直存在。
Passkey 是什么
Passkey 不是一个新协议,而是 WebAuthn 可发现凭据(discoverable credential)的“消费级马甲名”。它由 FIDO 联盟主导、苹果谷歌微软三大平台联合推进,核心思路很简单:用一对公私钥取代密码。
- 私钥永远只存在用户的设备里(手机、电脑、安全密钥),不会离开设备,更不会被发送给网站;
- 公钥则交给网站保存;
- 登录时,网站发一个随机挑战(challenge),设备用私钥对其签名,网站用公钥验签,验签通过即完成登录。
由于私钥根本不会传输,也就没有“密码被截获”这回事;而每个网站对应一套独立的密钥对,天然杜绝了“一个密码用到底”的重用风险。更关键的是,浏览器会在签名过程中把当前页面的来源(origin)一起签进去,钓鱼网站即便骗到了用户操作,也拿不到能在真实网站上使用的合法签名——这就是所谓“抗钓鱼”。
用户视角:怎么用
对普通用户来说,Passkey 的体验其实比密码简单得多:
- 注册:在网站上点“用 Passkey 注册”,手机或电脑弹出指纹、面容或 PIN 验证,确认后即完成创建,全程不需要输入任何东西;
- 登录:点“登录”,同样一次生物识别确认,登录完成;
- 多设备同步:主流厂商(苹果的 iCloud 钥匙串、谷歌密码管理器、微软 Windows Hello 等)都支持把 Passkey 加密同步到用户的其他设备,换手机也不会丢登录方式;
- 跨设备使用:如果要在一台没有对应 Passkey 的设备上登录,可以用手机扫码,通过混合传输(hybrid transport)临时借用手机上的 Passkey 完成认证。
下面这张时序图,展示了 Passkey 登录时,浏览器、用户设备(认证器)与服务器三方之间最核心的交互关系:

如果想系统学习 Passkey 与 WebAuthn 的细节,推荐三个权威资料站:passkey.org(面向大众科普)、passkeys.dev(面向开发者的实践指南)、webauthn.guide(对 WebAuthn 协议本身的图解教程)。
回顾 2024 年:那时候用 Go 做 Passkey 有多“折腾”
两年前写那篇入门文章的时候,Go 生态里并没有官方的 Passkey/WebAuthn 支持,开发者基本都要依赖第三方库(比如社区维护的 go-webauthn 等)。这些库普遍存在几个共性问题,也是这次提案开篇就点名批评的:
- 接口偏底层:库本身只负责协议层的编解码与校验,剩下的会话管理、存储抽象都要开发者自己搭;
- 各家都有自己的存储接口:几乎每个库都会定义一套
WebAuthnUser、SessionData之类的接口,接入一个新库往往意味着重新适配一遍存储层; - 状态管理容易出错:注册和登录过程中需要缓存的“挑战”(challenge)该存在哪里、什么时候失效、要不要一次性消费,很多细节都留给开发者自己判断,一不小心就埋下重放攻击的隐患。
换句话说,两年前做一个 Passkey 登录 Demo 并不难,但要把它做“对”、做“安全”,需要开发者对 WebAuthn 协议本身有相当程度的理解。这也正是这次官方提案想要解决的核心痛点。
提案主角:crypto/passkey 长什么样
这份提案来自 golang/go#80663,提出者是 Filippo Valsorda(FiloSottile)。他此前长期负责 Go 的密码学与安全工作,是 age 加密工具的作者,也是 Go 官方漏洞数据库(Go 1.21 之后新增的 govulncheck 体系)的重要推动者之一,在 Go 密码学社区里属于“一锤定音”级别的人物。
提案开篇给出的定位很直接:这是一个无状态、无回调、无接口的 Passkey 服务端 API,目标用户是中小型网站,而不是亚马逊、谷歌那种超大规模平台。
三个“不”字诀
- 无状态(no state):
RelyingParty是一个可以并发安全复用的“纯配置”对象,本身不持有任何注册或登录会话的状态; - 无回调(no callbacks):不会像很多库那样要求开发者传入
func(ctx, userID) (records, error)这样的回调函数; - 无接口(no interfaces):不强制开发者实现任何存储接口,应用怎么存数据完全自主决定。
这个设计选择在提案的“决策”部分讲得很坦诚:库内部不做任何 I/O,也就不需要在 API 里到处塞 context.Context,用起来自然轻。
核心概念:passkey record
这次提案里最值得记住的一个新概念,是passkey record(密码记录)。它不是一个 Go 结构体,而是一个类似“加盐密码哈希”的不透明字符串,把服务端需要保存的所有凭据信息编码进一个前缀化的文本格式里,形如:
$webauthn$v=1$transports=hybrid+internal$<base64 认证器数据>
应用侧只需要把这个字符串当成一个整体存进数据库,不需要关心里面到底装了什么。这个格式已经作为独立规范提交到了 c2sp.org(Community Cryptography Specification Project),意味着未来其他语言的 WebAuthn 库理论上也能读写同一种记录格式,实现跨语言、跨实现的互操作。
存储模型也因此变得极其简单:一张表,只按用户 ID 建索引,完全不需要按凭据 ID(credential ID)建唯一索引:
CREATE TABLE passkeys (
user_id TEXT NOT NULL,
record TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(passkeys_user_id)
);
CREATE INDEX passkeys_user_id ON passkeys(user_id);
这背后有一个反直觉但很扎实的安全论证:既然凭据永远是“先按用户 ID 查出该用户全部记录,再逐条比对”,就不存在“跨账号撞上同一个凭据 ID”的风险,WebAuthn 规范里建议做的“跨账号唯一性检查”自然也就不再需要。
注册流程

调用方式大致是这样(为便于理解,示例做了精简):
optionsJSON, err := rp.NewRegistration(
passkey.User{ID: opaqueUserID, Name: username}, existingPasskeys)
// ... 把 optionsJSON 发给前端,前端调用 navigator.credentials.create()
record, err := rp.Register(responseJSON)
// ... 把 record 这个字符串存进数据库
登录流程

这里有个很值得玩味的设计:登录响应里带出来的 UnauthenticatedUserID,在验证通过之前是不可信的——它只能被用来“查一下这个用户都有哪些 Passkey 记录”,绝不能直接当作已登录用户使用。方法名里特意保留了 Unauthenticated 前缀,就是为了在类型层面提醒开发者:这是一份“暂时不能信任”的数据。
几个最见功力的设计决策
一份好的 API 设计,往往体现在它拒绝了什么。这份提案的“Decisions”部分坦诚地记录了不少权衡,挑几个最有意思的说一说。
为什么放弃回调,改用 ParseResponse + Response 类型
一个看似更“安全”的设计是提供一个回调,比如 PasskeyRecordsForUserID func(ctx, userID) ([]string, error),让库自己去查用户的记录。但提案作者直接举了一个真实教训:Go 官方漏洞数据库里记录的 GO-2024-3321 就是一次“把未经验证的数据传给回调,导致应用做出了不该做的判断”的真实案例。于是最终方案选择了先 ParseResponse 解析出一个 Response 对象,再由应用自己决定如何用其中的 UnauthenticatedUserID 去查数据库——把决策权和风险提示都留在了应用这一侧,而不是藏在库的回调机制里。
User ID 为什么必须是随机不透明字符串
WebAuthn 的安全模型有一个不太直觉的细节:安全密钥类硬件即使在没有做用户验证的情况下,也可能泄露某个可发现凭据的存在、其凭据 ID 和用户 ID,但绝不会泄露用户名或显示名。这也是为什么规范要求 User ID 不能包含邮箱、用户名之类的可识别信息。
提案的做法是:不专门为生成 User ID 提供 API,直接建议用标准库自带的 crypto/rand.Text() 生成随机字符串,在账号创建时一次性生成,此后终身不变、不可与内部账号体系里的邮箱、用户名等标识混用。
Origin 与 RP ID 之间最容易踩的坑
这是提案里篇幅最长、作者自己都说“最可能引发争议”的一部分。
WebAuthn 的登录断言本质上是对“当前页面来源(origin)”的一次签名,而 RP ID(通常是网站的可注册域名,例如 example.com)决定了哪些来源可以使用某个凭据。
很多库为了图方便,会让开发者配置一个“允许的 origin 列表”,但这个做法本身就埋了雷:如果同一个 RP ID 下,不同来源的可信程度并不一样(比如登录门户、移动端 App、营销博客分属不同信任级别),一旦某个低信任来源被 XSS,攻击者就可能截获一个登录断言,拿去更高信任级别的来源上冒充登录。
提案给出的例子很典型:假设某云主机服务商在 example.com 提供控制台,又把客户的虚拟机挂在 NAME.provider.example 这样的子域名下,如果反向代理配置成“只要匹配 RP ID 就放行”,那么一个恶意租户完全可以在自己的虚拟机域名下截获别人的登录断言,再拿到主控制台上冒充登录。
最终方案是:RelyingParty 只接受单一、明确配置的 Origin,需要支持多来源时,开发者应该为每个来源分别创建一个 RelyingParty 实例(构造成本很低),而不是把“信任判断”交给运行时的请求头。作者也直言,这个设计目前还有开发者反馈“不太清楚多来源场景该怎么用”,欢迎社区继续给建议。
不支持 attestation,也不检查签名计数器
Attestation(认证器证明)在企业场景里有用,但在消费级 Passkey 生态里几乎不存在实际使用价值,所以提案直接选择不支持。同样,签名计数器(signature counter,本来是用来检测凭据被克隆的机制)也没有被校验,理由很务实:主流 Passkey 提供商基本都不维护这个计数器,强行校验反而会拒绝合法登录。这些取舍都体现了同一个原则——为真实世界里 95% 的场景做减法,而不是追求协议层面的“大而全”。
这份提案现在到什么阶段了
需要提醒的是,这仍然是一份 Proposal(提案),走的是 Go 官方标准的提案评审流程:先在 Issue 里公开讨论、收集反馈,经 Go 团队评估决定是否采纳,采纳后才会进入某个版本的开发计划,最终随正式 Release 发布。
目前 Issue 里已经放出了一份可交互的 API 文档(https://godoc-play.exe.xyz/oehterowvj5n),感兴趣的开发者可以直接去阅读完整API接口与文档,或者在 Issue 下留言参与讨论,比如作者反复提到的“多来源支持该怎么设计得更清楚”,就非常欢迎实战经验的反馈。
对于中小型网站的开发者来说,即便提案最终定稿前还会有改动,这次官方释放的信号已经足够明确:Passkey 接入的复杂度,正在从“开发者必须理解协议细节”降级为“开发者只需要按套路存一个字符串”。
如果你的团队正打算给产品加上免密登录,现在是一个很好的时机去关注这个包的进展,甚至提前按照它的存储模型(按用户 ID 索引、不透明记录字符串)来设计数据库表结构。
参考资料:
- Go 官方提案:proposal: crypto/passkey: new package —— https://github.com/golang/go/issues/80663
- Passkey Record 规范草案:https://c2sp.org/passkey-record
- Go 漏洞数据库 GO-2024-3321:https://pkg.go.dev/vuln/GO-2024-3321
- Passkey 大众科普:https://passkey.org
- Passkey 开发者指南:https://passkeys.dev
- WebAuthn 图解教程:https://webauthn.guide
还在为写 Agent 框架频频死循环、上下文爆炸而束手无策?我的新专栏 《从0 开始构建 Agent Harness》 将带你:
- 抛弃臃肿框架,回归“驾驭工程 (Harness Engineering)”的第一性原理
- 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等,复刻极简OpenClaw
- 构建坚不可摧的 Safety Middleware 与飞书人工审批防线
- 在底层实现 Token 成本审计、链路追踪与自动化跑分评估
- 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师”
扫描下方二维码,开启从 0 开始构建Agent Harness 的实战之旅。

你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?
- 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
- 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
- 想打造生产级的Go服务,却在工程化实践中屡屡受挫?
继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!
我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。
目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。
