Go Web編程–SecureCookie實現客戶端Session管理

  • 2020 年 3 月 12 日
  • 筆記

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獲取本文源代碼