构建无密码认证:passkey入门与Go实现

Passkey无密码认证技术入门介绍

e73f0c21f3607f7e3957e9813c9cb362.jpeg请点击上方蓝字TonyBai订阅公众号!

5f10515614da4b46405c17a23a34acf2.png

传统的密码认证一直以来都是数字时代的主流身份验证方式。然而,用户常常选择易记的弱密码并重复使用,导致账号易受攻击。密码泄露、钓鱼攻击等安全问题层出不穷,超过80%的数据泄露与密码相关。

bb9ad3e58779ad1236cfbcca5ea45cda.png

截图来自FIDO联盟官网

与此同时,频繁的密码管理和忘记密码情况严重影响用户体验。服务商在安全保存用户密码方面的责任也增加了系统建设和维护的成本。为了应对这些问题,科技行业开始积极探索无密码认证的方法。

无密码认证利用设备生物识别、硬件加密和其他更安全的验证手段,提供了更安全的登录体验。在Thoughtworks最新一期(第31期)技术雷达文档[3]中,一种名为passkey[4]的无密码认证技术被列入“试验” 象限,许多读者可能在github[5]或其他支持passkey的站点和应用中使用过这一技术了。

9f3a1b85ea3cdb4a417a8925eb7667d8.png

Passkey是FIDO联盟(Fast IDentity Online)[6]提出的一种无密码认证解决方案[7]。FIDO联盟是一个开放的行业协会,其核心使命是减少世界对密码的依赖。联盟成员包括众多知名的科技公司和组织,如Google、微软、Apple、Amazon等,致力于定义一套开放、可扩展、可互操作的机制,以降低用户和设备身份验证时对密码的依赖。

Passkey是FIDO联盟的首个无密码身份认证凭据方案,支持用户通过与解锁手机、平板或计算机相同的方式(如生物识别(比如屏幕指纹、面部识别等)、PIN码或图案)登录应用程序和网站。目前许多主流设备、操作系统原生应用、浏览器和站点都支持passkey技术(如下图),这使得passkey技术在未来的无密码认证认证领域展现出巨大的潜力。

f5bf4c7ce81809d4e90a6b62ee26f142.png

图来自passkeys.dev(截至20241026)

在这篇文章中,我将对passkey技术进行入门介绍,并通过Go实现一个简单的示例供大家参考。

1. passkey的工作原理

通过上面的介绍,我们大致知道了passkey是密码的替代品,一旦使用了passkey,我们登录网站时就无需再输入密码,用于网站对你的身份进行验证的passkey存储在你的设备本地,你顶多只需通过本地设备的生物识别(比如指纹、人脸或图案密码等)进行一次解锁即可。

从技术本质来说,paaskey就是“免密登录服务器”方案在Web服务和终端App领域的应用。没错!passkey就是基于非对称加密实现的一种无密码认证技术。下图展示了Bob这个用户登录不同Web服务时使用不同passkey的情景:

91c81f18ff61f0904d8b56826cadd399.png

如果你熟悉非对称加密的运作原理,你就可以立即get到passkey的工作原理。

注:在《Go语言精进之路:从新手到高手的编程思想、方法和技巧[8]》的第51条“使用net/http包实现安全通信”中有对非对称加密的全面系统讲解以及示例说明。如果你不是很熟悉,可以看一下我的这本书中的内容。

以上图中的Web Service1为例,用户Bob在注册时会在其自己的设备(比如电脑)上创建一对私钥与公钥,比如Bob的bob-ws1-private key和bob-ws1-public key,私钥会保存在Bob的设备上,而并不需要保密的公钥则会发送给Web Service1保存。之后,Web Service1对Bob进行身份验证的时候,只需发送一块数据给Bob设备上的应用(通常是浏览器),应用会申请使用Bob的私钥,这个过程可能需要bob输入设备的用户密码或使用生物识别(比如指纹)来授权。使用Bob的私钥对这块数据进行签名后,发回Web Service1,后者通过Bob保存在服务器上的公钥对这块签名后的数据进行验签,验签通过,则Bob的身份验证就通过了!当然这只是基本原理,还有很多场景、交互和技术细节,比如支持在网吧等公共计算机上借助个人的其他设备(比如手机)进行基于passkey的的身份验证等,这些需要进一步阅读相关规范。更多原理细节我们也会在接下来的内容中详细说明。

不过,在进一步了解原理之前,我们先来了解一下paaskey与FIDO、webauthn之间的关系。

FIDO2[9]是一个开放的认证标准框架,旨在取代传统密码认证。它包含WebAuthn[10](由W3C提供的WebAPI规范)和CTAP[11](客户端到认证器的协议),即客户端设备和外部认证器的通信标准。FIDO2的主要目标是增强网络安全性,支持无需密码的安全登录方式。

WebAuthn是FIDO2的WebAPI组件,定义了应用如何在网页上与浏览器协作,以支持基于公钥的认证方式。它允许浏览器和Web应用访问用户设备上的身份验证器(如指纹传感器或USB密钥),并进行认证交互。WebAuthn作为Web标准,得到了大多数现代浏览器的支持。

Passkey是对FIDO2标准的应用,以实现无密码认证。在技术栈上,Passkey利用WebAuthn和CTAP来构建实际应用体验,从而让用户在支持FIDO2的Web应用中享受无密码登录的便捷。这三者共同实现了现代无密码身份认证的完整生态体系。

下面我们通过一个序列图具体了解一下paaskey的工作原理:

6a20d8a315b782379d3eb486d6914462.png

上图展示了Passkey的工作流程,包括注册和认证两个主要流程。

在passkey(即基于WebAuthn的非密码认证机制)中,有三个主要的实体:

  • 浏览器(客户端):提供 WebAuthn API

  • 服务器(即规范中的依赖方(Relying Party)):验证用户身份

  • 认证器(Authenticator): 生成和存储密钥对(认证器可以是设备内置的,如TouchID、FaceID,或外部硬件如YubiKey)。

我们先来看看注册流程

用户输入用户名并触发注册流程,浏览器向服务器请求注册选项,服务器生成随机挑战(challenge)并创建注册选项。

浏览器调用WebAuthn API(navigator.credentials.create),操作系统检查可用的认证器,并根据认证器类型调用相应的系统API。 认证器请求用户验证(如需要),系统根据请求的用户验证级别来决定验证方式。验证级别包括无需验证(none)、隐式验证(silent,比如设备已解锁,使用之前的验证结果)以及必须验证(Required)。如果是必须验证,系统会显示验证提示(密码/生物识别/PIN等)。

用户提供身份验证信息后,认证器会生成新的公私钥对,并将私钥安全存储在认证器中,公钥和其他凭证数据(私钥签名后的挑战数据)返回给浏览器。浏览器将公钥和其他凭证发送给服务器,服务器验证凭证(通过公钥验签)并存储公钥,注册完成。

接下来,我们再来看认证流程

当用户输入用户名并触发登录后,浏览器会向服务器请求认证选项,服务器生成新的挑战并返回认证选项。

浏览器调用WebAuthn API (navigator.credentials.get),认证器使用私钥对挑战进行签名,并返回签名和其他断言数据给浏览器。

浏览器将断言发送给服务器,服务器使用存储的公钥验证签名,认证完成。

我们看到在整个注册和身份验证流程中,用户都无需记忆复杂的密码,机密信息(比如传统的密码)也无需传递给服务器保存,而公钥本身就是随意公开分发的,服务端甚至都无需对其进行任何加密处理。由此可以看到:passkey既提供了更好的安全性,又提供了更好的用户体验,是传统密码认证的理想替代方案之一。

注:使用另一个设备进行身份验证的流程,大家可以自行阅读passkey相关规范了解。

了解了原理之后,我们再来看一个简单的示例,直观地看看如何实现基于passkey的身份认证。

2. passkey身份认证示例

我们使用Go实现一个最简单的基于passkey进行注册和身份验证的示例。在这个示例里,我们将使用webauthn官方推荐的Go包[12]:go-webauthn/webauthn来实现服务端对passkey登录的支持。

注:本示例的工作环境为Go 1.23.0、macOS和Edge浏览器。

这个示例的文件布局如下:

// intro-to-passkey/demo
$tree -F .
.
├── go.mod
├── go.sum
├── main.go
└── static/
    └── index.html

首先我们通过一个静态文件服务器提供了前端首页,并注册了4个API端点用于处理Passkey注册和认证:

// intro-to-passkey/demo/main.go
func main() {
    // 静态文件服务
    http.Handle("/", http.FileServer(http.Dir("static")))

    // API 路由
    http.HandleFunc("/api/register/begin", handleBeginRegistration)
    http.HandleFunc("/api/register/finish", handleFinishRegistration)
    http.HandleFunc("/api/login/begin", handleBeginLogin)
    http.HandleFunc("/api/login/finish", handleFinishLogin)

    log.Println("Server running on http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

关键的passkey配置在init函数中:

func init() {            
    var err error        
    webAuthn, err = webauthn.New(&webauthn.Config{
        RPDisplayName: "Passkey Demo",                    // Relying Party Display Name
        RPID:          "localhost",                       // Relying Party ID
        RPOrigins:     []string{"http://localhost:8080"}, //允许的源
    })                   
    if err != nil {      
        log.Fatal(err)   
    }                    
    userDB = NewUserDB() // 初始化内存用户数据库
}

运行该go程序后,打开localhost:8080,我们将看到下面页面:

2c30d742e522684a4fca4c3485c3e77a.png

接下来,我们先来注册一个用户的passkey。在注册输入框中输入"tonybai",点击“注册”,浏览器会弹出下面对话框,提醒用户将为localhost创建密钥:

0ad1888d78f5f57c38c2e76856ab9c8c.png

点击“继续”,本地os会弹出身份验证对话框:

2f693f6f17e070a215895d106db7f1a6.png

输入你的os登录密码,便可继续注册过程。如果注册ok,页面会显示下面“注册成功”字样:

6611ab22ec058ce17c99ecb30f39c9fb.png

在服务器后端,上述的注册过程是由两个handler共同完成的,这也是webauthn规范确定的流程,大家可以结合上面的序列图一起看。

首先是处理/api/register/begin的handleBeginRegistration,它的大致逻辑如下:

func handleBeginRegistration(w http.ResponseWriter, r *http.Request) {
    // 1. 验证用户名是否已存在
    if _, exists&nb
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值