構(gòu)建無(wú)密碼認(rèn)證:passkey入門與Go實(shí)現(xiàn)
傳統(tǒng)的密碼認(rèn)證一直以來(lái)都是數(shù)字時(shí)代的主流身份驗(yàn)證方式。然而,用戶常常選擇易記的弱密碼并重復(fù)使用,導(dǎo)致賬號(hào)易受攻擊。密碼泄露、釣魚攻擊等安全問(wèn)題層出不窮,超過(guò)80%的數(shù)據(jù)泄露與密碼相關(guān)。
截圖來(lái)自FIDO聯(lián)盟官網(wǎng)
與此同時(shí),頻繁的密碼管理和忘記密碼情況嚴(yán)重影響用戶體驗(yàn)。服務(wù)商在安全保存用戶密碼方面的責(zé)任也增加了系統(tǒng)建設(shè)和維護(hù)的成本。為了應(yīng)對(duì)這些問(wèn)題,科技行業(yè)開(kāi)始積極探索無(wú)密碼認(rèn)證的方法。
無(wú)密碼認(rèn)證利用設(shè)備生物識(shí)別、硬件加密和其他更安全的驗(yàn)證手段,提供了更安全的登錄體驗(yàn)。在Thoughtworks最新一期(第31期)技術(shù)雷達(dá)文檔[3]中,一種名為passkey[4]的無(wú)密碼認(rèn)證技術(shù)被列入“試驗(yàn)” 象限,許多讀者可能在github[5]或其他支持passkey的站點(diǎn)和應(yīng)用中使用過(guò)這一技術(shù)了。
圖片
Passkey是FIDO聯(lián)盟(Fast IDentity Online)[6]提出的一種無(wú)密碼認(rèn)證解決方案[7]。FIDO聯(lián)盟是一個(gè)開(kāi)放的行業(yè)協(xié)會(huì),其核心使命是減少世界對(duì)密碼的依賴。聯(lián)盟成員包括眾多知名的科技公司和組織,如Google、微軟、Apple、Amazon等,致力于定義一套開(kāi)放、可擴(kuò)展、可互操作的機(jī)制,以降低用戶和設(shè)備身份驗(yàn)證時(shí)對(duì)密碼的依賴。
Passkey是FIDO聯(lián)盟的首個(gè)無(wú)密碼身份認(rèn)證憑據(jù)方案,支持用戶通過(guò)與解鎖手機(jī)、平板或計(jì)算機(jī)相同的方式(如生物識(shí)別(比如屏幕指紋、面部識(shí)別等)、PIN碼或圖案)登錄應(yīng)用程序和網(wǎng)站。目前許多主流設(shè)備、操作系統(tǒng)原生應(yīng)用、瀏覽器和站點(diǎn)都支持passkey技術(shù)(如下圖),這使得passkey技術(shù)在未來(lái)的無(wú)密碼認(rèn)證認(rèn)證領(lǐng)域展現(xiàn)出巨大的潛力。
圖片
圖來(lái)自passkeys.dev(截至20241026)
在這篇文章中,我將對(duì)passkey技術(shù)進(jìn)行入門介紹,并通過(guò)Go實(shí)現(xiàn)一個(gè)簡(jiǎn)單的示例供大家參考。
1. passkey的工作原理
通過(guò)上面的介紹,我們大致知道了passkey是密碼的替代品,一旦使用了passkey,我們登錄網(wǎng)站時(shí)就無(wú)需再輸入密碼,用于網(wǎng)站對(duì)你的身份進(jìn)行驗(yàn)證的passkey存儲(chǔ)在你的設(shè)備本地,你頂多只需通過(guò)本地設(shè)備的生物識(shí)別(比如指紋、人臉或圖案密碼等)進(jìn)行一次解鎖即可。
從技術(shù)本質(zhì)來(lái)說(shuō),paaskey就是“免密登錄服務(wù)器”方案在Web服務(wù)和終端App領(lǐng)域的應(yīng)用。沒(méi)錯(cuò)!passkey就是基于非對(duì)稱加密實(shí)現(xiàn)的一種無(wú)密碼認(rèn)證技術(shù)。下圖展示了Bob這個(gè)用戶登錄不同Web服務(wù)時(shí)使用不同passkey的情景:
圖片
如果你熟悉非對(duì)稱加密的運(yùn)作原理,你就可以立即get到passkey的工作原理。
注:在《Go語(yǔ)言精進(jìn)之路:從新手到高手的編程思想、方法和技巧[8]》的第51條“使用net/http包實(shí)現(xiàn)安全通信”中有對(duì)非對(duì)稱加密的全面系統(tǒng)講解以及示例說(shuō)明。如果你不是很熟悉,可以看一下我的這本書中的內(nèi)容。
以上圖中的Web Service1為例,用戶Bob在注冊(cè)時(shí)會(huì)在其自己的設(shè)備(比如電腦)上創(chuàng)建一對(duì)私鑰與公鑰,比如Bob的bob-ws1-private key和bob-ws1-public key,私鑰會(huì)保存在Bob的設(shè)備上,而并不需要保密的公鑰則會(huì)發(fā)送給Web Service1保存。之后,Web Service1對(duì)Bob進(jìn)行身份驗(yàn)證的時(shí)候,只需發(fā)送一塊數(shù)據(jù)給Bob設(shè)備上的應(yīng)用(通常是瀏覽器),應(yīng)用會(huì)申請(qǐng)使用Bob的私鑰,這個(gè)過(guò)程可能需要bob輸入設(shè)備的用戶密碼或使用生物識(shí)別(比如指紋)來(lái)授權(quán)。使用Bob的私鑰對(duì)這塊數(shù)據(jù)進(jìn)行簽名后,發(fā)回Web Service1,后者通過(guò)Bob保存在服務(wù)器上的公鑰對(duì)這塊簽名后的數(shù)據(jù)進(jìn)行驗(yàn)簽,驗(yàn)簽通過(guò),則Bob的身份驗(yàn)證就通過(guò)了!當(dāng)然這只是基本原理,還有很多場(chǎng)景、交互和技術(shù)細(xì)節(jié),比如支持在網(wǎng)吧等公共計(jì)算機(jī)上借助個(gè)人的其他設(shè)備(比如手機(jī))進(jìn)行基于passkey的的身份驗(yàn)證等,這些需要進(jìn)一步閱讀相關(guān)規(guī)范。更多原理細(xì)節(jié)我們也會(huì)在接下來(lái)的內(nèi)容中詳細(xì)說(shuō)明。
不過(guò),在進(jìn)一步了解原理之前,我們先來(lái)了解一下paaskey與FIDO、webauthn之間的關(guān)系。
FIDO2[9]是一個(gè)開(kāi)放的認(rèn)證標(biāo)準(zhǔn)框架,旨在取代傳統(tǒng)密碼認(rèn)證。它包含WebAuthn[10](由W3C提供的WebAPI規(guī)范)和CTAP[11](客戶端到認(rèn)證器的協(xié)議),即客戶端設(shè)備和外部認(rèn)證器的通信標(biāo)準(zhǔn)。FIDO2的主要目標(biāo)是增強(qiáng)網(wǎng)絡(luò)安全性,支持無(wú)需密碼的安全登錄方式。
WebAuthn是FIDO2的WebAPI組件,定義了應(yīng)用如何在網(wǎng)頁(yè)上與瀏覽器協(xié)作,以支持基于公鑰的認(rèn)證方式。它允許瀏覽器和Web應(yīng)用訪問(wèn)用戶設(shè)備上的身份驗(yàn)證器(如指紋傳感器或USB密鑰),并進(jìn)行認(rèn)證交互。WebAuthn作為Web標(biāo)準(zhǔn),得到了大多數(shù)現(xiàn)代瀏覽器的支持。
Passkey是對(duì)FIDO2標(biāo)準(zhǔn)的應(yīng)用,以實(shí)現(xiàn)無(wú)密碼認(rèn)證。在技術(shù)棧上,Passkey利用WebAuthn和CTAP來(lái)構(gòu)建實(shí)際應(yīng)用體驗(yàn),從而讓用戶在支持FIDO2的Web應(yīng)用中享受無(wú)密碼登錄的便捷。這三者共同實(shí)現(xiàn)了現(xiàn)代無(wú)密碼身份認(rèn)證的完整生態(tài)體系。
下面我們通過(guò)一個(gè)序列圖具體了解一下paaskey的工作原理:
圖片
上圖展示了Passkey的工作流程,包括注冊(cè)和認(rèn)證兩個(gè)主要流程。
在passkey(即基于WebAuthn的非密碼認(rèn)證機(jī)制)中,有三個(gè)主要的實(shí)體:
- 瀏覽器(客戶端):提供 WebAuthn API
- 服務(wù)器(即規(guī)范中的依賴方(Relying Party)):驗(yàn)證用戶身份
- 認(rèn)證器(Authenticator): 生成和存儲(chǔ)密鑰對(duì)(認(rèn)證器可以是設(shè)備內(nèi)置的,如TouchID、FaceID,或外部硬件如YubiKey)。
我們先來(lái)看看注冊(cè)流程。
用戶輸入用戶名并觸發(fā)注冊(cè)流程,瀏覽器向服務(wù)器請(qǐng)求注冊(cè)選項(xiàng),服務(wù)器生成隨機(jī)挑戰(zhàn)(challenge)并創(chuàng)建注冊(cè)選項(xiàng)。
瀏覽器調(diào)用WebAuthn API(navigator.credentials.create),操作系統(tǒng)檢查可用的認(rèn)證器,并根據(jù)認(rèn)證器類型調(diào)用相應(yīng)的系統(tǒng)API。 認(rèn)證器請(qǐng)求用戶驗(yàn)證(如需要),系統(tǒng)根據(jù)請(qǐng)求的用戶驗(yàn)證級(jí)別來(lái)決定驗(yàn)證方式。驗(yàn)證級(jí)別包括無(wú)需驗(yàn)證(none)、隱式驗(yàn)證(silent,比如設(shè)備已解鎖,使用之前的驗(yàn)證結(jié)果)以及必須驗(yàn)證(Required)。如果是必須驗(yàn)證,系統(tǒng)會(huì)顯示驗(yàn)證提示(密碼/生物識(shí)別/PIN等)。
用戶提供身份驗(yàn)證信息后,認(rèn)證器會(huì)生成新的公私鑰對(duì),并將私鑰安全存儲(chǔ)在認(rèn)證器中,公鑰和其他憑證數(shù)據(jù)(私鑰簽名后的挑戰(zhàn)數(shù)據(jù))返回給瀏覽器。瀏覽器將公鑰和其他憑證發(fā)送給服務(wù)器,服務(wù)器驗(yàn)證憑證(通過(guò)公鑰驗(yàn)簽)并存儲(chǔ)公鑰,注冊(cè)完成。
接下來(lái),我們?cè)賮?lái)看認(rèn)證流程。
當(dāng)用戶輸入用戶名并觸發(fā)登錄后,瀏覽器會(huì)向服務(wù)器請(qǐng)求認(rèn)證選項(xiàng),服務(wù)器生成新的挑戰(zhàn)并返回認(rèn)證選項(xiàng)。
瀏覽器調(diào)用WebAuthn API (navigator.credentials.get),認(rèn)證器使用私鑰對(duì)挑戰(zhàn)進(jìn)行簽名,并返回簽名和其他斷言數(shù)據(jù)給瀏覽器。
瀏覽器將斷言發(fā)送給服務(wù)器,服務(wù)器使用存儲(chǔ)的公鑰驗(yàn)證簽名,認(rèn)證完成。
我們看到在整個(gè)注冊(cè)和身份驗(yàn)證流程中,用戶都無(wú)需記憶復(fù)雜的密碼,機(jī)密信息(比如傳統(tǒng)的密碼)也無(wú)需傳遞給服務(wù)器保存,而公鑰本身就是隨意公開(kāi)分發(fā)的,服務(wù)端甚至都無(wú)需對(duì)其進(jìn)行任何加密處理。由此可以看到:passkey既提供了更好的安全性,又提供了更好的用戶體驗(yàn),是傳統(tǒng)密碼認(rèn)證的理想替代方案之一。
注:使用另一個(gè)設(shè)備進(jìn)行身份驗(yàn)證的流程,大家可以自行閱讀passkey相關(guān)規(guī)范了解。
了解了原理之后,我們?cè)賮?lái)看一個(gè)簡(jiǎn)單的示例,直觀地看看如何實(shí)現(xiàn)基于passkey的身份認(rèn)證。
2. passkey身份認(rèn)證示例
我們使用Go實(shí)現(xiàn)一個(gè)最簡(jiǎn)單的基于passkey進(jìn)行注冊(cè)和身份驗(yàn)證的示例。在這個(gè)示例里,我們將使用webauthn官方推薦的Go包[12]:go-webauthn/webauthn來(lái)實(shí)現(xiàn)服務(wù)端對(duì)passkey登錄的支持。
注:本示例的工作環(huán)境為Go 1.23.0、macOS和Edge瀏覽器。
這個(gè)示例的文件布局如下:
// intro-to-passkey/demo
$tree -F .
.
├── go.mod
├── go.sum
├── main.go
└── static/
└── index.html
首先我們通過(guò)一個(gè)靜態(tài)文件服務(wù)器提供了前端首頁(yè),并注冊(cè)了4個(gè)API端點(diǎn)用于處理Passkey注冊(cè)和認(rèn)證:
// intro-to-passkey/demo/main.go
func main() {
// 靜態(tài)文件服務(wù)
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))
}
關(guān)鍵的passkey配置在init函數(shù)中:
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() // 初始化內(nèi)存用戶數(shù)據(jù)庫(kù)
}
運(yùn)行該go程序后,打開(kāi)localhost:8080,我們將看到下面頁(yè)面:
圖片
接下來(lái),我們先來(lái)注冊(cè)一個(gè)用戶的passkey。在注冊(cè)輸入框中輸入"tonybai",點(diǎn)擊“注冊(cè)”,瀏覽器會(huì)彈出下面對(duì)話框,提醒用戶將為localhost創(chuàng)建密鑰:
圖片
點(diǎn)擊“繼續(xù)”,本地os會(huì)彈出身份驗(yàn)證對(duì)話框:
圖片
輸入你的os登錄密碼,便可繼續(xù)注冊(cè)過(guò)程。如果注冊(cè)ok,頁(yè)面會(huì)顯示下面“注冊(cè)成功”字樣:
圖片
在服務(wù)器后端,上述的注冊(cè)過(guò)程是由兩個(gè)handler共同完成的,這也是webauthn規(guī)范確定的流程,大家可以結(jié)合上面的序列圖一起看。
首先是處理/api/register/begin的handleBeginRegistration,它的大致邏輯如下:
func handleBeginRegistration(w http.ResponseWriter, r *http.Request) {
// 1. 驗(yàn)證用戶名是否已存在
if _, exists := userDB.users[data.Username]; exists {
http.Error(w, "User already exists", http.StatusBadRequest)
return
}
// 2. 創(chuàng)建新用戶
user := &User{
ID: []byte(data.Username),
Name: data.Username,
DisplayName: data.Username,
}
userDB.users[data.Username] = user
// 3. 生成注冊(cè)選項(xiàng)和會(huì)話數(shù)據(jù)
options, sessionData, err := webAuthn.BeginRegistration(user)
// 4. 存儲(chǔ)會(huì)話數(shù)據(jù)
sessionID := storeSession(sessionData)
http.SetCookie(w, &http.Cookie{
Name: "registration_session",
Value: sessionID,
Path: "/",
MaxAge: 300,
HttpOnly: true,
})
// 5. 返回注冊(cè)選項(xiàng)給客戶端
json.NewEncoder(w).Encode(options)
}
注意:這段代碼中的session與傳統(tǒng)Web應(yīng)用中用于跟蹤用戶登錄狀態(tài)的session不同。這種session機(jī)制是WebAuthn協(xié)議的一部分,用于確保認(rèn)證流程的安全性:
- 防止重放攻擊:每次認(rèn)證都會(huì)生成新的挑戰(zhàn)
- 確保認(rèn)證操作的完整性:開(kāi)始認(rèn)證和完成認(rèn)證必須使用相同的session數(shù)據(jù)
- 時(shí)效性控制:認(rèn)證過(guò)程必須在有限時(shí)間內(nèi)完成(上面示例中的有效期為5分鐘)
所以這里的session更像是一個(gè)"挑戰(zhàn)-響應(yīng)"認(rèn)證過(guò)程中的臨時(shí)狀態(tài)存儲(chǔ),而不是用來(lái)維持用戶登錄狀態(tài)的傳統(tǒng)session。用戶的登錄狀態(tài)管理應(yīng)該是在這個(gè)認(rèn)證系統(tǒng)之上另外實(shí)現(xiàn)的,比如使用JWT token或傳統(tǒng)的session機(jī)制。
handleFinishRegistration用于處理客戶端發(fā)到/api/register/finish的完成注冊(cè)請(qǐng)求,它的邏輯大致如下:
func handleFinishRegistration(w http.ResponseWriter, r *http.Request) {
// 1. 獲取并驗(yàn)證會(huì)話
sessionData, ok := getSession(cookie.Value)
if !ok {
http.Error(w, "Invalid session", http.StatusBadRequest)
return
}
// 2. 獲取用戶信息
username := string(sessionData.UserID)
user := userDB.users[username]
// 3. 驗(yàn)證并完成注冊(cè)
credential, err := webAuthn.FinishRegistration(user, *sessionData, r)
// 4. 保存憑證
userDB.Lock()
user.Credentials = append(user.Credentials, *credential)
userDB.Unlock()
// 5. 清理會(huì)話
delete(sessionStore, cookie.Value)
}
注冊(cè)passkey后,我們就可以來(lái)基于passkey進(jìn)行登錄了!服務(wù)端會(huì)使用passkey對(duì)用戶進(jìn)行身份驗(yàn)證。
我們?cè)诘卿涊斎肟蛑休斎?tonybai",然后點(diǎn)擊"Passkey登錄",本地os會(huì)彈出身份驗(yàn)證對(duì)話框:
圖片
輸入os登錄密碼后,便可繼續(xù)身份驗(yàn)證過(guò)程,如果服務(wù)端身份驗(yàn)證ok,頁(yè)面會(huì)顯示下面“登錄成功”字樣:
圖片
如果在登錄輸入框中輸入一個(gè)未曾注冊(cè)過(guò)的用戶名,則服務(wù)器會(huì)驗(yàn)證失敗,頁(yè)面會(huì)顯示如下錯(cuò)誤:
圖片
和注冊(cè)過(guò)程一樣,上述的驗(yàn)證過(guò)程也是由兩個(gè)handler共同完成的,這也是webauthn規(guī)范確定的流程。
首先是處理/api/login/begin的handleBeginLogin,它的大致邏輯如下:
func handleBeginLogin(w http.ResponseWriter, r *http.Request) {
// 1. 驗(yàn)證用戶是否存在
user, ok := userDB.users[data.Username]
if !ok {
http.Error(w, "User not found", http.StatusNotFound)
return
}
// 2. 生成認(rèn)證選項(xiàng)和會(huì)話數(shù)據(jù)
options, sessionData, err := webAuthn.BeginLogin(user)
// 3. 存儲(chǔ)會(huì)話數(shù)據(jù)
sessionID := storeSession(sessionData)
http.SetCookie(w, &http.Cookie{
Name: "login_session",
Value: sessionID,
Path: "/",
MaxAge: 300,
HttpOnly: true,
})
// 4. 返回認(rèn)證選項(xiàng)給客戶端
json.NewEncoder(w).Encode(options)
}
之后,是handleFinishLogin處理的來(lái)自客戶端到/api/login/finish的請(qǐng)求,以完成登錄流程:
func handleFinishLogin(w http.ResponseWriter, r *http.Request) {
// 1. 獲取并驗(yàn)證會(huì)話
sessionData, ok := getSession(cookie.Value)
if !ok {
http.Error(w, "Invalid session", http.StatusBadRequest)
return
}
// 2. 獲取用戶信息
username := string(sessionData.UserID)
user := userDB.users[username]
// 3. 驗(yàn)證并完成登錄
_, err = webAuthn.FinishLogin(user, *sessionData, r)
// 4. 清理會(huì)話
delete(sessionStore, cookie.Value)
}
我們看到注冊(cè)和登錄都采用兩步驗(yàn)證流程,每個(gè)流程都包含開(kāi)始和完成兩個(gè)步驟,同時(shí)使用會(huì)話保持認(rèn)證狀態(tài)的連續(xù)性。
整個(gè)示例的前端基本由js代碼完成:
<!DOCTYPE html>
<html>
<head>
<title>Passkey Demo</title>
<style>
.container {
margin: 20px;
padding: 20px;
border: 1px solid #ccc;
}
.form-group {
margin: 10px 0;
}
#status {
margin-top: 20px;
padding: 10px;
}
.error {
color: red;
}
.success {
color: green;
}
</style>
</head>
<body>
<div class="container">
<h2>注冊(cè)</h2>
<div class="form-group">
<input type="text" id="registerUsername" placeholder="用戶名">
<button notallow="register()">注冊(cè) Passkey</button>
</div>
</div>
<div class="container">
<h2>登錄</h2>
<div class="form-group">
<input type="text" id="loginUsername" placeholder="用戶名">
<button notallow="login()">Passkey 登錄</button>
</div>
</div>
<div id="status"></div>
<script>
// 工具函數(shù):將 ArrayBuffer 轉(zhuǎn)換為 Base64URL 字符串
function bufferToBase64URL(buffer) {
const bytes = new Uint8Array(buffer);
let str = '';
for (const byte of bytes) {
str += String.fromCharCode(byte);
}
return btoa(str)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
// 工具函數(shù):將 Base64URL 字符串轉(zhuǎn)換為 ArrayBuffer
function base64URLToBuffer(base64URL) {
if (!base64URL) {
throw new Error('Empty base64URL string');
}
const base64 = base64URL.replace(/-/g, '+').replace(/_/g, '/');
const padLen = (4 - (base64.length % 4)) % 4;
const padded = base64.padEnd(base64.length + padLen, '=');
const binary = atob(padded);
const buffer = new ArrayBuffer(binary.length);
const bytes = new Uint8Array(buffer);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return buffer;
}
function showStatus(message, isError = false) {
const status = document.getElementById('status');
status.textContent = message;
status.className = isError ? 'error' : 'success';
}
// 開(kāi)始注冊(cè)
async function startRegistration(username) {
try {
// 1. 從服務(wù)器獲取注冊(cè)選項(xiàng)
const response = await fetch('/api/register/begin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username }),
});
if (!response.ok) {
throw new Error(`Server error: ${response.status}`);
}
const responseData = await response.json();
// 確保我們使用的是 publicKey 對(duì)象
const options = responseData.publicKey;
if (!options) {
throw new Error('Invalid server response: missing publicKey');
}
// 2. 解碼 challenge
options.challenge = base64URLToBuffer(options.challenge);
// 3. 解碼 user.id
if (options.user && options.user.id) {
options.user.id = base64URLToBuffer(options.user.id);
}
console.log('Processed options:', options); // 調(diào)試輸出
// 4. 創(chuàng)建憑證
const credential = await navigator.credentials.create({
publicKey: options
});
// 5. 準(zhǔn)備發(fā)送到服務(wù)器的數(shù)據(jù)
const registrationData = {
id: credential.id,
rawId: bufferToBase64URL(credential.rawId),
type: credential.type,
response: {
attestationObject: bufferToBase64URL(credential.response.attestationObject),
clientDataJSON: bufferToBase64URL(credential.response.clientDataJSON)
}
};
// 6. 發(fā)送注冊(cè)數(shù)據(jù)到服務(wù)器
const finishResponse = await fetch('/api/register/finish', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(registrationData)
});
if (!finishResponse.ok) {
throw new Error(`Server error: ${finishResponse.status}`);
}
showStatus('注冊(cè)成功!');
} catch (error) {
console.error('Registration error:', error);
showStatus(`注冊(cè)失敗: ${error.message}`, true);
}
}
// 開(kāi)始登錄
async function startLogin(username) {
try {
// 1. 從服務(wù)器獲取登錄選項(xiàng)
const response = await fetch('/api/login/begin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username }),
});
if (!response.ok) {
throw new Error(`Server error: ${response.status}`);
}
const responseData = await response.json();
const options = responseData.publicKey;
if (!options) {
throw new Error('Invalid server response: missing publicKey');
}
// 2. 解碼 challenge
options.challenge = base64URLToBuffer(options.challenge);
// 3. 解碼 allowCredentials
if (options.allowCredentials) {
options.allowCredentials = options.allowCredentials.map(credential => ({
...credential,
id: base64URLToBuffer(credential.id),
}));
}
// 4. 獲取憑證
const credential = await navigator.credentials.get({
publicKey: options
});
// 5. 準(zhǔn)備發(fā)送到服務(wù)器的數(shù)據(jù)
const loginData = {
id: credential.id,
rawId: bufferToBase64URL(credential.rawId),
type: credential.type,
response: {
authenticatorData: bufferToBase64URL(credential.response.authenticatorData),
clientDataJSON: bufferToBase64URL(credential.response.clientDataJSON),
signature: bufferToBase64URL(credential.response.signature),
userHandle: credential.response.userHandle ? bufferToBase64URL(credential.response.userHandle) : null
}
};
// 6. 發(fā)送登錄數(shù)據(jù)到服務(wù)器
const finishResponse = await fetch('/api/login/finish', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(loginData)
});
if (!finishResponse.ok) {
throw new Error(`Server error: ${finishResponse.status}`);
}
showStatus('登錄成功!');
} catch (error) {
console.error('Login error:', error);
showStatus(`登錄失敗: ${error.message}`, true);
}
}
// 注冊(cè)按鈕處理函數(shù)
function register() {
const username = document.getElementById('registerUsername').value;
if (!username) {
showStatus('請(qǐng)輸入用戶名', true);
return;
}
startRegistration(username);
}
// 登錄按鈕處理函數(shù)
function login() {
const username = document.getElementById('loginUsername').value;
if (!username) {
showStatus('請(qǐng)輸入用戶名', true);
return;
}
startLogin(username);
}
</script>
</body>
</html>
這段代碼沒(méi)有使用任何第三方庫(kù)或框架,對(duì)js略知一二的讀者想必也能看個(gè)七七八八。
綜上,我們看到這個(gè)示例實(shí)現(xiàn)提供了完整的Passkey認(rèn)證功能,但需要注意這是一個(gè)演示版本。在生產(chǎn)環(huán)境中,還需要考慮更多,比如數(shù)據(jù)的持久化存儲(chǔ)、更完善的錯(cuò)誤處理等。
3. 小結(jié)
本文粗略探討了無(wú)密碼認(rèn)證技術(shù)中的一種新興方案——passkey。隨著傳統(tǒng)密碼認(rèn)證的安全隱患日益嚴(yán)重,passkey作為FIDO聯(lián)盟提出的解決方案,利用生物識(shí)別和硬件加密以及非對(duì)稱加密等先進(jìn)技術(shù),為用戶提供了更安全、便捷的身份驗(yàn)證體驗(yàn)。
在文中,我還詳細(xì)介紹了passkey的工作原理,包括注冊(cè)和登錄流程,強(qiáng)調(diào)了非對(duì)稱加密在身份驗(yàn)證中的重要作用。此外,通過(guò)一個(gè)基于Go語(yǔ)言的示例,我們展示了如何實(shí)現(xiàn)passkey的注冊(cè)和認(rèn)證功能,幫助讀者更好地理解其實(shí)際應(yīng)用。
整體來(lái)看,passkey不僅提升了安全性,還改善了用戶體驗(yàn),是未來(lái)無(wú)密碼認(rèn)證的有力候選方案。隨著passkey技術(shù)的發(fā)展,期待更多應(yīng)用場(chǎng)景的出現(xiàn),為用戶帶來(lái)更安全的網(wǎng)絡(luò)環(huán)境。
本文涉及的源碼可以在這里[13]下載。
4. 參考資料
- passkey.org[14] - https://passkey.org
- passkeys.dev[15] - https://passkeys.dev
- webauthn.guide[16] - https://webauthn.guide/
- FIDO alliance[17] - https://fidoalliance.org/
- webauthn.io[18] - https://webauthn.io/
- WebAuthn 規(guī)范[19] - https://www.w3.org/TR/webauthn/
- FIDO2 文檔[20] - https://fidoalliance.org/fido2/