在上一篇博客 理解Cookie和Session 中,咱們瞭解了 Cookie 和 Session 的一些基礎知識,也知道了 Session 的基本原理是由服務端保存一份狀態信息(以及它的惟一標識符),客戶端會經過這個惟一標識符來訪問這份狀態信息數據。html
整個客戶端和服務端的交互過程能夠歸納爲如下三個步驟:數據庫
在 Go 的標準庫中並無提供對 Sessoin 的實現,因此下面咱們經過分析《Go Web編程》一書中的示例來學習一下如何自行實現一個 Session 的功能。 (ps:雖然標準庫中沒有實現 session,可是有不少 Web 框架都提供了 session 的實現)編程
實現 Session 主要須要考慮如下幾點:安全
下面跟着相應的 go 代碼示例分析一下整個設計思路:cookie
Session 使用的是一種相似散列表的結構(也可能就是散列表)來保存的信息。若是您有任何 Web 開發經驗,您應該知道 Session 只有四個操做:設置值,獲取值,刪除值和獲取當前的 SessionID。 所以 Session 接口應該有四種方法來執行這種操做: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 是保存在服務端的數據,所以咱們能夠抽象出一個 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) }
定義好了 Provider 接口以後,咱們再寫一個註冊方法,使得咱們能夠根據 provider 管理器的名稱就能找到其對應的 provider 管理器ide
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是用來識別訪問 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 信息來驗證操做。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中 } }
在 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。
//在啓動函數中開啓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章