通過實例理解 Go Web 身份認證的幾種方式
在 2023 年 Q1 Go 官方用戶調查報告 [1] 中,API/RPC services、Websites/web services 都位於使用 Go 開發的應用類別的頭部 (如下圖):
我個人使用 Go 開發已很多年,但一直從事底層基礎設施、分佈式中間件等方向,Web 應用開發領域涉及較少,像 Web 應用領域常見的 CRUD 更是少有涉獵,不能不說是一種 “遺憾”^_^。未來一段時間,團隊會接觸到 Web 應用的開發,我打算對 Go Web 應用開發的重點環節做一個快速系統的梳理。
而身份認證 (Authentication,簡稱 AuthN) 是 Web 應用開發中一個關鍵的環節,也是首個環節,它負責驗證用戶身份,讓用戶可以以認證過的身份訪問系統中的資源和信息。
Go 語言作爲一門優秀的 Web 開發語言,提供了豐富的機制來實現 Web 應用的用戶身份認證。在這篇文章中,我就通過 Go 示例和大家一起探討一下當前 Web 應用開發中幾種常見的主流身份認證方式,幫助自己和各位讀者邁出 Web 應用開發修煉之路的第一步。
- 身份認證簡介
1.1 身份認證解決的問題
身份認證不侷限於 Web 應用,各種系統都會有身份認證,但本文我們聚焦 Web 應用領域的身份認證技術。
幾乎所有 Web 應用的安全性都是從身份認證開始的,身份認證是驗證用戶身份真實性的過程,是我們首先要部署的策略。位於下游的安全控制,如授權 (Authorization, AuthZ)、審計日誌(Audit log) 等,幾乎都需要用戶的身份。
身份認證的英文是 Authentication,簡寫爲 AuthN,大家不要將之與授權 Authorization(AuthZ) 混淆 (在後續系列文章中會繼續探討 AuthZ 相關的內容),他們所要解決的問題相似,但有不同,也有先後。通常先 AuthN,再 AuthZ。我們可以用下面的比喻來形象地解釋二者的聯繫與差異:
-
AuthN 就像是進入公司大樓的安檢,負責檢查員工的身份是否合法,是否具有進入公司的資格,它解決的是驗證員工身份的問題。
-
AuthZ 更像是公司內部的權限管理,某個員工進入了公司後 (AuthN 後) 想訪問一些重要資料,這時還需要確認該員工是否有相應的訪問權限。它解決的是授權訪問控制的問題。
簡單來說,AuthN 是驗證你是誰,authZ 是驗證你有哪些權限。AuthN 解決認證問題,AuthZ 解決授權問題,這兩個都重要,AuthN 解決外部的安全問題,authZ 解決內部的安全與合規問題。
1.2 身份認證的三要素
身份認證需要被認證方提供一些身份信息輸入,這些代表身份信息的輸入被稱爲身份認證要素(authentication factor)。這些要素有很多,大致可分爲三類:
- 你知道的東西 (What you know)
即基於被認證方知道的特定信息來驗證身份,最常見的如密碼等。
- 你擁有的東西 (What you have)
基於被認證方所擁有的特定物件來驗證身份,最常見的利用數字證書、令牌卡等。N 年前,在移動端應用還沒有發展起來時,一些人在銀行辦理電子銀行業務時會拿到一個 U 盾 (又稱爲 USBKey),其中存放着用於用戶身份識別的數字證書,這個 U 盾就屬於此類要素。
上面比喻中進入大樓時使用的員工卡也屬於這類要素。
- 你本身就具有的 (What you are)
即基於被認證方所擁有的生物特徵要素 (biometric factor) 來驗證身份,最常見的人臉識別、指紋 / 聲紋 / 虹膜識別和解鎖等。理論上來說,具備個人生物特徵的身份認證標誌具有不可仿冒性、唯一性。
如果上面比喻中的大樓已經開啓了人臉識別功能,那麼基於人臉識別的認證就屬於這類要素的認證。
通常我們會基於單個要素設計身份認證方案,一旦使用兩個或兩個以上不同類的要素,就可以被稱爲 ** 雙因素認證 (2FA)[2] 或多因素認證 (MFA)** 了。不過,2FA 和 MFA 都比較複雜,不再本篇文章討論範圍之內。
基於上述要素,我們就可以設計和實現各種適合不同類別 Web 應用或 API 服務的身份認證方法了。Web 應用和 API 服務都需要身份認證,它們有什麼差異呢?這些差異是否會對身份認證方案產生影響呢?我們接下來看一下。
1.3 Web 應用身份認證 vs. API 服務身份認證
Web 應用和 API 服務主要有以下幾點區別:
- 交互方式不同
Web 應用是瀏覽器與服務器之間的交互,用戶通過瀏覽器訪問 Web 應用。而 API 服務是程序 / 應用與服務器之間的交互,通過 API 請求獲取數據或執行操作。
- 返回數據格式不同
Web 應用通常會返回 html/js/css 等瀏覽器可解析執行的代碼,而 API 服務通常返回結構化數據,常見的如 JSON 或 XML 等。
- 使用場景不同
Web 應用主要面向人類用戶的使用,用戶通過瀏覽器進行操作。而 API 服務主要被其他程序調用,爲程序之間提供接口與數據支撐。
- 狀態管理不同
Web 應用在服務端保存會話狀態,瀏覽器通過 cookie 等保存用戶狀態。而 API 服務通常是無狀態的,每次請求都需要攜帶用於身份認證的信息,比如訪問令牌或 API Key 等。
- 安全方面的關注點不同
Web 應用更關注 XSS[3]、CSRF[4] 等輸入驗證安全,而 API 服務更關注身份認證 (authN)、授權 (authZ)、准入 (admission)、限流等訪問控制安全。
總之,Web 應用注重界面的展示和用戶交互;而 API 服務注重數據和服務的提供,它們有不同的使用場景、交互方式和安全關注點。
Web 應用和 API 服務的這些差異也導致了 Web 應用和 API 服務適合使用的身份認證方案上會有所不同。但前後端分離架構的出現和普及,讓前後端責任分離:前端專注於視圖和交互,後端專注數據和業務,並且前後端通過標準化的 API 接口進行數據交互。這可以讓後端提供統一的認證接口,不同的前端可以共享。像基於 Token 這樣的無狀態易理解的身份驗證機制逐漸成爲主流。也就是說,架構模式的變化,使得 Web 應用和 API 服務在身份驗證 (authN) 方案上出現了一些融合的現象,因此在身份認證方法上,Web 應用和 API 服務也存在一些交集。
下面維韋恩圖列出了三類身份認證方法,包括僅適用於 Web 應用的、僅適用於 API 服務的以及兩者都適用的:
本文聚焦 Web 應用的身份認證方式,接下來會重點說說上圖中綠色背景色的幾種身份認證方式。
- 安全信道是身份認證的前提和基礎
在對具體的 Web 身份認證方式進行說明之前,我們先來了解一下身份認證的前提和基礎 - 安全信道。
在 Web 應用身份認證的過程中,無論採用何種認證方式,用戶的身份要素信息 (用戶名 / 密碼、token、生物特徵信息) 都要傳遞給服務器,這時候如果傳遞此類信息的通信信道不安全,這些重要的認證要素信息就很容易被中間人截取、破解、篡改並被冒充,從而獲得 Web 應用的使用權。從服務端角度來看,如果沒有安全信道,服務器身份也容易被僞裝,導致用戶連接到 “冒牌服務器” 並導致嚴重後果。因此,沒有建立在安全信道上的身份認證是不安全,不具備實際應用價值的,甚至是完全沒有意義的。
此外,安全信道不僅對登錄階段的身份認證環節有重要意義,在用戶已登錄並訪問 Web 應用其他功能頁面時,安全通道也可以對數據的傳輸以及類似訪問令牌或 Cookie 數據的傳輸起到加密和保護作用。
在 Web 應用領域,最常用的安全信道建立方式是基於 HTTPS(HTTP over TLS) 或直接建立在 TLS 之上的自定義通信,TLS 利用證書對通信進行加密、驗證服務器身份(甚至是客戶端身份的驗證),保障信息的機密性和完整性。各大安全規範和標準如 PCI DSS(Payment Card Industry Data Security Standard)[5]、OWASP[6] 也強制要求使用 HTTPS 保障認證安全。
基於安全信道,我們還可以實施第一波的身份認證,這就是我們通常所說的基於 HTTPS(或 TLS) 的雙向身份認證。
注:在我的《Go 語言精進之路 vol2》[7] 一書中,對 TLS 的機制以及基於 Go 標準庫的 TLS 的雙向認證 [8] 有系統全面的說明,歡迎各位童鞋閱讀反饋。
這種認證方式採用的是身份認證要素中的第二類要素:What you have。客戶端帶着歸屬於自己的專有證書去服務端做身份驗證。如果 client 證書通過服務端的驗籤後,便可允許 client 進入 “大樓”。
下面是一個基於 TLS 證書做身份認證的客戶端與服務端交互的示意圖:
我們先看看對應上述示意圖中的客戶端的代碼:
// authn-examples/tls-authn/client/main.go
func main() {
// 1. 讀取客戶端證書文件
clientCert, err := tls.LoadX509KeyPair("client-cert.pem", "client-key.pem")
if err != nil {
log.Fatal(err)
}
// 2. 讀取中間CA證書文件
caCert, err := os.ReadFile("inter-cert.pem")
if err != nil {
log.Fatal(err)
}
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(caCert)
// 3. 發送請求
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{clientCert},
RootCAs: certPool,
},
},
}
req, err := http.NewRequest("GET", "https://server.com:8443", nil)
if err != nil {
log.Fatal(err)
}
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
// 4. 打印響應信息
fmt.Println("Response Status:", resp.Status)
// fmt.Println("Response Headers:", resp.Header)
body, _ := io.ReadAll(resp.Body)
fmt.Println("Response Body:", string(body))
}
客戶端加載 client-cert.pem 作爲後續與服務端通信的身份憑證,加載 inter-cert.pem 用於校驗服務端在 tls 握手過程發來的服務端證書 (server-cert.pem),避免連接到 “冒牌站點”。通過驗證後,客戶端向服務端發起 Get 請求並輸出響應的內容。
下面是服務端的代碼:
// authn-examples/tls-authn/server/main.go
func main() {
var validClients = map[string]struct{}{
"client.com": struct{}{},
}
// 1. 加載證書文件
cert, err := tls.LoadX509KeyPair("server-cert.pem", "server-key.pem")
if err != nil {
log.Fatal(err)
}
caCert, err := os.ReadFile("inter-cert.pem")
if err != nil {
log.Fatal(err)
}
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(caCert)
// 2. 配置TLS
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.RequireAndVerifyClientCert, // will trigger the invoke of VerifyPeerCertificate
ClientCAs: certPool,
}
// tls.Config設置
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
// 獲取客戶端證書
cert := verifiedChains[0][0]
// 提取CN作爲客戶端標識
clientID := cert.Subject.CommonName
fmt.Println(clientID)
_, ok := validClients[clientID]
if !ok {
return errors.New("invalid client id")
}
return nil
}
// 添加處理器
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World!"))
})
// 3. 創建服務器
srv := &http.Server{
Addr: ":8443",
TLSConfig: tlsConfig,
}
// 4. 啓動服務器
err = srv.ListenAndServeTLS("", "")
if err != nil {
log.Fatal(err)
}
}
注:在你的實驗環境中,需要在 / etc/hosts 文件中添加 server.com 的映射 ip 爲 127.0.0.1。
服務端代碼也不復雜,比較 “套路化”:加載服務端證書和中間 CA 證書 (用於驗籤 client 端的證書),這裏將 tls.Config.ClientAuth 設置爲 RequireAndVerifyClientCert,這會觸發服務端對客戶端證書的驗籤,同時在 tlsConfig.VerifyPeerCertificate 不爲 nil 的情況下,觸發對 tlsConfig.VerifyPeerCertificate 的函數的調用,在示例代碼中,我們爲 tlsConfig.VerifyPeerCertificate 賦值了一個匿名函數實現,在這個函數中,我們提取了客戶端證書中的客戶端標識 CN,並查看其是否在可信任的客戶端 ID 表中。
在這個示例中,這個 tlsConfig.VerifyPeerCertificate 執行的驗證有些多餘,但我們在實際代碼中可以使用 tlsConfig.VerifyPeerCertificate 來設置黑名單,攔截那些尚未過期、但可以驗籤通過的客戶端,實現一種客戶端證書過期前的作廢機制。
此外,上述示例中客戶端、服務端以及中間 CA 證書的製作代碼與《Go TLS 服務端綁定證書的幾種方式》[9] 一文中的證書製作很類似,大家可以直接參考本文示例代碼中的 tls-authn/make-certs 下面的代碼,這裏就不贅述了。
通過這種基於安全信道的身份驗證方式,客戶端證書可以強制認證用戶,理論上不需要額外再用用戶名密碼。認證之後客戶端在這個 TLS 連接上發送的所有信息都將綁定其身份。
不過通過頒發客戶端專用證書的方式僅適合一些像網絡銀行之類的專有業務,大多數 Web 應用會與客戶端間建立安全信道,但不會採用客戶端證書來認證用戶身份,在這樣的情況下,下面要說的這些身份認證方式就可以發揮作用了。
我們先來看一下最傳統的基於密碼的認證。
- 基於密碼的認證
基於密碼的認證屬於基於第一類身份認證要素:你知道的東西 (What you know) 的認證方式,這類認證也是 Web 應用中最經典、最常見的認證方式。我們先從基於傳統表單承載用戶名 / 密碼說起。
3.1. 基於用戶名 + 密碼的認證 (傳統表單方式)
這是最常見的 Web 應用認證方式:用戶通過提交包含用戶名和密碼的表單 (Form),服務端 Web 應用進行驗證。下面使用這種方式的客戶端與服務單的交互示意圖:
接下來,我們看看對應上述示意圖的實現代碼。我們先建立一個 html 文件,該文件非常簡單,就是一個可輸入用戶名和密碼的表單,點擊登錄按鈕將表單信息發送到服務端:
// authn-examples/password/classic/login.html
<!DOCTYPE html>
<html>
<head>
<title>登錄</title>
</head>
<body>
<form action="http://server.com:8080/login" method="post">
<label>用戶名:</label>
<input type="text" />
<label>密碼:</label>
<input type="password" />
<button type="submit">登錄</button>
</form>
</body>
</html>
發送的 HTTP Post 請求的包體 (Body) 中會包含頁面輸入的 username 和 password 的值,形式如下:
username=admin&password=123456
而我們的服務端的代碼如下:
// authn-examples/password/classic/main.go
func main() {
http.HandleFunc("/login", login)
http.ListenAndServe(":8080", nil)
}
func login(w http.ResponseWriter, r *http.Request) {
username := r.FormValue("username")
password := r.FormValue("password")
if isValidUser(username, password) {
w.Write([]byte("Welcome!"))
return
}
http.Error(w, "Invalid username or password", http.StatusUnauthorized) // 401
}
var credentials = map[string]string{
"admin": "123456",
}
func isValidUser(username, password string) bool {
// 驗證用戶名密碼
v, ok := credentials[username]
if !ok {
return false
}
if v != password {
return false
}
return true
}
服務端通過 Request 的 FormValue 方法獲得 username 和 password 的值,並與 credentials 存儲的合法用戶信息比對 (當然這只是演示代碼中的臨時手段,生產中不要這麼存儲用戶信息),比對成功,返回 "Welcome" 應答;比對失敗,返回 401 Unauthorized 錯誤。
注:包括本示例在內的後續所有示例的客戶端和服務端都在非安全信道上通信,目的是簡化示例代碼的編寫。大家在生產環境務必建立安全信道後再做後續的身份驗證。
基於傳統的表單用戶名和密碼可以作爲 Web 應用服務端身份驗證的方案,但問題來了:服務端認證成功後,用戶後續向 Web 應用服務端發起的請求是否還要繼續帶上用戶和密碼信息呢?如果不帶上用戶和密碼信息,服務端又如何驗證這些請求是來自之前已經認證成功後的用戶;如果後續每個請求都帶上以 Form 形式承載的用戶名和密碼,使用起來又非常不方便,還影響後續請求的正常數據的傳輸 (對 Body 數據有侵入)。
於是便有了 Session(會話)機制,它可以被認爲是基於經典的用戶名密碼 (表單承載) 認證方式的“延續”,使得密碼認證的成果不再侷限在缺乏連續性的單一請求級別上,而是擴展到後續的一段時間內或一系列與 Web 應用的互操作過程中,變成了連續、持久的登錄會話。
接下來,我們就來簡單看看基於 Session 的後續認證方式是如何工作的。
3.2 使用 Session:有狀態的認證方式
基於 Session 的認證方式是一種有狀態的方案,服務端會爲每個身份認證成功的用戶建立並保存相關 session 信息,同時服務端也會要求客戶端在瀏覽器側持久化與該 Session 有關少量信息,通常客戶端會通過開啓 Cookie 的方式來保存與用戶 Session 相關的信息。
服務端保存 Session 有多種方式,可以在進程內存中、文件中、數據庫、緩存 (Redis) 等,不同方式各有優缺點,比如將 Session 保存在內存中,最大的好處就是實現簡單且速度快,但由於不能持久化,服務實例重啓後就會丟失,此外當服務端有多副本時,session 信息無法在多實例共享;使用關係數據庫來保存 session,可以方便持久化,也方便與服務端多實例用戶數據共享,但數據庫交互成本較大;而使用緩存 (Redis) 存儲 session 信息是目前比較主流的方式,簡單、安全、快速,還可以很好地適合分佈式環境下 session 的共享。
下面是一個常見的基於 cookie 實現的 session 機制的客戶端與服務端的交互示意圖:
這裏也給出上述示意圖的一個參考實現示例(代碼僅用作演示,很多值設置並不規範和安全,不要用於生產)。
session 機制的開啓從用戶登錄開始,這個示例裏的 login.html 與上一個示例是一樣的:
// authn-examples/password/session/login.html
<!DOCTYPE html>
<html>
<head>
<title>登錄</title>
</head>
<body>
<form action="http://server.com:8080/login" method="post">
<label>用戶名:</label>
<input type="text" />
<label>密碼:</label>
<input type="password" />
<button type="submit">登錄</button>
</form>
</body>
</html>
服務端負責的 login Handler 代碼如下:
// authn-examples/password/session/main.go
var store = sessions.NewCookieStore([]byte("session-key"))
func main() {
http.HandleFunc("/login", login)
http.HandleFunc("/calc", calc)
http.HandleFunc("/calcAdd", calcAdd)
http.ListenAndServe(":8080", nil)
}
var credentials = map[string]string{
"admin": "123456",
"test": "654321",
}
func isValid(username, password string) bool {
// 驗證用戶名密碼
v, ok := credentials[username]
if !ok {
return false
}
if v != password {
return false
}
return true
}
func base64Encode(src string) string {
encoded := base64.StdEncoding.EncodeToString([]byte(src))
return encoded
}
func base64Decode(encoded string) string {
decoded, _ := base64.StdEncoding.DecodeString(encoded)
return string(decoded)
}
func randomStr() string {
// 生成隨機數
rand.Seed(time.Now().UnixNano())
random := rand.Intn(100000)
// 格式化爲05位字符串
str := fmt.Sprintf("%05d", random)
return str
}
func login(w http.ResponseWriter, r *http.Request) {
username := r.FormValue("username")
password := r.FormValue("password")
if isValid(username, password) {
session, err := store.Get(r, "server.com_"+username)
if err != nil {
fmt.Println("get session from session store error:", err)
http.Error(w, "Internal error", http.StatusInternalServerError)
}
// 設置session數據
random := randomStr()
usernameB64 := base64Encode(username + "-" + random)
session.Values["random"] = random
session.Save(r, w)
// 設置cookie
cookie := http.Cookie{Name: "server.com-session", Value: usernameB64}
http.SetCookie(w, &cookie)
// 登錄成功,跳轉到calc頁面
http.Redirect(w, r, "/calc", http.StatusSeeOther)
} else {
http.Error(w, "Invalid username or password", http.StatusUnauthorized) // 401
}
}
我們使用了 gorilla/sessions 這個 Go 社區廣泛使用的 session 庫來實現服務端 session 的相關操作。以 admin 用戶登錄爲例,當用戶名和密碼認證成功後,我們在 session store 中創建一個新的 session:server.com_admin。然後生成一個隨機數,將隨機數存儲在該 session 的名爲 "random" 的 key 的下面。之後,讓客戶端設置 cookie,name 爲 server.com-session。值爲 username 和 random 按特定格式組合後的 base64 編碼值。
登錄成功後,瀏覽器會跳到 calc 頁面,這裏我們輸入兩個整數,並點擊 "calc" 按鈕提交,提交動作會發送請求到 calcAdd Handler 中:
// authn-examples/password/session/main.go
func calcAdd(w http.ResponseWriter, r *http.Request) {
// 1. 獲取Cookie中的Session
cookie, err := r.Cookie("server.com-session")
if err != nil {
http.Error(w, "找不到cookie,請重新登錄", 401)
return
}
fmt.Printf("found cookie: %#v\n", cookie)
// 2. 獲取Session對象
usernameB64 := cookie.Value
usernameWithRandom := base64Decode(usernameB64)
ss := strings.Split(usernameWithRandom, "-")
username := ss[0]
random := ss[1]
session, err := store.Get(r, "server.com_"+username)
if err != nil {
http.Error(w, "找不到session, 請重新登錄", 401)
return
}
randomInSs := session.Values["random"]
if random != randomInSs {
http.Error(w, "session中信息不匹配, 請重新登錄", 401)
return
}
// 3. 轉換爲整型參數
a, err := strconv.Atoi(r.FormValue("a"))
if err != nil {
http.Error(w, "參數錯誤", 400)
return
}
b, err := strconv.Atoi(r.FormValue("b"))
if err != nil {
http.Error(w, "參數錯誤", 400)
return
}
// 4. 計算並返回結果
result := a + b
w.Write([]byte(fmt.Sprintf("%d", result)))
}
calcAdd Handler 會提取 Cookie "server.com-session" 中的值,根據值信息查找服務端本地是否存儲了對應的 session,並校驗與 session 中存儲的隨機碼是否一致。驗證通過後,直接返回結算結果;否則提醒客戶端重新登錄。
前面說過,session 是一種有狀態的輔助身份認證機制,需要客戶端和服務端的配合完成,一旦客戶端禁用了 Cookie 機制,上述的示例實現就失效了。當然有讀者會說,Session 可以不基於 Cookie 來實現,可以用 URL 重寫、隱藏表單字段、將 Session ID 放入 URL 路徑等方式來實現,客戶端也可以用 LocalStorage 等前端存儲機制來替代 Cookie。但無論哪種實現,這種有狀態機制帶來的複雜性都不低,並且在分佈式環境中需要 session 共享和同步機制,影響了 scaling。
隨着微服務架構的廣泛使用,無需在服務端存儲額外信息、天然支持後端服務分佈式多實例的無狀態的連續身份認證機制受到了更多的青睞。
其實基於 HTTP 的無狀態認證機制早已有之,最常見的莫過於 Basic Auth 了,接下來,我們就從 Basic Auth 開始,說幾種無狀態身份認證機制。
3.3 Basic Auth:最早的無狀態認證方式
Basic Auth 是 HTTP 最原始的身份驗證方式,在 HTTP1.0 規範中就已存在,其原因是 HTTP 是無狀態協議,每次請求都需要進行身份驗證才能訪問受保護資源。
Basic Auth 的原理也十分簡單,客戶端與服務端的交互如下圖:
Basic Auth 通過在客戶端的請求報文中添加 HTTP Authorization Header 的形式向服務器端發送認證憑據。HTTP Authorization Header 的構建通常分兩步。
-
將 “username:password” 的組合字符串進行 Base64 編碼,編碼值記作 b64Token。
-
將 Authorization: Basic b64Token 作爲 HTTP header 的一個字段發送給服務器端。
服務端收到請請求後提取出 Authorization 字段並做 Base64 解碼,得到 username 和 password,然後與存儲的信息作比對進行客戶端身份認證。
我們來看一個與上圖對應的示例的代碼,先看客戶端:
// authn-examples/password/basic/client/main.go
func main() {
client := &http.Client{}
req, _ := http.NewRequest("POST", "http://server.com:8080/", nil)
// 發送默認請求
response, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
// 解析響應頭
authHeader := response.Header.Get("WWW-Authenticate")
loginReq, _ := http.NewRequest("POST", "http://server.com:8080/login", nil)
username := "admin"
password := "123456"
// 判斷認證類型
if !strings.Contains(authHeader, "Basic") {
// 不支持的認證類型
fmt.Println("Unsupported authentication type:", authHeader)
return
}
// 使用Basic Auth, 添加Basic Auth頭
loginReq.SetBasicAuth(username, password)
response, err = client.Do(loginReq)
// 打印響應狀態
fmt.Println(response.StatusCode)
// 打印響應包體
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
客戶端的代碼比較簡單,並且流程與圖中的交互流程是完全一樣的。而服務端就是一個簡單的 http server,對來自客戶端的帶有 basic auth 的請求進行身份認證:
// authn-examples/password/basic/server/main.go
func main() {
// 創建一個基本的HTTP服務器
mux := http.NewServeMux()
username := "admin"
password := "123456"
// 針對/的handler
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
// 返回401 Unauthorized響應
w.Header().Set("WWW-Authenticate", "Basic realm=\"server.com\"")
w.WriteHeader(http.StatusUnauthorized)
})
// login handler
mux.HandleFunc("/login", func(w http.ResponseWriter, req *http.Request) {
// 從請求頭中獲取Basic Auth認證信息
user, pass, ok := req.BasicAuth()
if !ok {
// 認證失敗
w.WriteHeader(http.StatusUnauthorized)
return
}
// 驗證用戶名密碼
if user == username && pass == password {
// 認證成功
w.WriteHeader(http.StatusOK)
w.Write([]byte("Welcome to the protected resource!"))
} else {
// 認證失敗
http.Error(w, "Invalid username or password", http.StatusUnauthorized)
}
})
// 監聽8080端口
err := http.ListenAndServe(":8080", mux)
if err != nil {
log.Fatal(err)
}
}
採用 Basic Auth 身份認證方案的客戶端在每個請求中都要在 Header 中加上 Basic Auth 形式的身份信息,但服務端無需像 Session 那樣存儲任何額外的信息。
不過很顯然,Basic Auth 這種採用明文傳輸身份信息的方式在安全性方面飽受詬病,爲了避免在 Header 傳輸明文的安全問題,RFC 2617(以及後續更新版 RFC 7616) 定義了 HTTP Digest 身份認證方式。Digest 訪問認證不再明文傳輸密碼,而是傳遞用 hash 算法處理後密碼摘要,相對 Basic Auth 驗證安全性更高。接下來,我們就來看看 HTTP Digest 認證方式。
3.4 基於 HTTP Digest 認證
Digest 是一種 HTTP 摘要認證,你可以把它看作是 Basic Auth 的改良版本,針對 Base64 明文發送的風險,Digest 認證把用戶名和密碼加鹽(一個被稱爲 Nonce 的隨機值作爲鹽值)後,再通過 MD5/SHA 等哈希算法取摘要放到請求的 Header 中發送出去。Digest 的認證過程如下圖:
相對於 Basic Auth,Digest Auth 的一些值的生成過程還是略複雜的,這裏給出一個示例性質的代碼示例,可能不完全符合 Digest 規範,大家通過示例理解 Digest 的認證過程就可以了。
注:如要使用符合 RFC 7616 的 Digest 規範(或老版 RFC 2617 規範),可以找一些第三方包,比如 https://github.com/abbot/go-http-auth(只滿足 RFC 2617)。
// authn-examples/password/digest/client/main.go
func main() {
client := &http.Client{}
req, _ := http.NewRequest("POST", "http://server.com:8080/", nil)
// 發送默認請求
response, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
// 解析響應頭
authHeader := response.Header.Get("WWW-Authenticate")
loginReq, _ := http.NewRequest("POST", "http://server.com:8080/login", nil)
username := "admin"
password := "123456"
// 判斷認證類型
if !strings.Contains(authHeader, "Digest") {
// 不支持的認證類型
fmt.Println("Unsupported authentication type:", authHeader)
return
}
// 使用Digest Auth
//隨機數
cnonce := GenNonce()
//生成HA1
ha1 := GetHA1(username, password, cnonce)
//構建Authorization頭
auth := "Digest username=\"" + username + "\", nonce=\"" + cnonce + "\", algorithm=MD5, response=\"" + GetResponse(ha1, cnonce) + "\""
loginReq.Header.Set("Authorization", auth)
response, err = client.Do(loginReq)
// 打印響應狀態
fmt.Println(response.StatusCode)
// 打印響應包體
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
// 生成隨機數
func GenNonce() string {
h := md5.New()
io.WriteString(h, fmt.Sprint(rand.Int()))
return hex.EncodeToString(h.Sum(nil))
}
// 根據用戶名密碼和隨機數生成HA1
func GetHA1(username, password, cnonce string) string {
h := md5.New()
io.WriteString(h, username+":"+cnonce+":"+password)
return hex.EncodeToString(h.Sum(nil))
}
// 根據HA1,隨機數生成response
func GetResponse(ha1, cnonce string) string {
h := md5.New()
io.WriteString(h, strings.ToUpper("md5")+":"+ha1+":"+cnonce+"::"+strings.ToUpper("md5"))
return hex.EncodeToString(h.Sum(nil))
}
客戶端使用 username、password 和隨機數生成摘要以及一個 response 碼,並通過請求的頭 Authorization 字段發給服務端。
服務端解析 Authorization 字段中的各個值,然後採用同樣的算法算出一個新 response,與請求中的 response 比對,如果一致,則認爲認證成功:
// authn-examples/password/digest/server/main.go
func main() {
mux := http.NewServeMux()
password := "123456"
// 針對/的handler
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
// 返回401 Unauthorized響應
w.Header().Set("WWW-Authenticate", "Digest realm=\"server.com\"")
w.WriteHeader(http.StatusUnauthorized)
})
// login handler
mux.HandleFunc("/login", func(w http.ResponseWriter, req *http.Request) {
fmt.Println(req.Header)
//驗證參數
if Verify(req, password) {
fmt.Fprintln(w, "Verify Success!")
} else {
w.WriteHeader(401)
fmt.Fprintln(w, "Verify Failed!")
}
})
// 監聽8080端口
err := http.ListenAndServe(":8080", mux)
if err != nil {
log.Fatal(err)
}
}
func Verify(r *http.Request, password string) bool {
auth := r.Header.Get("Authorization")
params := strings.Split(auth, ",")
var username, cnonce, response string
for _, p := range params {
p := strings.Trim(p, " ")
kv := strings.Split(p, "=")
if kv[0] == "Digest username" {
username = strings.Trim(kv[1], "\"")
}
if kv[0] == "nonce" {
cnonce = strings.Trim(kv[1], "\"")
}
if kv[0] == "response" {
response = strings.Trim(kv[1], "\"")
}
}
if username == "" {
return false
}
//根據用戶名密碼及隨機數生成HA1
ha1 := GetHA1(username, password, cnonce)
//自己生成response與請求中response對比
return response == GetResponse(ha1, cnonce)
}
雖然實現了無狀態,安全性也高於 Basic Auth,但 Digest 方式的用戶體驗依然有限:每次向服務端發送請求,客戶端都要進行一次複雜計算,服務端也要再做一次相同的驗算和比對。
那麼是否有一種體驗更爲良好的無狀態身份認證方式呢?我們接下來看看基於 Token 的認證方式。
- 無狀態:基於 Token 的認證
基於 Token 的認證方式的備受青睞得益於 Web 領域前後端分離架構的發展以及微服務架構的流行,在 API 調用和網站間需要輕量級的認證機制來傳遞用戶信息。Token 認證機制正好滿足這一需求,而 JWT(JSON Web Token)[10] 是目前 Token 格式標準中使用最廣的一種。
4.1 JWT 原理
JWT 由頭部 (Header)、載荷(Payload) 和簽名 (Signature) 三部分組成,三部分之間用圓點連接,其形式如下:
xxxxx.yyyyy.zzzzz
一個真實的 JWT token 的例子如下面來自 jwt.io[11] 站點的截圖):
JWT token 的生成過程也非常清晰,下圖展示了上述截圖中 jwt token 的生成過程:
如果你不想依賴第三方庫,也可以自己實現生成 token 的函數,下面是一個示例:
// authn-examples/jwt/scratch/main.go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
)
type Header struct {
Alg string `json:"alg"`
Typ string `json:"typ"`
}
type Claims struct {
Sub string `json:"sub"`
Name string `json:"name"`
Iat int64 `json:"iat"`
}
// GenerateToken:不依賴第三方庫的JWT生成實現
func GenerateToken(claims *Claims, key string) (string, error) {
header, _ := json.Marshal(Header{
Alg: "HS256",
Typ: "JWT",
})
// 序列化Payload
payload, err := json.Marshal(claims)
if err != nil {
return "", err
}
// 拼接成JWT字符串
headerEncoded := base64.RawURLEncoding.EncodeToString(header)
payloadEncoded := base64.RawURLEncoding.EncodeToString([]byte(payload))
encodedToSign := headerEncoded + "." + payloadEncoded
// 使用HMAC+SHA256簽名
hash := hmac.New(sha256.New, []byte(key))
hash.Write([]byte(encodedToSign))
sig := hash.Sum(nil)
sigEncoded := base64.RawURLEncoding.EncodeToString(sig)
var token string
token += headerEncoded
token += "."
token += payloadEncoded
token += "."
token += sigEncoded
return token, nil
}
func main() {
var claims = &Claims{
Sub: "1234567890",
Name: "John Doe",
Iat: 1516239022,
}
result, _ := GenerateToken(claims, "iamtonybai")
fmt.Println(result)
}
對照着上面圖示的流程,理解這個示例非常容易。當然 jwt.io 官方也維護了一個使用簡單且靈活性更好的 Go module:golang-jwt/jwt[12],用這個 go module 生成上述 token 的示例代碼如下:
// authn-examples/jwt/golang-jwt/main.go
import (
"fmt"
"time"
jwt "github.com/golang-jwt/jwt/v5"
)
type MyCustomClaims struct {
Sub string `json:"sub"`
Name string `json:"name"`
jwt.RegisteredClaims // use its Subject and IssuedAt
}
func main() {
mySigningKey := []byte("iamtonybai")
// Create claims with multiple fields populated
claims := MyCustomClaims{
Name: "John Doe",
Sub: "1234567890",
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(time.Unix(1516239022, 0)), // 1516239022
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, _ := token.SignedString(mySigningKey)
fmt.Println(ss)
_, err := verifyToken(ss, "iamtonybai")
if err != nil {
fmt.Println("invalid token:", err)
return
}
fmt.Println("valid token")
}
這段代碼中還包含了一個對 jwt token 驗證合法性的函數 verifyToken,服務端每次收到客戶端請求中攜帶的 token 時,都可以使用 verifyToken 來驗證 token 是否合法,下面是 verifyToken 的實現邏輯:
// authn-examples/jwt/golang-jwt/main.go
// verifyToken 驗證JWT函數
func verifyToken(tokenString, key string) (*jwt.Token, error) {
// 解析Token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte(key), nil
})
if err != nil {
return nil, err
}
// 驗證簽名
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return token, nil
}
服務端驗證 token 的邏輯是先解析 token,得到 header、payload 對應的 base64UrlEncoded 後的結果,然後用 key 重新生成簽名,對比生成的簽名與 token 攜帶的簽名是否一致。
那麼在 Web 應用中如何實現基於 jwt token 的身份認證呢?我們繼續往下看。
4.2 使用 JWT token 做身份認證
在前面講解 Basic Auth、Digest Auth 時,Basic Auth、Digest 等服務端認證方式利用了 HTTP Header 的 Authorization 字段,基於 JWT token 的認證也是基於 Authorization 字段,只不過前綴從 Basic、Digest 換成了 Bearer:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2OTc4NjE5MzIsInVzZXJuYW1lIjoiYWRtaW4ifQ.go6NhfmYPZbtHEuJ1oULG890neo0yVdtFJwfAvHhxyE
基於 JWT token 的身份認證方式的客戶端與服務端的交互流程如下圖:
在這幅示意圖中,客戶端先用 basic auth 方式登錄服務端,服務端驗證通過後,在登錄應答中寫入一個 jwt token 作爲後續客戶端訪問服務端其他功能的依據。客戶端從登錄應答的包體中解析出 jwt token 後,可以將該 token 存放在 LocalStorage 中,然後在後續的發向該服務端的所有請求中都帶上這個 jwt token。服務端對這些請求都會校驗其攜帶的 jwt token,只有驗證通過的請求才能被正確處理。
下面來看看對應示意圖的示例源碼,先來看一下客戶端:
// authn-examples/jwt-authn/client/main.go
func main() {
client := &http.Client{}
req, _ := http.NewRequest("POST", "http://server.com:8080/", nil)
// 發送默認請求
response, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
// 解析響應頭
authHeader := response.Header.Get("WWW-Authenticate")
loginReq, _ := http.NewRequest("POST", "http://server.com:8080/login", nil)
username := "admin"
password := "123456"
// 判斷認證類型
if !strings.Contains(authHeader, "Basic") {
// 不支持的認證類型
fmt.Println("Unsupported authentication type:", authHeader)
return
}
// 使用Basic Auth, 添加Basic Auth頭
loginReq.SetBasicAuth(username, password)
response, err = client.Do(loginReq)
fmt.Println(response.StatusCode)
// 從響應包體中獲取服務端分配的jwt token
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
fmt.Println(err)
return
}
token := string(body)
fmt.Println("token=", token)
// 基於token訪問服務端其他功能
apiReq, _ := http.NewRequest("POST", "http://server.com:8080/calc", nil)
apiReq.Header.Set("Authorization", "Bearer "+token)
response, err = client.Do(apiReq)
fmt.Println(response.StatusCode)
defer response.Body.Close()
body, err = io.ReadAll(response.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
客戶端的操作流程與示意圖一樣,先用 basic auth 登錄 server,通過驗證後,拿到服務端生成的 token。後續到該服務端的所有請求只需在 Header 中帶上 token 即可。
服務端的代碼如下:
// authn-examples/jwt-authn/server/main.go
func main() {
// 創建一個基本的HTTP服務器
mux := http.NewServeMux()
username := "admin"
password := "123456"
key := "iamtonybai"
// 針對/的handler
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
// 返回401 Unauthorized響應
w.Header().Set("WWW-Authenticate", "Basic realm=\"server.com\"")
w.WriteHeader(http.StatusUnauthorized)
})
// login handler
mux.HandleFunc("/login", func(w http.ResponseWriter, req *http.Request) {
// 從請求頭中獲取Basic Auth認證信息
user, pass, ok := req.BasicAuth()
if !ok {
// 認證失敗
w.WriteHeader(http.StatusUnauthorized)
return
}
// 驗證用戶名密碼
if user == username && pass == password {
// 認證成功,生成token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"username": username,
"iat": jwt.NewNumericDate(time.Now().Add(time.Hour * 24)),
})
signedToken, _ := token.SignedString([]byte(key))
w.Write([]byte(signedToken))
} else {
// 認證失敗
http.Error(w, "Invalid username or password", http.StatusUnauthorized)
}
})
// calc handler
mux.HandleFunc("/calc", func(w http.ResponseWriter, req *http.Request) {
// 讀取並校驗jwt token
token := req.Header.Get("Authorization")[len("Bearer "):]
fmt.Println(token)
if _, err := verifyToken(token, key); err != nil {
// 認證失敗
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
w.Write([]byte("invoke calc ok"))
})
// 監聽8080端口
err := http.ListenAndServe(":8080", mux)
if err != nil {
log.Fatal(err)
}
}
我們看到,除了在 login handler 中使用 basic auth 做用戶密碼驗證外,其他功能 handler(如 calc) 中都使用 token 進行身份驗證。
與傳統會話式 (session) 認證相比,JWT 是無狀態的,更適用於分佈式微服務架構。與 Basic auth 和 digest 相比,jwt 在使用體驗上又領先一籌。憑藉其無需在服務端保存會話狀態、天生適合分佈式架構、令牌內容可以自定義擴展等優勢,現階段,jwt 已廣泛應用於以下場合:
-
前後端分離的 Web 應用和 API 認證
-
跨域單點登錄 (SSO)
-
微服務架構下服務間認證
-
無狀態和移動應用認證
不過 JWT 認證方式也有不足,比如:客戶端要承擔令牌存儲成本、如果令牌泄露未及時失效可能被濫用等。
講到這裏,從基本的用戶名密碼認證,到加上密碼散列的 Digest 認證,再到應用會話管理的 Session 認證,以及基於令牌的 JWT 認證,我們見證了認證機制的不斷進步和發展。
這些方法主要依賴賬號密碼這單一要素,提供了不同程度的安全性。但是隨着互聯網的快速發展,開發人員也在考慮改善用戶名密碼這種方式的使用體驗,一些一次性密碼認證方式便走入了我們的生活。接下來我們就來簡單說一下一次性密碼驗證。
- 基於一次性密碼驗證
一次性密碼(One Time Password, OTP)是一種只能使用一次的密碼,它在使用後立即失效。OTP 生成密碼的算法基於時間,在很短的時間內 (一般分鐘內或更短時間內) 只能使用一次;每次驗證都需要生成和輸入新的密碼,不能重複使用。
一次性密碼的優勢主要有以下幾點:
-
安全性高:一次性密碼只能使用一次,因此即使攻擊者獲得了密碼,也無法重複使用。
-
易用性強:一次性密碼通常是數字或字母組成的短語,易於記憶和輸入。
-
成本低:一次性密碼的生成和驗證成本相對較低。
信息論已經從理論上證明了:一次性密碼本是無條件安全的,在理論上是無法破譯的。不過現實中,還沒有一種理想的一次性密碼,大多數一次性密碼還處於身份認證的輔助地位,多作爲第二要素。
短信驗證碼就是一種我們生活中常見的一次性密碼,它是利用移動運營商的短信通道傳輸的一次性密碼。短信驗證碼通常由 6 位數字組成,有效期爲幾分鐘,並且只能使用一次,通過短信發送給用戶,非常方便用戶使用,用戶無需有記住密碼的煩惱。
短信驗證碼的工作流程如下:
-
客戶端發起認證請求,如登錄或註冊;
-
服務器生成 6 位隨機數字作爲驗證碼,通過文本短信發送到用戶註冊的手機號;
-
用戶接收短信並輸入驗證碼進行驗證;
-
服務器通過時間戳驗證此驗證碼是否有效 (一般在 5 分鐘內)。
-
驗證碼只能使用一次,服務器會將此條記錄標記爲使用。
短信驗證碼的優勢是方便快捷。目前國內大多數主流 Web 應用都支持手機驗證碼登錄。短信驗證碼通常用於以下場景:
-
用戶註冊
-
用戶登錄
-
支付或交易
-
輔助密碼找回等
不過手機驗證碼這種一次性密碼的安全性相對較低,因爲短信可以被截獲,攻擊者可以通過截獲短信來獲取驗證碼。
除短信驗證碼外,還有其他常見的 OTP 實現形式:
-
手機應用軟件 OTP:使用專門的手機 APP 軟件生成 OTP 碼,如 Google Authenticator、Microsoft Authenticator 等。
-
電子郵件 OTP:類似短信驗證碼,但通過郵件發送 6-8 位數字驗證碼到用戶註冊的郵箱。
-
語音驗證碼 OTP:服務端調用第三方語音平臺,使用文本到語音功能給用戶自動撥打認證電話,提示驗證碼。
總體來說,OTP 越來越多地被用到用戶身份認證上來,隨着以後技術的進步,其應用的廣度和深度會進一步擴大,安全性也會得到進一步提升。基於傳統密碼的認證方式早晚會被扔到歷史的舊物箱中。一些大廠,如 Google 都在研究替代傳統密碼的技術,比如 Passkey[13] 等,一些 Web 標準組織也在做無密碼認證的規範,比如 WebAuthn[14] 等。
- 小結
就寫到這裏吧,篇幅有些長了,關於 OAuth、OpenID 等身份認證技術就不在這裏寫了,後續找機會單獨梳理。
本文我們介紹了多種 Web 應用的身份認證技術方案,各種認證技術會依據對安全性、使用性和擴展性的不同需求而存在和發展。瞭解每種技術的原理和優劣勢,可幫助我們更好地選擇適合的方案。
首次梳理這麼多 Web 應用身份認證的資料,可能有些描述並不完全正確,歡迎指正。在撰寫本文時,大語言模型幫助編寫部分文字素材和代碼。
本文示例所涉及的 Go 源碼可以在這裏 [15] 下載。
- 參考資料
-
《API 安全實戰》[16] - https://book.douban.com/subject/36039150/
-
《API 安全技術與實戰》[17] - https://book.douban.com/subject/35429043/
-
《深入淺出密碼學》[18] - https://book.douban.com/subject/36179106/
-
Web Authentication Methods Compared[19] - https://testdriven.io/blog/web-authentication-methods/
-
認證:系統如何正確分辨操作用戶的真實身份?[20] - https://time.geekbang.org/column/article/329954
-
如何實現零信任網絡下安全的服務訪問?[21] - https://time.geekbang.org/column/article/345593
-
憑證:系統如何保證與用戶之間的承諾是準確完整且不可抵賴的?[22] - https://time.geekbang.org/column/article/333272
-
谷歌正推出 Passkey,密碼將成歷史 [23] - https://blog.google/technology/safety-security/the-beginning-of-the-end-of-the-password/
-
What is authentication?[24] - https://www.microsoft.com/zh-cn/security/business/security-101/what-is-authentication
-
Authentication(wikipedia)[25] - https://en.wikipedia.org/wiki/Authentication.html
-
RFC 7617: The 'Basic' HTTP Authentication Scheme[26] - https://datatracker.ietf.org/doc/html/rfc7617
-
RFC 7616: HTTP Digest Access Authentication[27] - https://datatracker.ietf.org/doc/html/rfc7616
-
RFC 7519: JSON Web Token(JWT)[28] - https://datatracker.ietf.org/doc/html/rfc7519
-
Introduction to JSON Web Tokens[29] - https://jwt.io/introduction
Gopher Daily(Gopher 每日新聞) - https://gopherdaily.tonybai.com
我的聯繫方式:
-
微博 (暫不可用):https://weibo.com/bigwhite20xx
-
微博 2:https://weibo.com/u/6484441286
-
博客:tonybai.com
-
github: https://github.com/bigwhite
-
Gopher Daily 歸檔 - https://github.com/bigwhite/gopherdaily
參考資料
[1]
2023 年 Q1 Go 官方用戶調查報告: https://go.dev/blog/survey2023-q1-results
[2]
雙因素認證 (2FA): https://wiki2.org/en/Multi-factor_authentication
[3]
XSS: https://owasp.org/www-community/attacks/xss/
[4]
CSRF: https://owasp.org/www-community/attacks/csrf
[5]
PCI DSS(Payment Card Industry Data Security Standard): https://www.pcisecuritystandards.org/
[6]
OWASP: https://owasp.org
[7]
《Go 語言精進之路 vol2》: https://item.jd.com/13694000.html
[8]
基於 Go 標準庫的 TLS 的雙向認證: https://tonybai.com/2015/04/30/go-and-https
[9]
《Go TLS 服務端綁定證書的幾種方式》: https://tonybai.com/2023/10/13/multiple-ways-to-bind-certificates-on-go-tls-server-side
[10]
JWT(JSON Web Token): https://jwt.io
[11]
jwt.io: https://jwt.io/
[12]
golang-jwt/jwt: https://github.com/golang-jwt/jwt
[13]
Passkey: https://blog.google/technology/safety-security/the-beginning-of-the-end-of-the-password/
[14]
WebAuthn: https://webauthn.guide
[15]
這裏: https://github.com/bigwhite/experiments/tree/master/authn-examples
[16]
《API 安全實戰》: https://book.douban.com/subject/36039150/
[17]
《API 安全技術與實戰》: https://book.douban.com/subject/35429043/
[18]
《深入淺出密碼學》: https://book.douban.com/subject/36179106/
[19]
Web Authentication Methods Compared: https://testdriven.io/blog/web-authentication-methods
[20]
認證:系統如何正確分辨操作用戶的真實身份?: https://time.geekbang.org/column/article/329954
[21]
如何實現零信任網絡下安全的服務訪問?: https://time.geekbang.org/column/article/345593
[22]
憑證:系統如何保證與用戶之間的承諾是準確完整且不可抵賴的?: https://time.geekbang.org/column/article/333272
[23]
谷歌正推出 Passkey,密碼將成歷史: https://blog.google/technology/safety-security/the-beginning-of-the-end-of-the-password/
[24]
What is authentication?: https://www.microsoft.com/zh-cn/security/business/security-101/what-is-authentication
[25]
Authentication(wikipedia): https://en.wikipedia.org/wiki/Authentication.html
[26]
RFC 7617: The 'Basic' HTTP Authentication Scheme: https://datatracker.ietf.org/doc/html/rfc7617
[27]
RFC 7616: HTTP Digest Access Authentication: https://datatracker.ietf.org/doc/html/rfc7616
[28]
RFC 7519: JSON Web Token(JWT): https://datatracker.ietf.org/doc/html/rfc7519
[29]
Introduction to JSON Web Tokens: https://jwt.io/introduction
[30]
“Gopher 部落” 知識星球: https://public.zsxq.com/groups/51284458844544
[31]
鏈接地址: https://m.do.co/c/bff6eed92687
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/oCzTVIx3OEdSY49OwnwW-A