在 Go 語言中使用 Session(一)

在上一篇博客 理解Cookie和Session 中,咱們瞭解了 Cookie 和 Session 的一些基礎知識,也知道了 Session 的基本原理是由服務端保存一份狀態信息(以及它的惟一標識符),客戶端會經過這個惟一標識符來訪問這份狀態信息數據。html

整個客戶端和服務端的交互過程能夠歸納爲如下三個步驟:數據庫

  • 客戶端第一次發送請求時,服務端建立 Session,並生成惟一標識符 SessionId
  • 服務端將 SessionId 發送給客戶端 (通常來講有兩種經常使用的方式:Cookie 和 URL 重寫)
  • 客戶端再次向服務端發送請求時一併將 SessionId 發送給服務端。

Go 實現 session

在 Go 的標準庫中並無提供對 Sessoin 的實現,因此下面咱們經過分析《Go Web編程》一書中的示例來學習一下如何自行實現一個 Session 的功能。 (ps:雖然標準庫中沒有實現 session,可是有不少 Web 框架都提供了 session 的實現)編程

實現 Session 主要須要考慮如下幾點:安全

  • Session 的建立
  • 全局 Session 管理器
  • SessionID 的全局惟一性
  • Session 的存儲(能夠存儲到內存、文件、數據庫等)
  • Session 過時處理

下面跟着相應的 go 代碼示例分析一下整個設計思路:cookie

定義 Session

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 管理器

咱們知道 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 管理器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

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章

相關文章
相關標籤/搜索