在 Go 語言中使用 Session(一)


在上一篇博客 理解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章


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM