Files
build-web-application-with-…/zh-tw/06.2.md
2019-02-26 01:40:54 +08:00

10 KiB
Raw Blame History

6.2 Go如何使用session

透過上一小節的介紹我們知道session是在伺服器端實現的一種使用者和伺服器之間認證的解決方案目前Go標準套件沒有為session提供任何支援這小節我們將會自己動手來實現go版本的session管理和建立。

session建立過程

session的基本原理是由伺服器為每個會話維護一份資訊資料客戶端和伺服器端依靠一個全域性唯一的標識來訪問這份資料以達到互動的目的。當用戶訪問Web應用時伺服器端程式會隨需要建立session這個過程可以概括為三個步驟

  • 產生全域性唯一識別符號sessionid
  • 開闢資料儲存空間。一般會在記憶體中建立相應的資料結構但這種情況下系統一旦掉電所有的會話資料就會丟失如果是電子商務類別網站這將造成嚴重的後果。所以為了解決這類別問題你可以將會話資料寫到檔案裡或儲存在資料庫中當然這樣會增加I/O開銷但是它可以實現某種程度的session持久化也更有利於session的共享
  • 將session的全域性唯一標示符傳送給客戶端。

以上三個步驟中最關鍵的是如何傳送這個session的唯一標識這一步上。考慮到HTTP協議的定義資料無非可以放到請求行、頭域或Body裡所以一般來說會有兩種常用的方式cookie和URL重寫。

  1. Cookie 伺服器端透過設定Set-cookie頭就可以將session的識別符號傳送到客戶端而客戶端此後的每一次請求都會帶上這個識別符號另外一般包含session資訊的cookie會將失效時間設定為0(會話cookie)即瀏覽器程序有效時間。至於瀏覽器怎麼處理這個0每個瀏覽器都有自己的方案但差別都不會太大(一般體現在新建瀏覽器視窗的時候)
  2. URL重寫 所謂URL重寫就是在返回給使用者的頁面裡的所有的URL後面追加session識別符號這樣使用者在收到響應之後無論點選響應頁面裡的哪個連結或提交表單都會自動帶上session識別符號從而就實現了會話的保持。雖然這種做法比較麻煩但是如果客戶端禁用了cookie的話此種方案將會是首選。

Go實現session管理

透過上面session建立過程的講解讀者應該對session有了一個大體的認識但是具體到動態頁面技術裡面又是怎麼實現session的呢下面我們將結合session的生命週期lifecycle來實現go語言版本的session管理。

session管理設計

我們知道session管理涉及到如下幾個因素

  • 全域性session管理器
  • 保證sessionid 的全域性唯一性
  • 為每個客戶關聯一個session
  • session 的儲存(可以儲存到記憶體、檔案、資料庫等)
  • session 過期處理

接下來我將講解一下我關於session管理的整個設計思路以及相應的go程式碼示例

Session管理器

定義一個全域性的session管理器


type Manager struct {
	cookieName  string     // private cookiename
	lock        sync.Mutex // protects session
	provider    Provider
	maxLifeTime int64
}

func NewManager(provideName, cookieName string, maxLifeTime int64) (*Manager, error) {
	provider, ok := provides[provideName]
	if !ok {
		return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName)
	}
	return &Manager{provider: provider, cookieName: cookieName, maxLifeTime: maxLifeTime}, nil
}

Go實現整個的流程應該也是這樣的在main套件中建立一個全域性的session管理器


var globalSessions *session.Manager
//然後在init函式中初始化
func init() {
	globalSessions, _ = NewManager("memory", "gosessionid", 3600)
}

我們知道session是儲存在伺服器端的資料它可以以任何的方式儲存比如儲存在記憶體、資料庫或者檔案中。因此我們抽象出一個Provider介面用以表徵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介面需要實現什麼樣的功能呢有過Web開發經驗的讀者知道對Session的處理基本就 設定值、讀取值、刪除值以及取得當前sessionID這四個操作所以我們的Session介面也就實現這四個操作。


type Session interface {
	Set(key, value interface{}) error // set session value
	Get(key interface{}) interface{}  // get session value
	Delete(key interface{}) error     // delete session value
	SessionID() string                // back current sessionID
}

以上設計思路來源於database/sql/driver先定義好介面然後具體的儲存session的結構實現相應的介面並註冊後相應功能這樣就可以使用了以下是用來隨需註冊儲存session的結構的Register函式的實現。


var provides = make(map[string]Provider)

// Register makes a session provide available by the provided name.
// If Register is called twice with the same name or if driver is nil,
// it panics.
func Register(name string, provider Provider) {
	if provider == nil {
		panic("session: Register provider is nil")
	}
	if _, dup := provides[name]; dup {
		panic("session: Register called twice for provider " + name)
	}
	provides[name] = provider
}

全域性唯一的Session ID

Session ID是用來識別訪問Web應用的每一個使用者因此必須保證它是全域性唯一的GUID下面程式碼展示瞭如何滿足這一需求


func (manager *Manager) sessionId() string {
	b := make([]byte, 32)
	if _, err := rand.Read(b); err != nil {
		return ""
	}
	return base64.URLEncoding.EncodeToString(b)
}

session建立

我們需要為每個來訪使用者分配或取得與他相關連的Session以便後面根據Session資訊來驗證操作。SessionStart這個函式就是用來檢測是否已經有某個Session與當前來訪使用者發生了關聯如果沒有則建立之。


func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) {
	manager.lock.Lock()
	defer manager.lock.Unlock()
	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)
	} else {
		sid, _ := url.QueryUnescape(cookie.Value)
		session, _ = manager.provider.SessionRead(sid)
	}
	return
}

我們用前面login操作來示範session的運用


func login(w http.ResponseWriter, r *http.Request) {
	sess := globalSessions.SessionStart(w, r)
	r.ParseForm()
	if r.Method == "GET" {
		t, _ := template.ParseFiles("login.gtpl")
		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)
	}
}

操作值:設定、讀取和刪除

SessionStart函式返回的是一個滿足Session介面的變數那麼我們該如何用他來對session資料進行操作呢

上面的例子中的程式碼session.Get("uid")已經展示了基本的讀取資料的操作,現在我們再來看一下詳細的操作:


func count(w http.ResponseWriter, r *http.Request) {
	sess := globalSessions.SessionStart(w, r)
	createtime := sess.Get("createtime")
	if createtime == nil {
		sess.Set("createtime", time.Now().Unix())
	} else if (createtime.(int64) + 360) < (time.Now().Unix()) {
		globalSessions.SessionDestroy(w, r)
		sess = globalSessions.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.gtpl")
	w.Header().Set("Content-Type", "text/html")
	t.Execute(w, sess.Get("countnum"))
}

透過上面的例子可以看到Session的操作和操作key/value資料庫類似:Set、Get、Delete等操作

因為Session有過期的概念所以我們定義了GC操作當訪問過期時間滿足GC的觸發條件後將會引起GC但是當我們進行了任意一個session操作都會對Session實體進行更新都會觸發對最後訪問時間的修改這樣當GC的時候就不會誤刪除還在使用的Session實體。

session重置

我們知道Web應用中有使用者退出這個操作那麼當用戶退出應用的時候我們需要對該使用者的session資料進行銷燬操作上面的程式碼已經示範瞭如何使用session重置操作下面這個函式就是實現了這個功能


//Destroy sessionid
func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request){
	cookie, err := r.Cookie(manager.cookieName)
	if err != nil || cookie.Value == "" {
		return
	} else {
		manager.lock.Lock()
		defer manager.lock.Unlock()
		manager.provider.SessionDestroy(cookie.Value)
		expiration := time.Now()
		cookie := http.Cookie{Name: manager.cookieName, Path: "/", HttpOnly: true, Expires: expiration, MaxAge: -1}
		http.SetCookie(w, &cookie)
	}
}

session銷燬

我們來看一下Session管理器如何來管理銷燬只要我們在Main啟動的時候啟動


func init() {
	go globalSessions.GC()
}

func (manager *Manager) GC() {
	manager.lock.Lock()
	defer manager.lock.Unlock()
	manager.provider.SessionGC(manager.maxLifeTime)
	time.AfterFunc(time.Duration(manager.maxLifeTime), func() { manager.GC() })
}

我們可以看到GC充分利用了time套件中的定時器功能當超時maxLifeTime之後呼叫GC函式這樣就可以保證maxLifeTime時間內的session都是可用的類似的方案也可以用於統計線上使用者數之類別的。

總結

至此 我們實現了一個用來在Web應用中全域性管理Session的SessionManager定義了用來提供Session儲存實現Provider的介面下一小節我們將會透過介面定義來實現一些Provider,供大家參考學習。