Go基礎學習記錄之如何在Golang中使用Session
Session背後的基本原則是伺服器維護每個客戶端的資訊,客戶端依賴唯一的SessionID來訪問此資訊。
當用戶訪問Web應用程式時,伺服器將根據需要使用以下三個步驟建立新Session:
- 建立唯一的Session ID
- 開啟資料儲存空間:通常我們將Session儲存在記憶體中,但如果系統意外中斷,您將丟失所有Session資料。如果Web應用程式處理敏感資料(例如電子商務),這可能是一個非常嚴重的問題。為了解決此問題,您可以將Session資料儲存在資料庫或檔案系統中。這使得資料永續性更加可靠,並且易於與其他應用程式共享,但需要權衡的是,讀取和寫入這些Session需要更多的伺服器端IO。
- 將唯一SessionID傳送到客戶端。
這裡的關鍵步驟是將唯一Session ID傳送到客戶端。在標準HTTP響應的上下文中,您可以使用響應行,標題或正文來完成此操作;因此,我們有兩種方法將Session ID傳送給客戶端:通過cookie或URL重寫。
- Cookie:伺服器可以輕鬆地在響應標頭內使用Set-cookie將Session ID傳送到客戶端,然後客戶端可以將此cookie用於將來的請求;我們經常將包含Session資訊的cookie的到期時間設定為0,這意味著cookie將儲存在記憶體中,並且只有在使用者關閉瀏覽器後才會被刪除。
- URL重寫:將Session ID作為引數附加到所有頁面的URL中。這種方式看起來很混亂,但如果客戶在瀏覽器中禁用了cookie,那麼這是最好的選擇。
使用Go來管理Session
Session管理設計
- 全域性Session管理。
- 保持Session ID唯一。
- 為每個使用者準備一個Session。
- Session儲存在記憶體,檔案或資料庫中。
- 處理過期的Session。
接下來,通過完整例項來演示下如何實現上面的設計
全域性Session管理
定義全域性Session管理器:
// Manager Session管理 type Manager struct { cookieNamestring locksync.Mutex providerProvider maxLifeTime int64 } // GetManager 獲取Session Manager func GetManager(providerName string, cookieName string, maxLifeTime int64) (*Manager, error) { provider, ok := providers[providerName] if !ok { return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", providerName) } return &Manager{ cookieName:cookieName, maxLifeTime: maxLifeTime, provider:provider, }, nil }
在main()函式中建立一個全域性Session管理器:
var appSession *Manager // 初始化session manager func init() { appSession, _ = GetManager("memory", "sessionid", 3600) go appSession.SessionGC() }
我們知道我們可以通過多種方式儲存Session,包括記憶體,檔案系統或直接進入資料庫。我們需要定義一個Provider介面,以表示Session管理器的底層結構:
// Provider 介面 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。建立一個新Session,如果它尚不存在則返回它。
- SessionDestroy給定一個sid,刪除相應的Session。
- SessionGC根據maxLifeTime刪除過期的Session變數。那麼我們的Session介面應該有什麼方法呢?如果您有任何Web開發經驗,您應該知道Session只有四個操作:設定值,獲取值,刪除值和獲取當前Session ID。因此,我們的Session介面應該有四種方法來執行這些操作。
// Session 介面 type Session interface { Set(key, value interface{}) error // 設定Session Get(key interface{}) interface{}// 獲取Session Del(key interface{}) error// 刪除Session SID() string// 當前Session ID }
這個設計源於database/sql/driver,它首先定義介面,然後在我們想要使用它時註冊特定的結構。以下程式碼是Session暫存器功能的內部實現。
var providers = make(map[string]Provider) // RegisterProvider 註冊Session 暫存器 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 }
保持Session ID唯一
Session ID用於標識Web應用程式的使用者,因此它們必須是唯一的。以下程式碼顯示瞭如何實現此目標:
// GenerateSID 產生唯一的Session ID func (m *Manager) GenerateSID() string { b := make([]byte, 32) if _, err := io.ReadFull(rand.Reader, b); err != nil { return "" } return base64.URLEncoding.EncodeToString(b) }
建立Session
我們需要分配或獲取現有Session以驗證使用者操作。SessionStart函式用於檢查與當前使用者相關的任何Session的存在,並在未找到任何Session時建立新Session。
// SessionStart 啟動Session功能 func (m *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) { m.lock.Lock() defer m.lock.Unlock() cookie, err := r.Cookie(m.cookieName) if err != nil || cookie.Value == "" { sid := m.GenerateSID() session, _ := m.provider.SessionInit(sid) newCookie := http.Cookie{ Name:m.cookieName, Value:url.QueryEscape(sid), Path:"/", HttpOnly: true, MaxAge:int(m.maxLifeTime), } http.SetCookie(w, &newCookie) } else { sid, _ := url.QueryUnescape(cookie.Value) session, _ := m.provider.SessionRead(sid) } return }
以下是使用Session進行登入操作的示例。
func login(w http.ResponseWriter, r *http.Request) { sess := appSession.SessionStart(w, r) r.ParseForm() if r.Method == "GET" { t, _ := template.ParseFiles("login.html") w.Header().Set("Content-Type", "text/html") t.Execute(w, sess.Get("username")) } else { sess.Set("username", r.Form["username"]) http.Redirect(w, r, "/", 302) } }
Session的相關操作
SessionStart函式返回實現Session介面的變數。我們如何使用它?您在上面的示例中看到了session.Get("uid")以進行基本操作。現在讓我們來看一個更詳細的例子。
func count(w http.ResponseWriter, r *http.Request) { sess := appSession.SessionStart(w, r) createtime := sess.Get("createtime") if createtime == nil { sess.Set("createtime", time.Now().Unix()) } else if (createtime.(int64) + 360) < (time.Now().Unix()) { appSession.SessionDestroy(w, r) sess = appSession.SessionStart(w, r) } ct := sess.Get("countnum") if ct == nil { sess.Set("countnum", 1) } else { sess.Set("countnum", (ct.(int) + 1)) } t, _ := template.ParseFiles("count.html") w.Header().Set("Content-Type", "text/html") t.Execute(w, sess.Get("countnum")) }
如您所見,對Session進行操作只需在Set,Get和Delete操作中使用鍵/值模式。由於Session具有到期時間的概念,因此我們定義GC以更新Session的最新修改時間。這樣,GC將不會刪除已過期但仍在使用的Session。
登出Session
我們知道Web應用程式具有登出操作。當用戶登出時,我們需要刪除相應的Session。我們已經在上面的示例中使用了重置操作 - 現在讓我們看一下函式體。
// SessionDestory 登出Session func (m *Manager) SessionDestory(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie(m.cookieName) if err != nil || cookie.Value == "" { return } m.lock.Lock() defer m.lock.Unlock() m.provider.SessionDestroy(cookie.Value) expiredTime := time.Now() newCookie := http.Cookie{ Name:m.cookieName, Path:"/", HttpOnly: true, Expires:expiredTime, MaxAge:-1, } http.SetCookie(w, &newCookie) }
刪除Session
讓我們看看如何讓Session管理器刪除Session。我們需要在main()函式中啟動GC:
func init() { go appSession.SessionGC() } // SessionGC Session 垃圾回收 func (m *Manager) SessionGC() { m.lock.Lock() defer m.lock.Unlock() m.provider.SessionGC(m.maxLifeTime) time.AfterFunc(time.Duration(m.maxLifeTime), func() { m.SessionGC() }) }
我們看到GC充分利用了時間包中的計時器功能。
它會在Session超時時自動呼叫GC,確保在maxLifeTime期間所有Session都可用。
類似的解決方案可用於計算線上使用者。