Go Web編程–SecureCookie實現客戶端Session管理
- 2020 年 3 月 12 日
- 筆記
在Web
應用開發中Session
是在用戶和服務器之間進行交換的非持久化交互信息。當用戶登錄時,可以在用戶和服務器之間生成Session
,然後來回交換數據,並在用戶登出時銷毀Session
。gorilla/sessions
軟件包提供了易於使用的Go
語言Session
實現。該軟件包提供了兩種不同的實現。第一個是文件系統存儲,它將每個會話存儲在服務器的文件系統中。另一個是Cookie
存儲,它使用我們上篇文章講的SecureCookie
在客戶端上存儲會話。同時還提供了用戶自定義Session
存儲實現的選項,我們可以根據應用的需求自己實現Session
存儲。因為我們的教程是學會使用為目的就不大費周章的去實現MySQL
或者Redis
版本的Session
存儲了,我們直接使用軟件包提供的Cookie
實現來完成本節的Session
相關內容。
Go Web 編程系列的每篇文章的源代碼都打了對應版本的軟件包,供大家參考。公眾號中回復
gohttp09
獲取本文源代碼
使用Cookie存儲用戶Session的優缺點
客戶端使用Cookie
管理用戶Session
較之在服務器進行用戶的Session
管理會有一些優勢。客戶端Session
增加了應用程序的可伸縮性,因為所有的會話數據都存儲在用戶端,因此可以將用戶的請求平衡到不同的遠端服務器,也不必在服務器端對所有用戶的會話進行統一管理,所以使用Cookie
存儲用戶Session
會更簡單一些。
當然有優勢就必定有劣勢,客戶端Cookie
的整體大小是有限制的。目前,Google Chrome
瀏覽器將Cookie
限制為4096
個位元組。
客戶端會話還意味着無法終止會話,從而導致註銷不完整。如果用戶在退出前保存了Cookie
中的會話信息,則他們可以使用該會話信息創建一個新的Cookie
,然後繼續使用該應用程序,為了最大程度地降低安全風險,我們可以將會話Cookie
設置為在合理的時間內過期,使用加密後的ScureCookie
存儲數據,同時還要避免在其中存儲敏感信息(即使是服務端管理Session
也不應該存儲類似密碼這種敏感信息)。
總之在考慮使用客戶端還是服務端存儲用戶Session
時一定要根據應用的使用場景來選擇,這一點很重要。
安裝gorilla/sessions
在開始編碼前先來安裝一下gorilla/sessions
軟件包,
$ go get github.com/gorilla/sessions
並簡單看一下軟件包功能特性的介紹
- 方便地設置簽名(也可以選擇加密)的
Cookie
。 - 自帶將會話存儲在
Cookie
或服務端文件系統中的SessionStore
實現。 - 支持Flash消息:讀取即銷毀的會話數據。
- 支持方便地切換會話數據的持久化方式。
- 為不同的
Session
存儲提供統一的接口和基礎設施。
演示用戶Session設計實現
我們今天的示例代碼是用gorilla/sessions
提供的CookieSessionStore
實現一個簡單的系統登錄功能。
我們會定義如下幾個路由:
/user/login
用戶登錄驗證,驗證成功後在用戶Session
數據中標記用戶是已驗證的。/user/logout
用戶登出,會在Session
中標記用戶是未認證的。/user/secret
通過用戶Session
判斷用戶是否已認證,未認證返回403 Forbidden
錯誤。
為了達到演示目的的同時減少文章中出現過多代碼,我們不會做前端頁面,通過命令行cURL
直接請求上面幾個URL
驗證我們的系統登錄功能。
初始化工作
我們現在項目的handler
目錄下新建一個user
子目錄,用於存放使用到用戶Session
的處理程序
... handler/ └── user/ └── init.go └── login.go └── logout.go └── secret.go ... main.go
其下的四個分別是包的初始化程序init.go
以及存放上面說的三個路由處理程序的.go
源文件。
初始化Session存儲
我們把Session
存儲的初始化工作放在user
包的init
函數中,這樣首次導入user
包時即可完成相關的初始化工作。
package user import "github.com/gorilla/sessions" const ( //64位 cookieStoreAuthKey = "..." //AES encrypt key必須是16或者32位 cookieStoreEncryptKey = "..." ) var sessionStore *sessions.CookieStore func init () { sessionStore = sessions.NewCookieStore( []byte(cookieStoreAuthKey), []byte(cookieStoreEncryptKey), ) sessionStore.Options = &sessions.Options{ HttpOnly: true, MaxAge: 60 * 15, } }
實現登錄驗證
// login.go var sessionCookieName = "user-session" func Login(w http.ResponseWriter, r *http.Request) { session, err := sessionStore.Get(r, sessionCookieName) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // 登錄驗證 name := r.FormValue("name") pass := r.FormValue("password") _, err = logic.AuthenticateUser(name, pass) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return } // 在session中標記用戶已經通過登錄驗證 session.Values["authenticated"] = true err = session.Save(r, w) fmt.Fprintln(w, "登錄成功!", err) }
- 我們將瀏覽器
Cookie
中存儲用戶Session
的Cookie-Name
設置成了user-session
。 - 登錄驗證就是簡單的用戶名和密碼查找匹配的用戶,在之前的文章應用數據庫和應用 ORM兩篇文章中有在
MySQL
數據庫中創建users
表,並介紹了怎麼使用ORM
操作數據庫,沒有看過的同學可以回看一下。 - 登錄驗證成功後在
Session
的authenticated
中標記了用戶已通過認證。session.Values
是類型map[interface{}]interface{}
的別名,所以可以往其中存儲任意類型的數據。
實現登出
登出我們這裡就是簡單的將Session
中authenticated
的值設置成了false
.
//logout.go func Logout(w http.ResponseWriter, r *http.Request) { session, _ := sessionStore.Get(r, sessionCookieName) session.Values["authenticated"] = false session.Save(r, w) }
使用Session認證用戶
//secret.go func Secret(w http.ResponseWriter, r *http.Request) { session, _ := sessionStore.Get(r, sessionCookieName) if auth, ok := session.Values["authenticated"].(bool); !ok || !auth { http.Error(w, "Forbidden", http.StatusForbidden) return } fmt.Fprintln(w, "這裡還是空空如也!") }
- 使用
Session
中存儲的數據值都是接口類型的,所以使用時要先對其進行類型斷言session.Values["authenticated"].(bool)
- 如果
authenticated
的值不為true
或者是從Session
中獲取不到對應的值,這裡直接返回HTTP 403 Forbidden
錯誤。
註冊路由
// router.go func RegisterRoutes(r *mux.Router) { ... userRouter := r.PathPrefix("/user").Subrouter() userRouter.HandleFunc("/login", user.Login).Methods("POST") userRouter.HandleFunc("/secret", user.Secret) userRouter.HandleFunc("/logout", user.Logout) ... }
驗證已實現的Session管理功能
編寫完上面的Session
管理的功能後,重啟服務器,然後使用cURL
分別請求URL
驗證一下效果。
curl -XPOST -d 'name=Klein&password=123' -c - http://localhost:8000/user/login
-c
選項表示將Cookie
寫入到後面的文件中,完整格式是-c -<file_name>
,短橫線後不帶文件名表示把Cookie
寫入到標準輸出中。
我們可以在下圖裡看到,Cookie
中的user-session
存儲的就是加密後的Session
數據了

圖片
如果請求中不攜帶這個Cookie
訪問/user/secret
會直接返回HTTP 403
錯誤

圖片
那麼接下來在使用cURL
請求/user/secret
時帶上上面返回的Cookie
值,看看請求是否能成功
curl --cookie "user-session=MTU4m..." http://localhost:8000/user/secret

圖片
Cookie
加密後的值太長了,搞得字兒好小,cURL
執行的結果顯示服務器成功地響應了我們的請求。你們試驗的時候換成自己生成的Cookie
值請求就可以啦。
你們實踐時也可以用PostMan
代替cURL
試驗,不過感覺PostMan
的返回不如cURL
來的明顯。
Go Web 編程系列的每篇文章的源代碼都打了對應版本的軟件包,供大家參考。公眾號中回復
gohttp09
獲取本文源代碼