AVt天堂网 手机版,亚洲va久久久噜噜噜久久4399,天天综合亚洲色在线精品,亚洲一级Av无码毛片久久精品

當(dāng)前位置:首頁 > 科技  > 軟件

通過實(shí)例理解Go Web身份認(rèn)證的幾種方式

來源: 責(zé)編: 時間:2023-10-26 17:11:44 240觀看
導(dǎo)讀在2023年Q1 Go官方用戶調(diào)查報告[1]中,API/RPC services、Websites/web services都位于使用Go開發(fā)的應(yīng)用類別的頭部(如下圖):我個人使用Go開發(fā)已很多年,但一直從事底層基礎(chǔ)設(shè)施、分布式中間件等方向,Web應(yīng)用開發(fā)領(lǐng)域涉及較

在2023年Q1 Go官方用戶調(diào)查報告[1]中,API/RPC services、Websites/web services都位于使用Go開發(fā)的應(yīng)用類別的頭部(如下圖):cXw28資訊網(wǎng)——每日最新資訊28at.com

圖片cXw28資訊網(wǎng)——每日最新資訊28at.com

我個人使用Go開發(fā)已很多年,但一直從事底層基礎(chǔ)設(shè)施、分布式中間件等方向,Web應(yīng)用開發(fā)領(lǐng)域涉及較少,像Web應(yīng)用領(lǐng)域常見的CRUD更是少有涉獵,不能不說是一種“遺憾”^_^。未來一段時間,團(tuán)隊(duì)會接觸到Web應(yīng)用的開發(fā),我打算對Go Web應(yīng)用開發(fā)的重點(diǎn)環(huán)節(jié)做一個快速系統(tǒng)的梳理。cXw28資訊網(wǎng)——每日最新資訊28at.com

而身份認(rèn)證(Authentication,簡稱AuthN)是Web應(yīng)用開發(fā)中一個關(guān)鍵的環(huán)節(jié),也是首個環(huán)節(jié),它負(fù)責(zé)驗(yàn)證用戶身份,讓用戶可以以認(rèn)證過的身份訪問系統(tǒng)中的資源和信息。cXw28資訊網(wǎng)——每日最新資訊28at.com

Go語言作為一門優(yōu)秀的Web開發(fā)語言,提供了豐富的機(jī)制來實(shí)現(xiàn)Web應(yīng)用的用戶身份認(rèn)證。在這篇文章中,我就通過Go示例和大家一起探討一下當(dāng)前Web應(yīng)用開發(fā)中幾種常見的主流身份認(rèn)證方式,幫助自己和各位讀者邁出Web應(yīng)用開發(fā)修煉之路的第一步。cXw28資訊網(wǎng)——每日最新資訊28at.com

1.身份認(rèn)證簡介

1.1 身份認(rèn)證解決的問題

身份認(rèn)證不局限于Web應(yīng)用,各種系統(tǒng)都會有身份認(rèn)證,但本文我們聚焦Web應(yīng)用領(lǐng)域的身份認(rèn)證技術(shù)。cXw28資訊網(wǎng)——每日最新資訊28at.com

幾乎所有Web應(yīng)用的安全性都是從身份認(rèn)證開始的,身份認(rèn)證是驗(yàn)證用戶身份真實(shí)性的過程,是我們首先要部署的策略。位于下游的安全控制,如授權(quán)(Authorization, AuthZ)、審計(jì)日志(Audit log)等,幾乎都需要用戶的身份。cXw28資訊網(wǎng)——每日最新資訊28at.com

身份認(rèn)證的英文是Authentication,簡寫為AuthN,大家不要將之與授權(quán)Authorization(AuthZ)混淆(在后續(xù)系列文章中會繼續(xù)探討AuthZ相關(guān)的內(nèi)容),他們所要解決的問題相似,但有不同,也有先后。通常先AuthN,再AuthZ。我們可以用下面的比喻來形象地解釋二者的聯(lián)系與差異:cXw28資訊網(wǎng)——每日最新資訊28at.com

AuthN就像是進(jìn)入公司大樓的安檢,負(fù)責(zé)檢查員工的身份是否合法,是否具有進(jìn)入公司的資格,它解決的是驗(yàn)證員工身份的問題。cXw28資訊網(wǎng)——每日最新資訊28at.com

AuthZ更像是公司內(nèi)部的權(quán)限管理,某個員工進(jìn)入了公司后(AuthN后)想訪問一些重要資料,這時還需要確認(rèn)該員工是否有相應(yīng)的訪問權(quán)限。它解決的是授權(quán)訪問控制的問題。cXw28資訊網(wǎng)——每日最新資訊28at.com

簡單來說,AuthN是驗(yàn)證你是誰,authZ是驗(yàn)證你有哪些權(quán)限。AuthN解決認(rèn)證問題,AuthZ解決授權(quán)問題,這兩個都重要,AuthN解決外部的安全問題,authZ解決內(nèi)部的安全與合規(guī)問題。cXw28資訊網(wǎng)——每日最新資訊28at.com

1.2 身份認(rèn)證的三要素

身份認(rèn)證需要被認(rèn)證方提供一些身份信息輸入,這些代表身份信息的輸入被稱為身份認(rèn)證要素(authentication factor)。這些要素有很多,大致可分為三類:cXw28資訊網(wǎng)——每日最新資訊28at.com

  • 你知道的東西(What you know)

即基于被認(rèn)證方知道的特定信息來驗(yàn)證身份,最常見的如密碼等。cXw28資訊網(wǎng)——每日最新資訊28at.com

  • 你擁有的東西(What you have)

基于被認(rèn)證方所擁有的特定物件來驗(yàn)證身份,最常見的利用數(shù)字證書、令牌卡等。N年前,在移動端應(yīng)用還沒有發(fā)展起來時,一些人在銀行辦理電子銀行業(yè)務(wù)時會拿到一個U盾(又稱為USBKey),其中存放著用于用戶身份識別的數(shù)字證書,這個U盾就屬于此類要素。cXw28資訊網(wǎng)——每日最新資訊28at.com

上面比喻中進(jìn)入大樓時使用的員工卡也屬于這類要素。cXw28資訊網(wǎng)——每日最新資訊28at.com

  • 你本身就具有的(What you are)

即基于被認(rèn)證方所擁有的生物特征要素(biometric factor)來驗(yàn)證身份,最常見的人臉識別、指紋/聲紋/虹膜識別和解鎖等。理論上來說,具備個人生物特征的身份認(rèn)證標(biāo)志具有不可仿冒性、唯一性。cXw28資訊網(wǎng)——每日最新資訊28at.com

如果上面比喻中的大樓已經(jīng)開啟了人臉識別功能,那么基于人臉識別的認(rèn)證就屬于這類要素的認(rèn)證。cXw28資訊網(wǎng)——每日最新資訊28at.com

通常我們會基于單個要素設(shè)計(jì)身份認(rèn)證方案,一旦使用兩個或兩個以上不同類的要素,就可以被稱為**雙因素認(rèn)證(2FA)[2]或多因素認(rèn)證(MFA)**了。不過,2FA和MFA都比較復(fù)雜,不再本篇文章討論范圍之內(nèi)。cXw28資訊網(wǎng)——每日最新資訊28at.com

基于上述要素,我們就可以設(shè)計(jì)和實(shí)現(xiàn)各種適合不同類別Web應(yīng)用或API服務(wù)的身份認(rèn)證方法了。Web應(yīng)用和API服務(wù)都需要身份認(rèn)證,它們有什么差異呢?這些差異是否會對身份認(rèn)證方案產(chǎn)生影響呢?我們接下來看一下。cXw28資訊網(wǎng)——每日最新資訊28at.com

1.3 Web應(yīng)用身份認(rèn)證 vs. API服務(wù)身份認(rèn)證

Web應(yīng)用和API服務(wù)主要有以下幾點(diǎn)區(qū)別:cXw28資訊網(wǎng)——每日最新資訊28at.com

  • 交互方式不同

Web應(yīng)用是瀏覽器與服務(wù)器之間的交互,用戶通過瀏覽器訪問Web應(yīng)用。而API服務(wù)是程序/應(yīng)用與服務(wù)器之間的交互,通過API請求獲取數(shù)據(jù)或執(zhí)行操作。cXw28資訊網(wǎng)——每日最新資訊28at.com

  • 返回?cái)?shù)據(jù)格式不同

Web應(yīng)用通常會返回html/js/css等瀏覽器可解析執(zhí)行的代碼,而API服務(wù)通常返回結(jié)構(gòu)化數(shù)據(jù),常見的如JSON或XML等。cXw28資訊網(wǎng)——每日最新資訊28at.com

  • 使用場景不同

Web應(yīng)用主要面向人類用戶的使用,用戶通過瀏覽器進(jìn)行操作。而API服務(wù)主要被其他程序調(diào)用,為程序之間提供接口與數(shù)據(jù)支撐。cXw28資訊網(wǎng)——每日最新資訊28at.com

  • 狀態(tài)管理不同

Web應(yīng)用在服務(wù)端保存會話狀態(tài),瀏覽器通過cookie等保存用戶狀態(tài)。而API服務(wù)通常是無狀態(tài)的,每次請求都需要攜帶用于身份認(rèn)證的信息,比如訪問令牌或API Key等。cXw28資訊網(wǎng)——每日最新資訊28at.com

  • 安全方面的關(guān)注點(diǎn)不同

Web應(yīng)用更關(guān)注XSS[3]、CSRF[4]等輸入驗(yàn)證安全,而API服務(wù)更關(guān)注身份認(rèn)證(authN)、授權(quán)(authZ)、準(zhǔn)入(admission)、限流等訪問控制安全。cXw28資訊網(wǎng)——每日最新資訊28at.com

總之,Web應(yīng)用注重界面的展示和用戶交互;而API服務(wù)注重?cái)?shù)據(jù)和服務(wù)的提供,它們有不同的使用場景、交互方式和安全關(guān)注點(diǎn)。cXw28資訊網(wǎng)——每日最新資訊28at.com

Web應(yīng)用和API服務(wù)的這些差異也導(dǎo)致了Web應(yīng)用和API服務(wù)適合使用的身份認(rèn)證方案上會有所不同。但前后端分離架構(gòu)的出現(xiàn)和普及,讓前后端責(zé)任分離:前端專注于視圖和交互,后端專注數(shù)據(jù)和業(yè)務(wù),并且前后端通過標(biāo)準(zhǔn)化的API接口進(jìn)行數(shù)據(jù)交互。這可以讓后端提供統(tǒng)一的認(rèn)證接口,不同的前端可以共享。像基于Token這樣的無狀態(tài)易理解的身份驗(yàn)證機(jī)制逐漸成為主流。也就是說,架構(gòu)模式的變化,使得Web應(yīng)用和API服務(wù)在身份驗(yàn)證(authN)方案上出現(xiàn)了一些融合的現(xiàn)象,因此在身份認(rèn)證方法上,Web應(yīng)用和API服務(wù)也存在一些交集。cXw28資訊網(wǎng)——每日最新資訊28at.com

下面維韋恩圖列出了三類身份認(rèn)證方法,包括僅適用于Web應(yīng)用的、僅適用于API服務(wù)的以及兩者都適用的:cXw28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片cXw28資訊網(wǎng)——每日最新資訊28at.com

本文聚焦Web應(yīng)用的身份認(rèn)證方式,接下來會重點(diǎn)說說上圖中綠色背景色的幾種身份認(rèn)證方式。cXw28資訊網(wǎng)——每日最新資訊28at.com

2. 安全信道是身份認(rèn)證的前提和基礎(chǔ)

在對具體的Web身份認(rèn)證方式進(jìn)行說明之前,我們先來了解一下身份認(rèn)證的前提和基礎(chǔ) - 安全信道。cXw28資訊網(wǎng)——每日最新資訊28at.com

在Web應(yīng)用身份認(rèn)證的過程中,無論采用何種認(rèn)證方式,用戶的身份要素信息(用戶名/密碼、token、生物特征信息)都要傳遞給服務(wù)器,這時候如果傳遞此類信息的通信信道不安全,這些重要的認(rèn)證要素信息就很容易被中間人截取、破解、篡改并被冒充,從而獲得Web應(yīng)用的使用權(quán)。從服務(wù)端角度來看,如果沒有安全信道,服務(wù)器身份也容易被偽裝,導(dǎo)致用戶連接到“冒牌服務(wù)器”并導(dǎo)致嚴(yán)重后果。因此,沒有建立在安全信道上的身份認(rèn)證是不安全,不具備實(shí)際應(yīng)用價值的,甚至是完全沒有意義的。cXw28資訊網(wǎng)——每日最新資訊28at.com

此外,安全信道不僅對登錄階段的身份認(rèn)證環(huán)節(jié)有重要意義,在用戶已登錄并訪問Web應(yīng)用其他功能頁面時,安全通道也可以對數(shù)據(jù)的傳輸以及類似訪問令牌或Cookie數(shù)據(jù)的傳輸起到加密和保護(hù)作用。cXw28資訊網(wǎng)——每日最新資訊28at.com

在Web應(yīng)用領(lǐng)域,最常用的安全信道建立方式是基于HTTPS(HTTP over TLS)或直接建立在TLS之上的自定義通信,TLS利用證書對通信進(jìn)行加密、驗(yàn)證服務(wù)器身份(甚至是客戶端身份的驗(yàn)證),保障信息的機(jī)密性和完整性。各大安全規(guī)范和標(biāo)準(zhǔn)如PCI DSS(Payment Card Industry Data Security Standard)[5]、OWASP[6]也強(qiáng)制要求使用HTTPS保障認(rèn)證安全。cXw28資訊網(wǎng)——每日最新資訊28at.com

基于安全信道,我們還可以實(shí)施第一波的身份認(rèn)證,這就是我們通常所說的基于HTTPS(或TLS)的雙向身份認(rèn)證。cXw28資訊網(wǎng)——每日最新資訊28at.com

注:在我的《Go語言精進(jìn)之路vol2》[7]一書中,對TLS的機(jī)制以及基于Go標(biāo)準(zhǔn)庫的TLS的雙向認(rèn)證[8]有系統(tǒng)全面的說明,歡迎各位童鞋閱讀反饋。cXw28資訊網(wǎng)——每日最新資訊28at.com

這種認(rèn)證方式采用的是身份認(rèn)證要素中的第二類要素:What you have。客戶端帶著歸屬于自己的專有證書去服務(wù)端做身份驗(yàn)證。如果client證書通過服務(wù)端的驗(yàn)簽后,便可允許client進(jìn)入“大樓”。cXw28資訊網(wǎng)——每日最新資訊28at.com

下面是一個基于TLS證書做身份認(rèn)證的客戶端與服務(wù)端交互的示意圖:cXw28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片cXw28資訊網(wǎng)——每日最新資訊28at.com

我們先看看對應(yīng)上述示意圖中的客戶端的代碼:cXw28資訊網(wǎng)——每日最新資訊28at.com

// authn-examples/tls-authn/client/main.gofunc 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. 發(fā)送請求 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. 打印響應(yīng)信息 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作為后續(xù)與服務(wù)端通信的身份憑證,加載inter-cert.pem用于校驗(yàn)服務(wù)端在tls握手過程發(fā)來的服務(wù)端證書(server-cert.pem),避免連接到“冒牌站點(diǎn)”。通過驗(yàn)證后,客戶端向服務(wù)端發(fā)起Get請求并輸出響應(yīng)的內(nèi)容。cXw28資訊網(wǎng)——每日最新資訊28at.com

下面是服務(wù)端的代碼:cXw28資訊網(wǎng)——每日最新資訊28at.com

// authn-examples/tls-authn/server/main.gofunc 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設(shè)置 tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {  // 獲取客戶端證書  cert := verifiedChains[0][0]  // 提取CN作為客戶端標(biāo)識  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. 創(chuàng)建服務(wù)器 srv := &http.Server{  Addr:      ":8443",  TLSConfig: tlsConfig, } // 4. 啟動服務(wù)器 err = srv.ListenAndServeTLS("", "") if err != nil {  log.Fatal(err) }}

注:在你的實(shí)驗(yàn)環(huán)境中,需要在/etc/hosts文件中添加server.com的映射ip為127.0.0.1。cXw28資訊網(wǎng)——每日最新資訊28at.com

服務(wù)端代碼也不復(fù)雜,比較“套路化”:加載服務(wù)端證書和中間CA證書(用于驗(yàn)簽client端的證書),這里將tls.Config.ClientAuth設(shè)置為RequireAndVerifyClientCert,這會觸發(fā)服務(wù)端對客戶端證書的驗(yàn)簽,同時在tlsConfig.VerifyPeerCertificate不為nil的情況下,觸發(fā)對tlsConfig.VerifyPeerCertificate的函數(shù)的調(diào)用,在示例代碼中,我們?yōu)閠lsConfig.VerifyPeerCertificate賦值了一個匿名函數(shù)實(shí)現(xiàn),在這個函數(shù)中,我們提取了客戶端證書中的客戶端標(biāo)識CN,并查看其是否在可信任的客戶端ID表中。cXw28資訊網(wǎng)——每日最新資訊28at.com

在這個示例中,這個tlsConfig.VerifyPeerCertificate執(zhí)行的驗(yàn)證有些多余,但我們在實(shí)際代碼中可以使用tlsConfig.VerifyPeerCertificate來設(shè)置黑名單,攔截那些尚未過期、但可以驗(yàn)簽通過的客戶端,實(shí)現(xiàn)一種客戶端證書過期前的作廢機(jī)制。cXw28資訊網(wǎng)——每日最新資訊28at.com

此外,上述示例中客戶端、服務(wù)端以及中間CA證書的制作代碼與《Go TLS服務(wù)端綁定證書的幾種方式》[9]一文中的證書制作很類似,大家可以直接參考本文示例代碼中的tls-authn/make-certs下面的代碼,這里就不贅述了。cXw28資訊網(wǎng)——每日最新資訊28at.com

通過這種基于安全信道的身份驗(yàn)證方式,客戶端證書可以強(qiáng)制認(rèn)證用戶,理論上不需要額外再用用戶名密碼。認(rèn)證之后客戶端在這個TLS連接上發(fā)送的所有信息都將綁定其身份。cXw28資訊網(wǎng)——每日最新資訊28at.com

不過通過頒發(fā)客戶端專用證書的方式僅適合一些像網(wǎng)絡(luò)銀行之類的專有業(yè)務(wù),大多數(shù)Web應(yīng)用會與客戶端間建立安全信道,但不會采用客戶端證書來認(rèn)證用戶身份,在這樣的情況下,下面要說的這些身份認(rèn)證方式就可以發(fā)揮作用了。cXw28資訊網(wǎng)——每日最新資訊28at.com

我們先來看一下最傳統(tǒng)的基于密碼的認(rèn)證。cXw28資訊網(wǎng)——每日最新資訊28at.com

3. 基于密碼的認(rèn)證

基于密碼的認(rèn)證屬于基于第一類身份認(rèn)證要素:你知道的東西(What you know)的認(rèn)證方式,這類認(rèn)證也是Web應(yīng)用中最經(jīng)典、最常見的認(rèn)證方式。我們先從基于傳統(tǒng)表單承載用戶名/密碼說起。cXw28資訊網(wǎng)——每日最新資訊28at.com

3.1. 基于用戶名+密碼的認(rèn)證(傳統(tǒng)表單方式)

這是最常見的Web應(yīng)用認(rèn)證方式:用戶通過提交包含用戶名和密碼的表單(Form),服務(wù)端Web應(yīng)用進(jìn)行驗(yàn)證。下面使用這種方式的客戶端與服務(wù)單的交互示意圖:cXw28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片cXw28資訊網(wǎng)——每日最新資訊28at.com

接下來,我們看看對應(yīng)上述示意圖的實(shí)現(xiàn)代碼。我們先建立一個html文件,該文件非常簡單,就是一個可輸入用戶名和密碼的表單,點(diǎn)擊登錄按鈕將表單信息發(fā)送到服務(wù)端:cXw28資訊網(wǎng)——每日最新資訊28at.com

// authn-examples/password/classic/login.html<!DOCTYPE html><html><head>  <title>登錄</title></head><body><form actinotallow="http://server.com:8080/login" method="post">  <label>用戶名:</label>  <input type="text" name="username"/>  <label>密碼:</label>  <input type="password" name="password"/>  <button type="submit">登錄</button></form></body></html>

發(fā)送的HTTP Post請求的包體(Body)中會包含頁面輸入的username和password的值,形式如下:cXw28資訊網(wǎng)——每日最新資訊28at.com

username=admin&password=123456

而我們的服務(wù)端的代碼如下:cXw28資訊網(wǎng)——每日最新資訊28at.com

// authn-examples/password/classic/main.gofunc 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 {    // 驗(yàn)證用戶名密碼    v, ok := credentials[username]    if !ok {        return false    }    if v != password {        return false    }    return true}

服務(wù)端通過Request的FormValue方法獲得username和password的值,并與credentials存儲的合法用戶信息比對(當(dāng)然這只是演示代碼中的臨時手段,生產(chǎn)中不要這么存儲用戶信息),比對成功,返回"Welcome"應(yīng)答;比對失敗,返回401 Unauthorized錯誤。cXw28資訊網(wǎng)——每日最新資訊28at.com

注:包括本示例在內(nèi)的后續(xù)所有示例的客戶端和服務(wù)端都在非安全信道上通信,目的是簡化示例代碼的編寫。大家在生產(chǎn)環(huán)境務(wù)必建立安全信道后再做后續(xù)的身份驗(yàn)證。cXw28資訊網(wǎng)——每日最新資訊28at.com

基于傳統(tǒng)的表單用戶名和密碼可以作為Web應(yīng)用服務(wù)端身份驗(yàn)證的方案,但問題來了:服務(wù)端認(rèn)證成功后,用戶后續(xù)向Web應(yīng)用服務(wù)端發(fā)起的請求是否還要繼續(xù)帶上用戶和密碼信息呢?如果不帶上用戶和密碼信息,服務(wù)端又如何驗(yàn)證這些請求是來自之前已經(jīng)認(rèn)證成功后的用戶;如果后續(xù)每個請求都帶上以Form形式承載的用戶名和密碼,使用起來又非常不方便,還影響后續(xù)請求的正常數(shù)據(jù)的傳輸(對Body數(shù)據(jù)有侵入)。cXw28資訊網(wǎng)——每日最新資訊28at.com

于是便有了Session(會話)機(jī)制,它可以被認(rèn)為是基于經(jīng)典的用戶名密碼(表單承載)認(rèn)證方式的“延續(xù)”,使得密碼認(rèn)證的成果不再局限在缺乏連續(xù)性的單一請求級別上,而是擴(kuò)展到后續(xù)的一段時間內(nèi)或一系列與Web應(yīng)用的互操作過程中,變成了連續(xù)、持久的登錄會話。cXw28資訊網(wǎng)——每日最新資訊28at.com

接下來,我們就來簡單看看基于Session的后續(xù)認(rèn)證方式是如何工作的。cXw28資訊網(wǎng)——每日最新資訊28at.com

3.2 使用Session:有狀態(tài)的認(rèn)證方式

基于Session的認(rèn)證方式是一種有狀態(tài)的方案,服務(wù)端會為每個身份認(rèn)證成功的用戶建立并保存相關(guān)session信息,同時服務(wù)端也會要求客戶端在瀏覽器側(cè)持久化與該Session有關(guān)少量信息,通??蛻舳藭ㄟ^開啟Cookie的方式來保存與用戶Session相關(guān)的信息。cXw28資訊網(wǎng)——每日最新資訊28at.com

服務(wù)端保存Session有多種方式,可以在進(jìn)程內(nèi)存中、文件中、數(shù)據(jù)庫、緩存(Redis)等,不同方式各有優(yōu)缺點(diǎn),比如將Session保存在內(nèi)存中,最大的好處就是實(shí)現(xiàn)簡單且速度快,但由于不能持久化,服務(wù)實(shí)例重啟后就會丟失,此外當(dāng)服務(wù)端有多副本時,session信息無法在多實(shí)例共享;使用關(guān)系數(shù)據(jù)庫來保存session,可以方便持久化,也方便與服務(wù)端多實(shí)例用戶數(shù)據(jù)共享,但數(shù)據(jù)庫交互成本較大;而使用緩存(Redis)存儲session信息是目前比較主流的方式,簡單、安全、快速,還可以很好地適合分布式環(huán)境下session的共享。cXw28資訊網(wǎng)——每日最新資訊28at.com

下面是一個常見的基于cookie實(shí)現(xiàn)的session機(jī)制的客戶端與服務(wù)端的交互示意圖:cXw28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片cXw28資訊網(wǎng)——每日最新資訊28at.com

這里也給出上述示意圖的一個參考實(shí)現(xiàn)示例(代碼僅用作演示,很多值設(shè)置并不規(guī)范和安全,不要用于生產(chǎn))。cXw28資訊網(wǎng)——每日最新資訊28at.com

session機(jī)制的開啟從用戶登錄開始,這個示例里的login.html與上一個示例是一樣的:cXw28資訊網(wǎng)——每日最新資訊28at.com

// authn-examples/password/session/login.html<!DOCTYPE html><html><head>  <title>登錄</title></head><body><form actinotallow="http://server.com:8080/login" method="post">  <label>用戶名:</label>  <input type="text" name="username"/>  <label>密碼:</label>  <input type="password" name="password"/>  <button type="submit">登錄</button>  </form></body></html>

服務(wù)端負(fù)責(zé)的login Handler代碼如下:cXw28資訊網(wǎng)——每日最新資訊28at.com

// authn-examples/password/session/main.govar 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 { // 驗(yàn)證用戶名密碼 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 { // 生成隨機(jī)數(shù) 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)  }  // 設(shè)置session數(shù)據(jù)  random := randomStr()  usernameB64 := base64Encode(username + "-" + random)  session.Values["random"] = random  session.Save(r, w)  // 設(shè)置cookie  cookie := http.Cookie{Name: "server.com-session", Value: usernameB64}  http.SetCookie(w, &cookie)  // 登錄成功,跳轉(zhuǎn)到calc頁面  http.Redirect(w, r, "/calc", http.StatusSeeOther) } else {  http.Error(w, "Invalid username or password", http.StatusUnauthorized) // 401 }}

我們使用了gorilla/sessions這個Go社區(qū)廣泛使用的session庫來實(shí)現(xiàn)服務(wù)端session的相關(guān)操作。以admin用戶登錄為例,當(dāng)用戶名和密碼認(rèn)證成功后,我們在session store中創(chuàng)建一個新的session:server.com_admin。然后生成一個隨機(jī)數(shù),將隨機(jī)數(shù)存儲在該session的名為"random"的key的下面。之后,讓客戶端設(shè)置cookie,name為server.com-session。值為username和random按特定格式組合后的base64編碼值。cXw28資訊網(wǎng)——每日最新資訊28at.com

登錄成功后,瀏覽器會跳到calc頁面,這里我們輸入兩個整數(shù),并點(diǎn)擊"calc"按鈕提交,提交動作會發(fā)送請求到calcAdd Handler中:cXw28資訊網(wǎng)——每日最新資訊28at.com

// authn-examples/password/session/main.gofunc 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. 轉(zhuǎn)換為整型參數(shù) a, err := strconv.Atoi(r.FormValue("a")) if err != nil {  http.Error(w, "參數(shù)錯誤", 400)  return } b, err := strconv.Atoi(r.FormValue("b")) if err != nil {  http.Error(w, "參數(shù)錯誤", 400)  return } // 4. 計(jì)算并返回結(jié)果 result := a + b w.Write([]byte(fmt.Sprintf("%d", result)))}

calcAdd Handler會提取Cookie "server.com-session"中的值,根據(jù)值信息查找服務(wù)端本地是否存儲了對應(yīng)的session,并校驗(yàn)與session中存儲的隨機(jī)碼是否一致。驗(yàn)證通過后,直接返回結(jié)算結(jié)果;否則提醒客戶端重新登錄。cXw28資訊網(wǎng)——每日最新資訊28at.com

前面說過,session是一種有狀態(tài)的輔助身份認(rèn)證機(jī)制,需要客戶端和服務(wù)端的配合完成,一旦客戶端禁用了Cookie機(jī)制,上述的示例實(shí)現(xiàn)就失效了。當(dāng)然有讀者會說,Session可以不基于Cookie來實(shí)現(xiàn),可以用URL重寫、隱藏表單字段、將Session ID放入URL路徑等方式來實(shí)現(xiàn),客戶端也可以用LocalStorage等前端存儲機(jī)制來替代Cookie。但無論哪種實(shí)現(xiàn),這種有狀態(tài)機(jī)制帶來的復(fù)雜性都不低,并且在分布式環(huán)境中需要session共享和同步機(jī)制,影響了scaling。cXw28資訊網(wǎng)——每日最新資訊28at.com

隨著微服務(wù)架構(gòu)的廣泛使用,無需在服務(wù)端存儲額外信息、天然支持后端服務(wù)分布式多實(shí)例的無狀態(tài)的連續(xù)身份認(rèn)證機(jī)制受到了更多的青睞。cXw28資訊網(wǎng)——每日最新資訊28at.com

其實(shí)基于HTTP的無狀態(tài)認(rèn)證機(jī)制早已有之,最常見的莫過于Basic Auth了,接下來,我們就從Basic Auth開始,說幾種無狀態(tài)身份認(rèn)證機(jī)制。cXw28資訊網(wǎng)——每日最新資訊28at.com

3.3 Basic Auth:最早的無狀態(tài)認(rèn)證方式

Basic Auth是HTTP最原始的身份驗(yàn)證方式,在HTTP1.0規(guī)范中就已存在,其原因是HTTP是無狀態(tài)協(xié)議,每次請求都需要進(jìn)行身份驗(yàn)證才能訪問受保護(hù)資源。cXw28資訊網(wǎng)——每日最新資訊28at.com

Basic Auth的原理也十分簡單,客戶端與服務(wù)端的交互如下圖:cXw28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片cXw28資訊網(wǎng)——每日最新資訊28at.com

Basic Auth通過在客戶端的請求報文中添加HTTP Authorization Header的形式向服務(wù)器端發(fā)送認(rèn)證憑據(jù)。HTTP Authorization Header的構(gòu)建通常分兩步。cXw28資訊網(wǎng)——每日最新資訊28at.com

  • 將“username:password”的組合字符串進(jìn)行Base64編碼,編碼值記作b64Token。
  • 將Authorization: Basic b64Token作為HTTP header的一個字段發(fā)送給服務(wù)器端。

服務(wù)端收到請請求后提取出Authorization字段并做Base64解碼,得到username和password,然后與存儲的信息作比對進(jìn)行客戶端身份認(rèn)證。cXw28資訊網(wǎng)——每日最新資訊28at.com

我們來看一個與上圖對應(yīng)的示例的代碼,先看客戶端:cXw28資訊網(wǎng)——每日最新資訊28at.com

// authn-examples/password/basic/client/main.gofunc main() { client := &http.Client{} req, _ := http.NewRequest("POST", "http://server.com:8080/", nil) // 發(fā)送默認(rèn)請求 response, err := client.Do(req) if err != nil {  fmt.Println(err)  return } // 解析響應(yīng)頭 authHeader := response.Header.Get("WWW-Authenticate") loginReq, _ := http.NewRequest("POST", "http://server.com:8080/login", nil) username := "admin" password := "123456" // 判斷認(rèn)證類型 if !strings.Contains(authHeader, "Basic") {  // 不支持的認(rèn)證類型  fmt.Println("Unsupported authentication type:", authHeader)  return } // 使用Basic Auth, 添加Basic Auth頭 loginReq.SetBasicAuth(username, password) response, err = client.Do(loginReq) // 打印響應(yīng)狀態(tài) fmt.Println(response.StatusCode) // 打印響應(yīng)包體 defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil {  fmt.Println(err)  return } fmt.Println(string(body))}

客戶端的代碼比較簡單,并且流程與圖中的交互流程是完全一樣的。而服務(wù)端就是一個簡單的http server,對來自客戶端的帶有basic auth的請求進(jìn)行身份認(rèn)證:cXw28資訊網(wǎng)——每日最新資訊28at.com

// authn-examples/password/basic/server/main.gofunc main() { // 創(chuàng)建一個基本的HTTP服務(wù)器 mux := http.NewServeMux() username := "admin" password := "123456" // 針對/的handler mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {  // 返回401 Unauthorized響應(yīng)  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認(rèn)證信息  user, pass, ok := req.BasicAuth()  if !ok {   // 認(rèn)證失敗   w.WriteHeader(http.StatusUnauthorized)   return  }  // 驗(yàn)證用戶名密碼  if user == username && pass == password {   // 認(rèn)證成功   w.WriteHeader(http.StatusOK)   w.Write([]byte("Welcome to the protected resource!"))  } else {   // 認(rèn)證失敗   http.Error(w, "Invalid username or password", http.StatusUnauthorized)  } }) // 監(jiān)聽8080端口 err := http.ListenAndServe(":8080", mux) if err != nil {  log.Fatal(err) }}

采用Basic Auth身份認(rèn)證方案的客戶端在每個請求中都要在Header中加上Basic Auth形式的身份信息,但服務(wù)端無需像Session那樣存儲任何額外的信息。cXw28資訊網(wǎng)——每日最新資訊28at.com

不過很顯然,Basic Auth這種采用明文傳輸身份信息的方式在安全性方面飽受詬病,為了避免在Header傳輸明文的安全問題,RFC 2617(以及后續(xù)更新版RFC 7616)定義了HTTP Digest身份認(rèn)證方式。Digest訪問認(rèn)證不再明文傳輸密碼,而是傳遞用hash算法處理后密碼摘要,相對Basic Auth驗(yàn)證安全性更高。接下來,我們就來看看HTTP Digest認(rèn)證方式。cXw28資訊網(wǎng)——每日最新資訊28at.com

3.4 基于HTTP Digest認(rèn)證

Digest是一種HTTP摘要認(rèn)證,你可以把它看作是Basic Auth的改良版本,針對Base64明文發(fā)送的風(fēng)險,Digest認(rèn)證把用戶名和密碼加鹽(一個被稱為Nonce的隨機(jī)值作為鹽值)后,再通過MD5/SHA等哈希算法取摘要放到請求的Header中發(fā)送出去。Digest的認(rèn)證過程如下圖:cXw28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片cXw28資訊網(wǎng)——每日最新資訊28at.com

相對于Basic Auth,Digest Auth的一些值的生成過程還是略復(fù)雜的,這里給出一個示例性質(zhì)的代碼示例,可能不完全符合Digest規(guī)范,大家通過示例理解Digest的認(rèn)證過程就可以了。cXw28資訊網(wǎng)——每日最新資訊28at.com

注:如要使用符合RFC 7616的Digest規(guī)范(或老版RFC 2617規(guī)范),可以找一些第三方包,比如https://github.com/abbot/go-http-auth(只滿足RFC 2617)。cXw28資訊網(wǎng)——每日最新資訊28at.com

// authn-examples/password/digest/client/main.gofunc main() { client := &http.Client{} req, _ := http.NewRequest("POST", "http://server.com:8080/", nil) // 發(fā)送默認(rèn)請求 response, err := client.Do(req) if err != nil {  fmt.Println(err)  return } // 解析響應(yīng)頭 authHeader := response.Header.Get("WWW-Authenticate") loginReq, _ := http.NewRequest("POST", "http://server.com:8080/login", nil) username := "admin" password := "123456" // 判斷認(rèn)證類型 if !strings.Contains(authHeader, "Digest") {  // 不支持的認(rèn)證類型  fmt.Println("Unsupported authentication type:", authHeader)  return } // 使用Digest Auth //隨機(jī)數(shù) cnonce := GenNonce() //生成HA1 ha1 := GetHA1(username, password, cnonce) //構(gòu)建Authorization頭 auth := "Digest username=/"" + username + "/", nnotallow=/"" + cnonce + "/", algorithm=MD5, respnotallow=/"" + GetResponse(ha1, cnonce) + "/"" loginReq.Header.Set("Authorization", auth) response, err = client.Do(loginReq) // 打印響應(yīng)狀態(tài) fmt.Println(response.StatusCode) // 打印響應(yīng)包體 defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil {  fmt.Println(err)  return } fmt.Println(string(body))}// 生成隨機(jī)數(shù)func GenNonce() string { h := md5.New() io.WriteString(h, fmt.Sprint(rand.Int())) return hex.EncodeToString(h.Sum(nil))}// 根據(jù)用戶名密碼和隨機(jī)數(shù)生成HA1func GetHA1(username, password, cnonce string) string { h := md5.New() io.WriteString(h, username+":"+cnonce+":"+password) return hex.EncodeToString(h.Sum(nil))}// 根據(jù)HA1,隨機(jī)數(shù)生成responsefunc 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和隨機(jī)數(shù)生成摘要以及一個response碼,并通過請求的頭Authorization字段發(fā)給服務(wù)端。cXw28資訊網(wǎng)——每日最新資訊28at.com

服務(wù)端解析Authorization字段中的各個值,然后采用同樣的算法算出一個新response,與請求中的response比對,如果一致,則認(rèn)為認(rèn)證成功:cXw28資訊網(wǎng)——每日最新資訊28at.com

// authn-examples/password/digest/server/main.gofunc main() { mux := http.NewServeMux() password := "123456" // 針對/的handler mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {  // 返回401 Unauthorized響應(yīng)  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)  //驗(yàn)證參數(shù)  if Verify(req, password) {   fmt.Fprintln(w, "Verify Success!")  } else {   w.WriteHeader(401)   fmt.Fprintln(w, "Verify Failed!")  } }) // 監(jiān)聽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 } //根據(jù)用戶名密碼及隨機(jī)數(shù)生成HA1 ha1 := GetHA1(username, password, cnonce) //自己生成response與請求中response對比 return response == GetResponse(ha1, cnonce)}

雖然實(shí)現(xiàn)了無狀態(tài),安全性也高于Basic Auth,但Digest方式的用戶體驗(yàn)依然有限:每次向服務(wù)端發(fā)送請求,客戶端都要進(jìn)行一次復(fù)雜計(jì)算,服務(wù)端也要再做一次相同的驗(yàn)算和比對。cXw28資訊網(wǎng)——每日最新資訊28at.com

那么是否有一種體驗(yàn)更為良好的無狀態(tài)身份認(rèn)證方式呢?我們接下來看看基于Token的認(rèn)證方式。cXw28資訊網(wǎng)——每日最新資訊28at.com

4. 無狀態(tài):基于Token的認(rèn)證

基于Token的認(rèn)證方式的備受青睞得益于Web領(lǐng)域前后端分離架構(gòu)的發(fā)展以及微服務(wù)架構(gòu)的流行,在API調(diào)用和網(wǎng)站間需要輕量級的認(rèn)證機(jī)制來傳遞用戶信息。Token認(rèn)證機(jī)制正好滿足這一需求,而JWT(JSON Web Token)[10]是目前Token格式標(biāo)準(zhǔn)中使用最廣的一種。cXw28資訊網(wǎng)——每日最新資訊28at.com

4.1 JWT原理

JWT由頭部(Header)、載荷(Payload)和簽名(Signature)三部分組成,三部分之間用圓點(diǎn)連接,其形式如下:cXw28資訊網(wǎng)——每日最新資訊28at.com

xxxxx.yyyyy.zzzzz

一個真實(shí)的JWT token的例子如下面來自jwt.io[11]站點(diǎn)的截圖):cXw28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片cXw28資訊網(wǎng)——每日最新資訊28at.com

JWT token的生成過程也非常清晰,下圖展示了上述截圖中jwt token的生成過程:cXw28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片cXw28資訊網(wǎng)——每日最新資訊28at.com

如果你不想依賴第三方庫,也可以自己實(shí)現(xiàn)生成token的函數(shù),下面是一個示例:cXw28資訊網(wǎng)——每日最新資訊28at.com

// authn-examples/jwt/scratch/main.gopackage mainimport ( "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生成實(shí)現(xiàn)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)}

對照著上面圖示的流程,理解這個示例非常容易。當(dāng)然jwt.io官方也維護(hù)了一個使用簡單且靈活性更好的Go module:golang-jwt/jwt[12],用這個go module生成上述token的示例代碼如下:cXw28資訊網(wǎng)——每日最新資訊28at.com

// authn-examples/jwt/golang-jwt/main.goimport (    "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驗(yàn)證合法性的函數(shù)verifyToken,服務(wù)端每次收到客戶端請求中攜帶的token時,都可以使用verifyToken來驗(yàn)證token是否合法,下面是verifyToken的實(shí)現(xiàn)邏輯:cXw28資訊網(wǎng)——每日最新資訊28at.com

// authn-examples/jwt/golang-jwt/main.go// verifyToken 驗(yàn)證JWT函數(shù)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    }    // 驗(yàn)證簽名    if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {        return nil, jwt.ErrSignatureInvalid    }    return token, nil}

服務(wù)端驗(yàn)證token的邏輯是先解析token,得到header、payload對應(yīng)的base64UrlEncoded后的結(jié)果,然后用key重新生成簽名,對比生成的簽名與token攜帶的簽名是否一致。cXw28資訊網(wǎng)——每日最新資訊28at.com

那么在Web應(yīng)用中如何實(shí)現(xiàn)基于jwt token的身份認(rèn)證呢?我們繼續(xù)往下看。cXw28資訊網(wǎng)——每日最新資訊28at.com

4.2 使用JWT token做身份認(rèn)證

在前面講解Basic Auth、Digest Auth時,Basic Auth、Digest等服務(wù)端認(rèn)證方式利用了HTTP Header的Authorization字段,基于JWT token的認(rèn)證也是基于Authorization字段,只不過前綴從Basic、Digest換成了Bearer:cXw28資訊網(wǎng)——每日最新資訊28at.com

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2OTc4NjE5MzIsInVzZXJuYW1lIjoiYWRtaW4ifQ.go6NhfmYPZbtHEuJ1oULG890neo0yVdtFJwfAvHhxyE

基于JWT token的身份認(rèn)證方式的客戶端與服務(wù)端的交互流程如下圖:cXw28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片cXw28資訊網(wǎng)——每日最新資訊28at.com

在這幅示意圖中,客戶端先用basic auth方式登錄服務(wù)端,服務(wù)端驗(yàn)證通過后,在登錄應(yīng)答中寫入一個jwt token作為后續(xù)客戶端訪問服務(wù)端其他功能的依據(jù)。客戶端從登錄應(yīng)答的包體中解析出jwt token后,可以將該token存放在LocalStorage中,然后在后續(xù)的發(fā)向該服務(wù)端的所有請求中都帶上這個jwt token。服務(wù)端對這些請求都會校驗(yàn)其攜帶的jwt token,只有驗(yàn)證通過的請求才能被正確處理。cXw28資訊網(wǎng)——每日最新資訊28at.com

下面來看看對應(yīng)示意圖的示例源碼,先來看一下客戶端:cXw28資訊網(wǎng)——每日最新資訊28at.com

// authn-examples/jwt-authn/client/main.gofunc main() { client := &http.Client{} req, _ := http.NewRequest("POST", "http://server.com:8080/", nil) // 發(fā)送默認(rèn)請求 response, err := client.Do(req) if err != nil {  fmt.Println(err)  return } // 解析響應(yīng)頭 authHeader := response.Header.Get("WWW-Authenticate") loginReq, _ := http.NewRequest("POST", "http://server.com:8080/login", nil) username := "admin" password := "123456" // 判斷認(rèn)證類型 if !strings.Contains(authHeader, "Basic") {  // 不支持的認(rèn)證類型  fmt.Println("Unsupported authentication type:", authHeader)  return } // 使用Basic Auth, 添加Basic Auth頭 loginReq.SetBasicAuth(username, password) response, err = client.Do(loginReq) fmt.Println(response.StatusCode) // 從響應(yīng)包體中獲取服務(wù)端分配的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訪問服務(wù)端其他功能 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,通過驗(yàn)證后,拿到服務(wù)端生成的token。后續(xù)到該服務(wù)端的所有請求只需在Header中帶上token即可。cXw28資訊網(wǎng)——每日最新資訊28at.com

服務(wù)端的代碼如下:cXw28資訊網(wǎng)——每日最新資訊28at.com

// authn-examples/jwt-authn/server/main.gofunc main() { // 創(chuàng)建一個基本的HTTP服務(wù)器 mux := http.NewServeMux() username := "admin" password := "123456" key := "iamtonybai" // 針對/的handler mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {  // 返回401 Unauthorized響應(yīng)  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認(rèn)證信息  user, pass, ok := req.BasicAuth()  if !ok {   // 認(rèn)證失敗   w.WriteHeader(http.StatusUnauthorized)   return  }  // 驗(yàn)證用戶名密碼  if user == username && pass == password {   // 認(rèn)證成功,生成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 {   // 認(rèn)證失敗   http.Error(w, "Invalid username or password", http.StatusUnauthorized)  } }) // calc handler mux.HandleFunc("/calc", func(w http.ResponseWriter, req *http.Request) {  // 讀取并校驗(yàn)jwt token  token := req.Header.Get("Authorization")[len("Bearer "):]  fmt.Println(token)  if _, err := verifyToken(token, key); err != nil {   // 認(rèn)證失敗   http.Error(w, "Invalid token", http.StatusUnauthorized)   return  }  w.Write([]byte("invoke calc ok")) }) // 監(jiān)聽8080端口 err := http.ListenAndServe(":8080", mux) if err != nil {  log.Fatal(err) }}

我們看到,除了在login handler中使用basic auth做用戶密碼驗(yàn)證外,其他功能handler(如calc)中都使用token進(jìn)行身份驗(yàn)證。cXw28資訊網(wǎng)——每日最新資訊28at.com

與傳統(tǒng)會話式(session)認(rèn)證相比,JWT是無狀態(tài)的,更適用于分布式微服務(wù)架構(gòu)。與Basic auth和digest相比,jwt在使用體驗(yàn)上又領(lǐng)先一籌。憑借其無需在服務(wù)端保存會話狀態(tài)、天生適合分布式架構(gòu)、令牌內(nèi)容可以自定義擴(kuò)展等優(yōu)勢,現(xiàn)階段,jwt已廣泛應(yīng)用于以下場合:cXw28資訊網(wǎng)——每日最新資訊28at.com

  • 前后端分離的Web應(yīng)用和API認(rèn)證
  • 跨域單點(diǎn)登錄(SSO)
  • 微服務(wù)架構(gòu)下服務(wù)間認(rèn)證
  • 無狀態(tài)和移動應(yīng)用認(rèn)證

不過JWT認(rèn)證方式也有不足,比如:客戶端要承擔(dān)令牌存儲成本、如果令牌泄露未及時失效可能被濫用等。cXw28資訊網(wǎng)——每日最新資訊28at.com

講到這里,從基本的用戶名密碼認(rèn)證,到加上密碼散列的Digest認(rèn)證,再到應(yīng)用會話管理的Session認(rèn)證,以及基于令牌的JWT認(rèn)證,我們見證了認(rèn)證機(jī)制的不斷進(jìn)步和發(fā)展。cXw28資訊網(wǎng)——每日最新資訊28at.com

這些方法主要依賴賬號密碼這單一要素,提供了不同程度的安全性。但是隨著互聯(lián)網(wǎng)的快速發(fā)展,開發(fā)人員也在考慮改善用戶名密碼這種方式的使用體驗(yàn),一些一次性密碼認(rèn)證方式便走入了我們的生活。接下來我們就來簡單說一下一次性密碼驗(yàn)證。cXw28資訊網(wǎng)——每日最新資訊28at.com

5. 基于一次性密碼驗(yàn)證

一次性密碼(One Time Password, OTP)是一種只能使用一次的密碼,它在使用后立即失效。OTP生成密碼的算法基于時間,在很短的時間內(nèi)(一般分鐘內(nèi)或更短時間內(nèi))只能使用一次;每次驗(yàn)證都需要生成和輸入新的密碼,不能重復(fù)使用。cXw28資訊網(wǎng)——每日最新資訊28at.com

一次性密碼的優(yōu)勢主要有以下幾點(diǎn):cXw28資訊網(wǎng)——每日最新資訊28at.com

  • 安全性高:一次性密碼只能使用一次,因此即使攻擊者獲得了密碼,也無法重復(fù)使用。
  • 易用性強(qiáng):一次性密碼通常是數(shù)字或字母組成的短語,易于記憶和輸入。
  • 成本低:一次性密碼的生成和驗(yàn)證成本相對較低。

信息論已經(jīng)從理論上證明了:一次性密碼本是無條件安全的,在理論上是無法破譯的。不過現(xiàn)實(shí)中,還沒有一種理想的一次性密碼,大多數(shù)一次性密碼還處于身份認(rèn)證的輔助地位,多作為第二要素。cXw28資訊網(wǎng)——每日最新資訊28at.com

短信驗(yàn)證碼就是一種我們生活中常見的一次性密碼,它是利用移動運(yùn)營商的短信通道傳輸?shù)囊淮涡悦艽a。短信驗(yàn)證碼通常由6位數(shù)字組成,有效期為幾分鐘,并且只能使用一次,通過短信發(fā)送給用戶,非常方便用戶使用,用戶無需有記住密碼的煩惱。cXw28資訊網(wǎng)——每日最新資訊28at.com

短信驗(yàn)證碼的工作流程如下:cXw28資訊網(wǎng)——每日最新資訊28at.com

  • 客戶端發(fā)起認(rèn)證請求,如登錄或注冊;
  • 服務(wù)器生成6位隨機(jī)數(shù)字作為驗(yàn)證碼,通過文本短信發(fā)送到用戶注冊的手機(jī)號;
  • 用戶接收短信并輸入驗(yàn)證碼進(jìn)行驗(yàn)證;
  • 服務(wù)器通過時間戳驗(yàn)證此驗(yàn)證碼是否有效(一般在5分鐘內(nèi))。
  • 驗(yàn)證碼只能使用一次,服務(wù)器會將此條記錄標(biāo)記為使用。

短信驗(yàn)證碼的優(yōu)勢是方便快捷。目前國內(nèi)大多數(shù)主流Web應(yīng)用都支持手機(jī)驗(yàn)證碼登錄。短信驗(yàn)證碼通常用于以下場景:cXw28資訊網(wǎng)——每日最新資訊28at.com

  • 用戶注冊
  • 用戶登錄
  • 支付或交易
  • 輔助密碼找回等

不過手機(jī)驗(yàn)證碼這種一次性密碼的安全性相對較低,因?yàn)槎绦趴梢员唤孬@,攻擊者可以通過截獲短信來獲取驗(yàn)證碼。cXw28資訊網(wǎng)——每日最新資訊28at.com

除短信驗(yàn)證碼外,還有其他常見的OTP實(shí)現(xiàn)形式:cXw28資訊網(wǎng)——每日最新資訊28at.com

  • 手機(jī)應(yīng)用軟件OTP:使用專門的手機(jī)APP軟件生成OTP碼,如Google Authenticator、Microsoft Authenticator等。
  • 電子郵件OTP:類似短信驗(yàn)證碼,但通過郵件發(fā)送6-8位數(shù)字驗(yàn)證碼到用戶注冊的郵箱。
  • 語音驗(yàn)證碼OTP:服務(wù)端調(diào)用第三方語音平臺,使用文本到語音功能給用戶自動撥打認(rèn)證電話,提示驗(yàn)證碼。

總體來說,OTP越來越多地被用到用戶身份認(rèn)證上來,隨著以后技術(shù)的進(jìn)步,其應(yīng)用的廣度和深度會進(jìn)一步擴(kuò)大,安全性也會得到進(jìn)一步提升?;趥鹘y(tǒng)密碼的認(rèn)證方式早晚會被扔到歷史的舊物箱中。一些大廠,如Google都在研究替代傳統(tǒng)密碼的技術(shù),比如Passkey[13]等,一些Web標(biāo)準(zhǔn)組織也在做無密碼認(rèn)證的規(guī)范,比如WebAuthn[14]等。cXw28資訊網(wǎng)——每日最新資訊28at.com

6. 小結(jié)

就寫到這里吧,篇幅有些長了,關(guān)于OAuth、OpenID等身份認(rèn)證技術(shù)就不在這里寫了,后續(xù)找機(jī)會單獨(dú)梳理。cXw28資訊網(wǎng)——每日最新資訊28at.com

本文我們介紹了多種Web應(yīng)用的身份認(rèn)證技術(shù)方案,各種認(rèn)證技術(shù)會依據(jù)對安全性、使用性和擴(kuò)展性的不同需求而存在和發(fā)展。了解每種技術(shù)的原理和優(yōu)劣勢,可幫助我們更好地選擇適合的方案。cXw28資訊網(wǎng)——每日最新資訊28at.com

首次梳理這么多Web應(yīng)用身份認(rèn)證的資料,可能有些描述并不完全正確,歡迎指正。在撰寫本文時,大語言模型幫助編寫部分文字素材和代碼。cXw28資訊網(wǎng)——每日最新資訊28at.com

本文示例所涉及的Go源碼可以在這里[15]下載。cXw28資訊網(wǎng)——每日最新資訊28at.com

7. 參考資料

  • 《API安全實(shí)戰(zhàn)》[16] - https://book.douban.com/subject/36039150/
  • 《API安全技術(shù)與實(shí)戰(zhàn)》[17] - https://book.douban.com/subject/35429043/
  • 《深入淺出密碼學(xué)》[18] - https://book.douban.com/subject/36179106/
  • Web Authentication Methods Compared[19] - https://testdriven.io/blog/web-authentication-methods/
  • 認(rèn)證:系統(tǒng)如何正確分辨操作用戶的真實(shí)身份?[20] - https://time.geekbang.org/column/article/329954
  • 如何實(shí)現(xiàn)零信任網(wǎng)絡(luò)下安全的服務(wù)訪問?[21] - https://time.geekbang.org/column/article/345593
  • 憑證:系統(tǒng)如何保證與用戶之間的承諾是準(zhǔn)確完整且不可抵賴的?[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


cXw28資訊網(wǎng)——每日最新資訊28at.com

本文鏈接:http://www.tebozhan.com/showinfo-26-15216-0.html通過實(shí)例理解Go Web身份認(rèn)證的幾種方式

聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權(quán)等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。郵件:2376512515@qq.com

上一篇: 聊聊Java中線程的生命周期

下一篇: C++線程間共享數(shù)據(jù)的常見問題及解決方法

標(biāo)簽:
  • 熱門焦點(diǎn)
  • 轎車從天而降電動車主被撞身亡 超速搶道所致:現(xiàn)場視頻讓網(wǎng)友吵翻

    近日,上海青浦區(qū)法院判決轎車從天而降電動車主被撞身亡案,轎車車主被判有期徒刑一年。案件顯示當(dāng)時男子駕駛轎車在上海某路段行駛,前車忽然轉(zhuǎn)彎提速超車,
  • 十個可以手動編寫的 JavaScript 數(shù)組 API

    JavaScript 中有很多API,使用得當(dāng),會很方便,省力不少。 你知道它的原理嗎? 今天這篇文章,我們將對它們進(jìn)行一次小總結(jié)。現(xiàn)在開始吧。1.forEach()forEach()用于遍歷數(shù)組接收一參
  • 如何使用JavaScript創(chuàng)建一只圖像放大鏡?

    譯者 | 布加迪審校 | 重樓如果您曾經(jīng)瀏覽過購物網(wǎng)站,可能遇到過圖像放大功能。它可以讓您放大圖像的特定區(qū)域,以便瀏覽。結(jié)合這個小小的重要功能可以大大改善您網(wǎng)站的用戶體驗(yàn)
  • 多線程開發(fā)帶來的問題與解決方法

    使用多線程主要會帶來以下幾個問題:(一)線程安全問題  線程安全問題指的是在某一線程從開始訪問到結(jié)束訪問某一數(shù)據(jù)期間,該數(shù)據(jù)被其他的線程所修改,那么對于當(dāng)前線程而言,該線程
  • 這款新興工具平臺,讓你的電腦效率翻倍

    隨著信息技術(shù)的發(fā)展,我們獲取信息的渠道越來越多,但是處理信息的效率卻成為一個瓶頸。于是各種工具應(yīng)運(yùn)而生,都在爭相解決我們的工作效率問題。今天我要給大家介紹一款效率
  • 三分鐘白話RocketMQ系列—— 如何發(fā)送消息

    我們知道RocketMQ主要分為消息 生產(chǎn)、存儲(消息堆積)、消費(fèi) 三大塊領(lǐng)域。那接下來,我們白話一下,RocketMQ是如何發(fā)送消息的,揭秘消息生產(chǎn)全過程。注意,如果白話中不小心提到相關(guān)代
  • 一文搞定Java NIO,以及各種奇葩流

    大家好,我是哪吒。很多朋友問我,如何才能學(xué)好IO流,對各種流的概念,云里霧里的,不求甚解。用到的時候,現(xiàn)百度,功能雖然實(shí)現(xiàn)了,但是為什么用這個?不知道。更別說效率問題了~下次再遇到,
  • 2天漲粉255萬,又一賽道在抖音爆火

    來源:運(yùn)營研究社作者 | 張知白編輯 | 楊佩汶設(shè)計(jì) | 晏談夢潔這個暑期,旅游賽道徹底火了:有的「地方」火了&mdash;&mdash;貴州村超旅游收入 1 個月超過 12 億;有的「博主」火了&m
  • 上海舉辦人工智能大會活動,建設(shè)人工智能新高地

    人工智能大會在上海浦江兩岸隆重拉開帷幕,人工智能新技術(shù)、新產(chǎn)品、新應(yīng)用、新理念集中亮相。8月30日晚,作為大會的特色活動之一的上海人工智能發(fā)展盛典人工
Top