本文將從Web應用程式處理請求時需要使用者資訊,同時HTTP又是無狀態協定這個矛盾點出發。從該問題出發,簡單描述瞭解決該問題的Token
機制,進而引出Cookie
的實現方案。
基於此我們將詳細描述Cookie
的規範,然後詳細描述具體的實現方式,進一步描述Gin
框架對Cookie
操作提供的API
,最終提供了一個詳細的程式碼實現。
我們還將詳細描述Gin
框架提供API
的實現原理,幫助使用者更好得使用這兩個API
。
在 如何使用Gin搭建一個Go Web應用程式 一文中,我們已經瞭解瞭如何使用Gin
搭建一個簡單的Web應用程式。然而,在現實的Web應用程式中,大部分功能都是需要使用者的身份資訊才能處理。舉例來說,在一個視訊網站檢視使用者最近觀看記錄,如果缺少使用者身份資訊,此時將無法對請求進行處理。
但是HTTP協定的設計,是無狀態的,也就是每次請求都是獨立的。基於此,應該有一套機制,能夠在使用者身份認證成功後,給使用者分配一個Token
,後續使用者在每次請求時,都攜帶上該Token
,使得伺服器能夠從請求中獲取使用者資訊,解決HTTP無狀態問題。大概流程如下:
上面流程中,需要伺服器端按照某個協定,向用戶端返回Token
;使用者端通過該協定,成功解析出伺服器端返回的Token
,然後在每次請求中攜帶該Token
。然後伺服器端再根據協定,從中解析出Token
資訊,獲取請求使用者資訊。
當前常用的有Cookie
,Jwt
,OAuth2.0
等標準,其各有優缺點。其中Cookie
是一種儲存在使用者端瀏覽器中的資料。伺服器端可以通過設定HTTP響應頭將Token
儲存在Cookie
當中,並在後續請求中從Cookie
中讀取Token
。而JWT
則是一種基於JSON格式的安全令牌,可用於在使用者端和伺服器端之間傳遞資訊。
之前,我們在 一文讀懂Cookie 中,已經瞭解Cookie
的相關內容。基於此,我們這次使用Cookie
來實現上述所說的流程,按照Cookie
的規範來實現Token
的返回和請求中Token
的解析。
這裡我們對HTTP協定中的Cookie
規範再補充一下,這裡分為兩部分,第一部分是伺服器端如何向用戶端傳送 Cookie
,第二部分是使用者端向伺服器端傳送請求時如何攜帶Cookie
資訊。
對於伺服器端向用戶端傳送Cookie
的手段,HTTP協定存在一個Set-Cookie
的頭部欄位,伺服器可以通過Set-Cookie
頭部欄位將Cookie
傳送給使用者端。例如下面這個例子:
Set-Cookie: username=abc; expires=Wed, 09 Jun 2023 10:18:14 GMT; path=/
在這個例子中,伺服器設定了一個名為username
的Cookie
,它的值是abc
,過期時間是2023年6月9日,路徑為/
。瀏覽器在接收到該Cookie
時,便將其儲存起來。
使用者端請求時攜帶Cookie
的方式,則是通過HTTP協定中的Cookie
頭部欄位,使用者端可以通過該頭部欄位攜帶資訊給伺服器端,比如下面這個例子:
Cookie: sessionid=1234
在這個例子中,HTTP請求中攜帶了一個name
為sessionid
,value
為 1234
的 Cookie
。當伺服器端接收到該HTTP
請求後,從中解析出Cookie
的資訊,然後基於此實現後續的流程。
回看上述流程,主要分為兩個大部分: 使用者端和伺服器端。在使用者端部分,關鍵任務包括儲存瀏覽器返回的Cookie
資訊以及在請求時攜帶Cookie
資訊給伺服器。對於伺服器端,則是在通過身份校驗之後,能夠按照規範使用者端返回Cookie
,並在接收到請求時,能夠正確解析出請求中的 Cookie
資訊,識別出使用者資訊。
對於使用者端部分,在瀏覽器接收到HTTP響應時,如果響應體中有Set-Cookie
頭部欄位,瀏覽器會自動儲存Cookie
資訊;使用者端發起請求時,需要將 Cookie
資訊傳遞給伺服器。此時瀏覽器會自動攜帶通過校驗的Cookie
。如果通過校驗,此時會在HTTP請求頭中攜帶Cookie
資訊給伺服器端,下面是一個大概的校驗流程:
在整個流程中,使用者端儲存Token
資訊和在請求時攜帶Token
資訊這兩部分工作,瀏覽器已經幫我們實現了。剩下的工作集中在伺服器端的,主要涉及按照Cookie
的規範給使用者端返回使用者標識,並在接收到使用者端請求時從HTTP請求中讀取Cookie
以獲取到使用者的資訊。與Cookie
相關的詳細內容可以參考文章一文讀懂Cookie。
因此下面我們需要做的兩件事情,其一,伺服器需要按照Cookie
的規範往使用者端傳送Cookie
的內容;其次,伺服器在處理請求時,需要從HTTP請求頭中讀取出Cookie
的資訊,成功識別使用者身份。
Gin
框架中提供了一些API
,能夠幫助我們在伺服器端,按照Cookie
規範給使用者端傳送Cookie
資訊,同時也有API
能夠幫助我們解析Cookie
的資訊。下面我們先來了解相關的API
,然後再基於這些API
,搭建一個能夠自動識別使用者資訊的 Web
應用程式。
gin.Context
物件中的 SetCookie
方法用於向用戶端返回響應的同時,在Set-Cookie
頭部攜帶Cookie
資訊。下面是該方法的詳細說明:
func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)
name
:cookie 的名稱(必須)。value
:cookie 的值(必須)。maxAge
:cookie 的過期時間,以秒為單位。如果為負數,則表示對談 cookie(在瀏覽器關閉之後刪除),如果為零,則表示立即刪除 cookie(可選,預設值為-1)。path
:cookie 的路徑。如果為空字串,則使用當前請求的 URI 路徑作為預設值(可選,預設值為空字串)。domain
:cookie 的域名。如果為空字串,則不設定域名(可選,預設值為空字串)。secure
:指定是否僅通過 HTTPS 連線傳送 cookie。如果為 true,則僅通過 HTTPS 連線傳送 cookie;否則,使用 HTTP 或 HTTPS 連線都可以傳送 cookie(可選,預設值為 false)。httpOnly
:指定 cookie 是否可通過 JavaScript 存取。如果為 true,則無法通過 JavaScript 存取 cookie;否則,可以通過 JavaScript 存取 cookie(可選,預設值為 true)。在處理常式中,通過呼叫SetCookie
方法,便可以向用戶端傳送一個HTTP cookie。這裡舉一個程式碼範例,來幫助讀者更好得理解該API
,下面舉一個程式碼範例,如下:
func main() {
router := gin.Default()
router.GET("/set-cookie", func(c *gin.Context) {
c.SetCookie("user", "john", 3600, "/", "", false, true)
c.String(http.StatusOK, "cookie set successfully")
})
router.Run(":8080")
}
在這個範例中,使用 SetCookie
方法設定一個名為user
的 cookie。這個 cookie 的值是john
,在 1 小時後過期。該程式碼還設定了路徑為「/」以及HttpOnly屬性為true。
下面啟動該伺服器,使用者端向伺服器端傳送請求,請求路徑為/set-cookie
,上面的處理常式將會被執行,然後我們來看其響應內容:
# 1. 傳送請求
curl -i http://localhost:8080/set-cookie
# 2. 返回響應
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Set-Cookie: user=john; Path=/; Max-Age=3600; HttpOnly
Date: Sun, 20 Aug 2023 07:39:15 GMT
Content-Length: 23
cookie set successfully
檢視上面第6行,可以看到,我們通過SetCookie
方法,成功設定了一個Cookie
,然後以在HTTP頭部的形式返回。
往使用者端返回Cookie
後,瀏覽器會將Cookie
儲存起來,然後在下次請求時將Cookie
跟隨請求一起傳送給伺服器端。
在HTTP無狀態協定的情況下,我們使用Cookie
來識別使用者資訊,此時伺服器端需要正確解析出HTTP 頭部中Cookie
的資訊,Gin
框架中的gin.Context
提供了Cookie
方法,方便我們獲取到Cookie
的資訊。下面是該方法的定義說明:
func (c *Context) Cookie(name string) (string, error)
使用Cookie
方法可以獲取指定名稱的Cookie值,如果不存在指定名字的Cookie
,此時將會返回錯誤。下面給一個簡單範例程式碼的說明:
func main() {
router := gin.Default()
// 定義路由
router.GET("/cookie", func(c *gin.Context) {
// 獲取名為 "username" 的 cookie
cookie, err := c.Cookie("username")
if err != nil {
// 如果 cookie 不存在,則返回錯誤資訊
c.JSON(http.StatusBadRequest, gin.H{"error": "Bad request"})
return
}
// 在響應中返回 cookie 值
c.JSON(http.StatusOK, gin.H{"username": cookie})
})
router.Run(":8080")
}
在上述範例中,我們定義了一個 /cookie
路由,使用 c.Cookie("username")
方法來獲取名為 username
的 Cookie 值。如果 Cookie 不存在,則返回一個錯誤響應。否則,我們將在響應中返回 Cookie 的值。
下面我們通過curl
命令來對/cookie
請求,通過 -b
標識來攜帶cookie
值:
# -v, --verbose 這個引數會開啟curl的詳細模式,輸出一些額外的資訊,包括HTTP請求和響應頭資訊。
curl -b -v -b "username=hello cookie;" http://localhost:8080/cookie
下面我們來看具體的請求體和響應體的內容:
GET /cookie HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.79.1
Accept: */*
Cookie: username=hello cookie;
可以看到,我們請求體攜帶了Cookie
欄位,Cookie
的名稱為 username
,我們前面伺服器端便是嘗試獲取名為 username
的 Cookie,下面我們看請求的響應體,看是否成功解析了HTTP 請求 Cookie的內容:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 20 Aug 2023 08:12:45 GMT
Content-Length: 27
{"username":"hello cookie"}
可以看到,伺服器端程式通過Cookie
方法成功解析了HTTP請求頭部中Cookie
欄位的值,然後將解析的結果正常返回使用者端。
下面我們來搭建一個基於Cookie
實現使用者身份驗證的Web
應用程式,首先需要一個登入頁面,用於驗證使用者身份資訊,驗證通過後,我們將通過Cookie
給使用者端返回一個 Token
。
同時,我們還需要建立一個頁面,需要驗證使用者身份資訊,在驗證過程中,我們會檢查使用者請求中是否攜帶Cookie
,同時Cookie
中攜帶的資料是否正確,基於此實現使用者身份的驗證。下面是一個簡單程式碼的範例:
func main() {
route := gin.Default()
route.GET("/login", func(c *gin.Context) {
// HTTP 響應中攜帶 Cookie
// Set cookie {"label": "ok" }, maxAge 30 seconds.
c.SetCookie("label", "ok", 30, "/", "localhost", false, true)
c.String(200, "Login success!")
})
route.GET("/home", func(c *gin.Context) {
// 獲取 name = label 的 Cookie 的 value
if cookie, err := c.Cookie("label"); err == nil {
// 判斷 Cookie的value 是否滿足預期
if cookie == "ok" {
c.JSON(200, gin.H{"data": "Your home page"})
}
} else {
c.JSON(http.StatusForbidden, gin.H{"error": "Forbidden with no cookie"})
}
})
route.Run(":8080")
}
首先是一個/login
請求路由,通過SetCookie
方法給使用者端返回Cookie
,基於此返回使用者Token
。
然後/home
路由的處理,則是通過gin.Context
中Cookie
方法獲取到HTTP請求頭部中Cookie
的資訊 ,然後驗證Cookie
中的value是否滿足預期。
這個是一個簡單的程式碼範例,比如使用者身份認證機制等,則需要自行完善,這裡不再完整展示。
下面將簡單描述gin.Context
物件中SetCookie
方法和Cookie
方法的實現原理,幫助讀者更好使用這兩個API
。
SetCookie
方法的實現原理如下,首先,SetCookie
方法會建立一個http.Cookie
物件,並設定其名稱、值、路徑、域名、過期時間等屬性。例如,以下程式碼建立了一個名為sessionid
的Cookie
:
cookie := &http.Cookie{
Name: "sessionid",
Value: "1234",
Expires: time.Now().Add(24 * time.Hour),
Path: "/",
Domain: "",
Secure: false,
HttpOnly:true,
}
接下來,將上述Cookie
物件轉換為字串格式,並設定到HTTP響應頭的Set-Cookie
欄位中。程式碼實現如下:
func SetCookie(w ResponseWriter, cookie *Cookie) {
if v := cookie.String(); v != "" {
w.Header().Add("Set-Cookie", v)
}
}
這裡第三行將Cookie
儲存到Header
物件當中,Header
是專門用於儲存HTTP響應頭部的資訊。呼叫Add
方法時,會根據指定的Key
,在 Header
物件中查詢相應的值列表。如果這個鍵不存在,則會在 Header
物件中建立一個新的值列表;否則,會在已有的值列表末尾新增新的值,大概流程如下:
在返回HTTP響應時,會遍歷Header
物件,填充HTTP響應頭部資訊,然後返回給使用者端,比如上面Header
生成的HTTP響應頭部如下:
Set-Cookie: v1
Set-Cookie: v2
Agent: Windows
SetCookie
方法便是通過上述所說流程,將Cookie
的資訊設定到HTTP響應體頭部當中去,然後返回給使用者端。
在呼叫 Cookie()
方法時,系統會首先檢查請求頭部中是否包含名為 Cookie
的欄位。如果該欄位不存在,則返回空字串。
如果請求頭部中包含 Cookie
欄位,同時Cookie
的name
為呼叫Cookie()
方法指定的值,則系統會解析該欄位並將其轉換為一個 http.Cookie
物件。這個物件包含了所有的 Cookie
屬性,例如名稱、值、路徑、過期時間、域名等等。最後,返回轉換後的http.Cookie
物件中值,大概流程如下:
總的來說,Cookie()
方法的實現原理比較簡單,它只是通過查詢 HTTP 請求頭部中的 Cookie 資訊,並將其轉換為 http.Cookie
物件來獲取請求中特定 Cookie 值。
在本文中,我們深入探討了Web應用程式在處理使用者資訊時所面臨的挑戰,特別是在HTTP協定作為無狀態協定的背景下。我們從這一矛盾出發,介紹瞭解決方案中的Token
機制,並引出了基於Cookie
的實現方案。
我們詳細闡述了Cookie
的規範,包括伺服器端如何傳送Cookie
以及使用者端如何在請求中攜帶Cookie
資訊。
我們進一步深入探討了具體的實現方式,並介紹了Gin
框架提供的API
,這些API
使得在伺服器端按照Cookie
規範傳送和解析Cookie變得更加容易。通過一個實際的程式碼範例,我們演示瞭如何使用這些API來構建一個基於Cookie實現使用者身份驗證的Web應用程式。
在探討API
的使用之餘,我們也深入剖析了Gin框架提供的API的實現原理,為讀者提供了更深層次的理解。
基於此,完成了對Gin中Cookie支援的介紹,希望對你有所幫助。