在上一篇博客 理解Cookie和Session 中,我們了解了 Cookie 和 Session 的一些基礎知識,也知道了 Session 的基本原理是由服務端保存一份狀態信息(以及它的唯一標識符),客戶端會通過這個唯一標識符來訪問這份狀態信息數據。
整個客戶端和服務端的交互過程可以概括為以下三個步驟:
- 客戶端第一次發送請求時,服務端創建 Session,並生成唯一標識符 SessionId
- 服務端將 SessionId 發送給客戶端
(一般來說有兩種常用的方式:Cookie 和 URL 重寫) - 客戶端再次向服務端發送請求時一並將 SessionId 發送給服務端。
Go 實現 session
在 Go 的標准庫中並沒有提供對 Sessoin 的實現,所以下面我們通過分析《Go Web編程》一書中的示例來學習一下如何自行實現一個 Session 的功能。
(ps:雖然標准庫中沒有實現 session,但是有很多 Web 框架都提供了 session 的實現)
實現 Session 主要需要考慮以下幾點:
- Session 的創建
- 全局 Session 管理器
- SessionID 的全局唯一性
- Session 的存儲(可以存儲到內存、文件、數據庫等)
- Session 過期處理
下面跟着相應的 go 代碼示例分析一下整個設計思路:
定義 Session
Session 使用的是一種類似散列表的結構(也可能就是散列表)來保存的信息。如果您有任何 Web 開發經驗,您應該知道 Session 只有四個操作:設置值,獲取值,刪除值和獲取當前的 SessionID。
因此 Session 接口應該有四種方法來執行這種操作:
type Session interface {
Set(key, value interface{}) error //設置Session
Get(key interface{}) interface{} //獲取Session
Delete(key interface{}) error //刪除Session
SessionID() string //當前SessionID
}
可以通過多種方式保存 Session,包括內存,文件和數據庫等,所以這里定義了一個 Session 操作接口,不同存儲方式的 Session 操作有所不同,實現也不同。
Session 管理器
我們知道 Session 是保存在服務端的數據,因此我們可以抽象出一個 Provider 接口來表示 Session 管理器的底層結構。Provider 將通過 SessionID 來訪問和管理 Session。
type Provider interface {
SessionInit(sid string) (Session, error)
SessionRead(sid string) (Session, error)
SessionDestroy(sid string) error
SessionGC(maxLifeTime int64)
}
- SessionInit 實現 Session 的初始化,如果成功則返回新的 Session
- SessionRead 返回由相應 sid 表示的 Session,如果不存在,那么將以 sid 為參數調用 SessionInit 方法創建並返回一個新的 Session 變量。
- SessionDestroy 給定一個 sid,刪除相應的 Session。
- SessionGC 根據 maxLifeTime 刪除過期的 Session 變量
定義好了 Provider 接口之后,我們再寫一個注冊方法,使得我們可以根據 provider 管理器的名稱就能找到其對應的 provider 管理器
var providers = make(map[string]Provider)
//注冊一個能通過名稱來獲取的 session provider 管理器
func RegisterProvider(name string, provider Provider) {
if provider == nil {
panic("session: Register provider is nil")
}
if _, p := providers[name]; p {
panic("session: Register provider is existed")
}
providers[name] = provider
}
接着再把 Provider 封裝一下,定義一個全局的 Session 的管理器
type Manager struct {
cookieName string //cookie的名稱
lock sync.Mutex //鎖,保證並發時數據的安全一致
provider Provider //管理session
maxLifeTime int64 //超時時間
}
func NewManager(providerName, cookieName string, maxLifetime int64) (*Manager, error){
provider, ok := providers[providerName]
if !ok {
return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", providerName)
}
//返回一個 Manager 對象
return &Manager{
cookieName: cookieName,
maxLifeTime: maxLifetime,
provider: provider,
}, nil
}
然后在 main 包中創建一個全局的 Session 管理器
var globalSession *Manager
func init() {
globalSession, _ = NewManager("memory", "sessionid", 3600)
}
唯一的 Session ID
Session ID是用來識別訪問 Web 應用的每一個用戶的,因此需要保證它是全局唯一的,示例代碼如下:
func (manager *Manager) sessionId() string {
b := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return ""
}
return base64.URLEncoding.EncodeToString(b)
}
創建 Session
我們需要為每個來訪的用戶分配或者獲取與它相關連的 Session,以便后面根據 Session 信息來驗證操作。SessionStart 這個函數就是用來檢測是否已經有某個 Session 與當期來訪用戶發生了關聯,如果沒有則創建它。
//根據當前請求的cookie中判斷是否存在有效的session, 不存在則創建
func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) {
//為該方法加鎖
manager.lock.Lock()
defer manager.lock.Unlock()
//獲取 request 請求中的 cookie 值
cookie, err := r.Cookie(manager.cookieName)
if err != nil || cookie.Value == "" {
sid := manager.sessionId()
session, _ = manager.provider.SessionInit(sid)
cookie := http.Cookie{
Name: manager.cookieName,
Value: url.QueryEscape(sid), //轉義特殊符號@#¥%+*-等
Path: "/",
HttpOnly: true,
MaxAge: int(manager.maxLifeTime)}
http.SetCookie(w, &cookie) //將新的cookie設置到響應中
} else {
sid, _ := url.QueryUnescape(cookie.Value)
session, _ = manager.provider.SessionRead(sid)
}
return
}
現在我們已經可以通過 SessionStart 方法返回一個滿足 Session 接口的變量了。下面通過一個例子來展示一下 Session 的讀寫操作:
//根據用戶名判斷是否存在該用戶的session,不存在則創建
func login(w http.ResponseWriter, r *http.Request){
sess := globalSession.SessionStart(w, r)
r.ParseForm()
name := sess.Get("username")
if name != nil {
sess.Set("username", r.Form["username"]) //將表單提交的username值設置到session中
}
}
注銷 Session
在 Web 應用中通常有用戶退出登錄操作,那么當用戶退出應用的時候,我們就可以對該用戶的 session 數據進行注銷。
// SessionDestroy 注銷 Session
func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(manager.cookieName)
if err != nil || cookie.Value == "" {
return
}
manager.lock.Lock()
defer manager.lock.Unlock()
manager.provider.SessionDestroy(cookie.Value)
expiredTime := time.Now()
newCookie := http.Cookie{
Name: manager.cookieName,
Path: "/", HttpOnly: true,
Expires: expiredTime,
MaxAge: -1, //會話級cookie
}
http.SetCookie(w, &newCookie)
}
現在我們有對 Session 進行 讀(Get)、寫(Set)、刪除(Destroy)操作的方法了,下面結合這三個操作來展示一個示例:
//記錄該session被訪問的次數
func count(w http.ResponseWriter, r *http.Request) {
sess := globalSession.SessionStart(w, r) //獲取session實例
createTime := sess.Get("createTime") //獲得該session的創建時間
if createTime == nil {
sess.Set("createTime", time.Now().Unix())
} else if (createTime.(int64) + 360) < (time.Now().Unix()) { //已過期
//注銷舊的session信息,並新建一個session globalSession.SessionDestroy(w, r)
sess = globalSession.SessionStart(w, r)
}
count := sess.Get("countnum")
if count == nil {
sess.Set("countnum", 1)
} else {
sess.Set("countnum", count.(int) + 1)
}
}
刪除 Session
接着再來看看如何讓 Session 管理器刪除 Session。
//在啟動函數中開啟GC
func init() {
go globalSession.SessionGC()
}
func (manager *Manager) SessionGC() {
manager.lock.Lock()
defer manager.lock.Unlock()
manager.provider.SessionGC(manager.maxLifeTime)
//使用time包中的計時器功能,它會在session超時時自動調用GC方法
time.AfterFunc(time.Duration(manager.maxLifeTime), func() {
manager.SessionGC()
})
}
以上類似的解決方法可用於計算在線用戶上。
至此,我們實現了一個用來在 Web 應用中全局管理 Session 的 SessionManager,定義了用來提供 Session 存儲實現 Provider 接口。
接口的具體實現
關於針對 Session 接口和 Provider 接口的具體實現,這里就不展開了,有興趣的讀者可參考《Go Web編程》第 6.3 小節的內容
參考:
《Go Web 編程》第6章