1. 程式人生 > >在 Go 語言中使用 Session(一)

在 Go 語言中使用 Session(一)

art manager 步驟 新建 tex 代碼 request ace use

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

在 Go 語言中使用 Session(一)