本文是继《接入 GitHub 登录(2019)》和《接入 Google 登录(2019)》后的第3️⃣篇关于在网站上接入外部登录/授权系统的文章。GitHub/Google 登录已经足够好用、易用,但是要和今天要介绍的 WebAuthn/Passkeys/通行密钥 比起来,还是要慢、复杂挺多。前两者需要与外部网站交互,而后者只需要和手中的设备本地交互,速度和安全性方面高很多,我本人也很早就开始关注了。今天这篇文章,起稿于2️⃣年前,足足晚(拖)了近3️⃣年之久。大概有以下几个原因:
- WebAuthn 是一项比较新的技术,最早应该是公开出现于 2016 年,网上的文献较少;
- Apple 于 WWDC 2022 才推出 Passkeys 相关技术,可以用作 WebAuthn 的认证端;
- 我是火狐的长期用户,火狐对 WebAuthn 跟进得比较晚,大概是最近一年;
- GitHub 最近几个月接入了 Passkeys 一键登录;
- 我已逐渐不能容忍 Google 越来越丑的登录界面。
所以,我觉得目前条件已经比较成熟了,离生活已经越来越近。 到目前为止,我在 MacOS 上测试过 Firefox、Chrome、Safari,在 iPhone/iPad 上测试过 Firefox、Safari,均已经比较好地支持了标准。所以本博客了也尽快支持了使用 Passkeys/通行密钥 登录相关代码和功能。
两种登录方式的介绍
作为对比,有必要先介绍一下传统密码登录方式。
传统密码登录
传统密码模式登录时,浏览器会把用户输入的密码发送给服务器进行对比验证,这种验证是基于对称密码的验证,服务器和用户在浏览器端输入的是同一个文本密码。 涉及到密码共享/共用/泄露的问题,是很大的安全隐患。密码如果一旦泄露,他人可以完全模仿本人。
所以后来出现了二阶段认证(Two-Factor Authentication)。 用户不仅需要输入密码,还需要输入一个六位数的设备端密码,就像早期银行早期的 K宝/U盾 一样。 这个设备端的密码,在多数情况下,是 OTP(One-Time Password,一次性密码算法),根据用户的帐号随时间变化、采用固定的算法生成的序列。 他人没有用户的设备、也没有这个 OTP 算法(的种子),所以就算拿到密码,也几乎无济于事。 但是,很明显的是,二阶段认证只是作为一种为了缓解传统密码安全性攻击的一种弥补而存在,并不是一种全新的事物(从头设计)。
WebAuthn/Passkeys/通行密钥
先简单区别一下这几个术语的概念与区别:
- WebAuthn 全称 Web Authentication,是一种适用于 Web/网站 的认证方案。包括服务端、浏览器和设备端(授权/认证端),及其三者之间的通信协议。
- Passkeys 是 Apple 推出的认证端方案。Apple 有 Mac/iPhone/iPad,而它们又有 指纹识别/TouchID、面部识别/FaceID,显然绝佳;
- 通行密钥 只不过是 Passkeys 的中文翻译。
在未特别注明、不需要明确需要区分的场合,本文可能混用这几个名词。
WebAuthn 基于非对称加密🔐/公钥加密。作为对比,上述传统密码也被称作对称加密/私钥加密。
提到非对称加密/公钥加密,大家至少对以下内容比较耳熟能详(毕竟这已经是 1970s 的事物了):
- 密码是成对出现的,分为公钥和私钥;
- 公钥可以随意公开,私钥需要安全地保存;
- 由公钥加密的数据,只能由对应的私钥解密;
- 由私钥加密的数据,只能由对应的公钥解密;
- 通过私钥可以求出对应的公钥,反之则不行;
还有就是签名与证书:
- 加密后的数据 = 用私钥加密(数据)
- 证书 ≈ 加密后的数据 + 用私钥加密(公钥) + 公钥
所以总体来说,WebAuthn 并不算是一种特别新事物,至少底层看起来不是。毕竟,我们日常高频使用的 SSH/HTTPS 已经使用这种技术得有 20 来年了。WebAuthn 的“新”在于,它给出了一种浏览器标准:
-
分为服务端、浏览器和认证端
- 服务端拥有用户实体(唯一编号,ID)和公钥列表;
- 认证端保存了用户实体的唯一编号和私钥列表;
- 认证端可以是外部设备,比如 YubiKey,Fido 等
- 也可以是浏览器自行实现的“设备”,它调用系统的平台设备功能,比如 Apple 的 指纹识别/面部识别
- 浏览器桥接服务端和认证端完成用户认证过程。
-
公钥(以及相关附加数据)作为用户凭证而存在。
- 公钥就像手机号一样,能对应到唯一的用户,可以随意创建,随意删除。
- 公钥只是作为代表用户的一种凭证而存在,并不等同于就是用户这个实体。
-
服务端用户实体如何表示。
- 用户,拥有不变的编号(ID,字节数组),拥有可变的帐号名(Name,比如邮箱📪),拥有友好的显示名(DisplayName,比如姓名、昵称)。
-
浏览器如何安全地桥接服务端和认证端完成认证
- 包括 Relying Party(特指网站名称、域名、以及 Origins)、Challenge、签名数据 等的表示。
- 包括发起注册请求、完成注册请求、发起登录请求、完成登录请求。
-
认证端保存了用户的私钥列表
认证设备中的私钥是跟特定网站的用户编号绑定的,用于在浏览器请求认证时创建出能代表用户该网站的授权数据。
WebAuthn
有了上述小节中关于非对称加密以及简单对标准的理解,可以大概得出 WebAuthn 的以下工作过程。
注册 / Registration
特别注意:WebAuthn 的注册并不是指向服务器注册一个新用户,而是指向已经存在的用户添加一个新的凭证🪪。它是一个官方成文的说法,我也只是依照官方惯例,没有换用其它词汇。所以,向网站“注册新用户”并不属于 WebAuthn 自身的内容。
登录 / Login
常规理解方式下我觉得 WebAuthn 的用户登录方式应该是:
- 点击“我要登录”按钮,浏览器通过认证端列出可登录的用户名列表,然后一键登录选择的用户,这跟微信扫码登录有点儿像;
- 但是实际上,网上现存的很多例子(包括官方),都是“明示”你要先输入一个待登录的用户名,然后再点击“我要登录”。我感觉这有点儿奇怪。
不需要先输入待登录的用户名的登录过程叫作“可发现的登录 / Discoverable Login”。 我仅以此为例说明登录过程。
示例
我写的完整可用的例子,见仓库:movsb/golang-webauthn-example。 几乎只需要做少量修改应该就可以接入你自己的系统(如果技术选型类似的话)。
下面的参考链接中有官方提供的在线演示网站:https://webauthn.io/。
实物演示效果
一些参考链接
- Guide to Web Authentication
- WebAuthn.io
- Passkeys: the web authentication standard
- Meet passkeys - WWDC22 - Videos - Apple Developer
- Introduction to WebAuthn API and Passkey
- Apple Just Killed the Password—for Real This Time
- MacOS 13 Ventura: Features, Details, Release Date | WIRED
- Web Authentication API - Web APIs | MDN
- Passkeys in iOS 16 and macOS 13 enable passwordless sign-in - 9to5Mac
- duo-labs/webauthn: WebAuthn (FIDO2) server library written in Go
- duo-labs/webauthn.io: The source code for webauthn.io, a demonstration of WebAuthn.
- koesie10/webauthn: Go package for easy WebAuthn integration
- hbolimovsky/webauthn-example: Basic WebAuthn client and server in go
- WebAuthn Basic Web Client/Server · Herbie's Blog