在 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 -
我們可以在下圖里看到, 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 來的明顯。