Go Web編程--SecureCookie實現客戶端Session管理
在 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
獲取本文源代碼
前文回顧