Web 應用開發中 Session 是在用戶和服務器之間進行交換的非持久化交互信息。當用戶登錄時,可以在用戶和服務器之間生成 Session ,然後來回交換數據,並在用戶登出時銷燬 Sessiongorilla/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 中存儲用戶 SessionCookie-Name 設置成了 user-session
  • 登錄驗證就是簡單的用戶名和密碼查找匹配的用戶,在之前的文章 應用數據庫應用 ORM 兩篇文章中有在 MySQL 數據庫中創建 users 表,並介紹了怎麼使用 ORM 操作數據庫,沒有看過的同學可以回看一下。
  • 登錄驗證成功後在 Sessionauthenticated 中標記了用戶已通過認證。 session.Values 是類型 map[interface{}]interface{} 的別名,所以可以往其中存儲任意類型的數據。

實現登出

登出我們這裏就是簡單的將 Sessionauthenticated 的值設置成了 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 獲取本文源代碼

前文回顧

Go Web 編程--如何確保Cookie數據的安全傳輸

Go Web編程--應用ORM

Go Web編程--應用ORM

五分鐘用Docker快速搭建Go開發環境

深入學習用Go編寫HTTP服務器

相關文章