Go gorilla/sessions庫(kù)安裝使用
簡(jiǎn)介
上一篇文章《Go 每日一庫(kù)之 securecookie》中,我們介紹了 cookie。同時(shí)提到 cookie 有兩個(gè)缺點(diǎn),一是數(shù)據(jù)不宜過(guò)大,二是安全問(wèn)題。session 是服務(wù)器端的存儲(chǔ)方案,可以存儲(chǔ)大量的數(shù)據(jù),而且不需要向客戶(hù)端傳輸,從而解決了這兩個(gè)問(wèn)題。
但是 session 需要一個(gè)能唯一標(biāo)識(shí)用戶(hù)的 ID,這個(gè) ID 一般存放在 cookie 中發(fā)送到客戶(hù)端保存,隨每次請(qǐng)求一起發(fā)送到服務(wù)器。cookie 和 session 通常配套使用。
gorilla/sessions是 gorilla web 開(kāi)發(fā)工具包中管理 session 的庫(kù)。它提供了基于 cookie 和本地文件系統(tǒng)的 session。同時(shí)預(yù)留擴(kuò)展接口,可以使用其它的后端存儲(chǔ) session 數(shù)據(jù)。
本文先介紹sessions提供的兩種 session 存儲(chǔ)方式,然后通過(guò)第三方擴(kuò)展介紹在多個(gè) Web 服務(wù)器實(shí)例間如何保持登錄狀態(tài)。
快速使用
本文代碼使用 Go Modules。
創(chuàng)建目錄并初始化:
$ mkdir gorilla/sessions && cd gorilla/sessions $ go mod init github.com/darjun/go-daily-lib/gorilla/sessions
安裝gorilla/sessions庫(kù):
$ go get -u github.com/valyala/gorilla/sessions
現(xiàn)在我們實(shí)現(xiàn)在服務(wù)器端通過(guò) session 存儲(chǔ)一些信息的功能:
package main
import (
"fmt"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"log"
"net/http"
"os"
)
var (
store = sessions.NewFilesystemStore("./", securecookie.GenerateRandomKey(32), securecookie.GenerateRandomKey(32))
)
func set(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "user")
session.Values["name"] = "dj"
session.Values["age"] = 18
err := sessions.Save(r, w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintln(w, "Hello World")
}
func read(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "user")
fmt.Fprintf(w, "name:%s age:%d\n", session.Values["name"], session.Values["age"])
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/set", set)
r.HandleFunc("/read", read)
log.Fatal(http.ListenAndServe(":8080", r))
}整個(gè)程序邏輯比較清晰,分別在/set和/read路徑下掛上設(shè)置和讀取的處理函數(shù)。重點(diǎn)是變量store。我們調(diào)用session.NewFilesystemStore()方法創(chuàng)建了一個(gè)*sessions.FilesystemStore類(lèi)型的對(duì)象,它會(huì)將我們的 session 內(nèi)容存儲(chǔ)到文件系統(tǒng)(即本地磁盤(pán)上)。我們需要給NewFilesytemStore()方法傳入至少 2 個(gè)參數(shù),第一個(gè)參數(shù)指定 session 存儲(chǔ)的本地磁盤(pán)路徑。后續(xù)參數(shù)依次指定hashKey和blockKey(可省略),前者用于驗(yàn)證,后者用于加密,我們可以使用securecookie生成足夠隨機(jī)的 key,詳情見(jiàn)前一篇介紹securecookie的文章。
sessions為所有的 session 存儲(chǔ)抽象了一個(gè)接口Store:
type Store interface {
Get(r *http.Request, name string) (*Session, error)
New(r *http.Request, name string) (*Session, error)
Save(r *http.Request, w http.ResponseWriter, s *Session) error
}實(shí)現(xiàn)這個(gè)接口可以自定義我們存儲(chǔ) session 的位置和格式。
在set處理函數(shù)中,我們調(diào)用store.Get(r, "user")獲取名為user的 session,如果 session 不存在,則創(chuàng)建一個(gè)新的。sessions庫(kù)支持為同一個(gè)用戶(hù)創(chuàng)建多個(gè) session,store.Get()方法的第二個(gè)參數(shù)指定名字。獲取到的*Session結(jié)構(gòu)如下:
type Session struct {
ID string
Values map[interface{}]interface{}
Options *Options
IsNew bool
store Store
name string
}數(shù)據(jù)直接存放在Session.Values字段中,這是一個(gè)類(lèi)型為map[interface{}]interface{}的字段,幾乎能保存任何類(lèi)型的數(shù)據(jù)(之所以我這里要說(shuō)幾乎,因?yàn)檫€要考慮序列化到存儲(chǔ)的限制,有些數(shù)據(jù)類(lèi)型無(wú)法序列化為字節(jié)流保存,如chan)。
在set處理函數(shù)中,我們直接操作Values字段,最后我們調(diào)用store.Save(r, w, session)將 session數(shù)據(jù)保存到對(duì)應(yīng)的存儲(chǔ)中。
在get處理函數(shù)中,同樣地我們先調(diào)用store.Get(r, "user")獲取*Session對(duì)象,然后讀取里面的name和age值。
運(yùn)行:
$ go run main.go
首先訪(fǎng)問(wèn)localhost:8080/set,通過(guò)瀏覽器的開(kāi)發(fā)者工具Application頁(yè)簽查看 cookie:

我們發(fā)現(xiàn) session 的名字會(huì)作為 cookie 名發(fā)送到客戶(hù)端,session ID 被保存為 cookie 的值。
然后我們?cè)L問(wèn)localhost:8080/read,讀取到 session 保存的數(shù)據(jù):

另前面說(shuō)過(guò)FilesystemStore數(shù)據(jù)是存儲(chǔ)在本地硬盤(pán)上的,在運(yùn)行程序的本地目錄我們看到有以 session 開(kāi)頭的文件,文件名 session 后面的部分就是 session ID:

cookie 存儲(chǔ)
除了默認(rèn)的將本地文件系統(tǒng)作為存儲(chǔ)外,sessions還支持將 cookie 作為存儲(chǔ),也就是將 session 的數(shù)據(jù)直接通過(guò) cookie 在客戶(hù)端和服務(wù)器之間傳輸。cookie 存儲(chǔ)的創(chuàng)建方式與文件系統(tǒng)存儲(chǔ)的創(chuàng)建方式類(lèi)似:
var store = sessions.NewCookieStore(securecookie.GenerateRandomKey(32), securecookie.GenerateRandomKey(32))
sessions.NewCookieStore()方法的第一個(gè)參數(shù)為 hashKey 用于驗(yàn)證,第二個(gè)參數(shù)為 blockKey 用于加密,與sessions.NewFilesystemStore()一樣。
其他部分的代碼完全不用修改,運(yùn)行程序的結(jié)果與上面的一致。session 數(shù)據(jù)保存在 cookie 中,隨每次請(qǐng)求由客戶(hù)端傳給服務(wù)器。這種方式其實(shí)就是之前文章中介紹的 cookie 用法。
記錄登錄狀態(tài)
之前我們介紹gorilla/mux時(shí)介紹過(guò)使用 cookie 保存登錄狀態(tài)。當(dāng)時(shí)將用戶(hù)名和密碼經(jīng)過(guò)簡(jiǎn)單的 Base64 編碼后就直接存放在 cookie 中了,基本處于“裸露”狀態(tài)。只要有意,很容易就能竊取用戶(hù)名和密碼。現(xiàn)在我們將用戶(hù)關(guān)鍵信息存儲(chǔ)在 session 中,cookie 中只存儲(chǔ)一個(gè) session ID。
首先,我們?cè)O(shè)計(jì) 3 個(gè)頁(yè)面,登錄頁(yè)面,主頁(yè)面,授權(quán)才能訪(fǎng)問(wèn)的 secret 頁(yè)面。登錄頁(yè)面只需要用戶(hù)名&密碼的輸入框和登錄按鈕即可:
// login.tpl <form action="/login" method="post"> <label>Username:</label> <input name="username"><br> <label>Password:</label> <input name="password" type="password"><br> <button type="submit">登錄</button> </form>
登錄請(qǐng)求根據(jù)方法不同需要執(zhí)行不同的操作,GET 方法表示請(qǐng)求登錄的頁(yè)面,POST 方法表示執(zhí)行登錄操作。我們使用handlers.MethodHandler這個(gè)中間件來(lái)處理同一個(gè)路徑的不同方法的請(qǐng)求:
r.Handle("/login", handlers.MethodHandler{
"GET": http.HandlerFunc(Login),
"POST": http.HandlerFunc(DoLogin),
})Login處理函數(shù)很簡(jiǎn)單,只是展示頁(yè)面:
func Login(w http.ResponseWriter, r *http.Request) {
ptTemplate.ExecuteTemplate(w, "login.tpl", nil)
}這里我使用 Go 標(biāo)準(zhǔn)庫(kù)html/template模版庫(kù)來(lái)加載和管理各個(gè)頁(yè)面的模板:
var (
ptTemplate *template.Template
)
func init() {
template.Must(template.New("").ParseGlob("./tpls/*.tpl"))
}DoLogin處理函數(shù),需要驗(yàn)證登錄請(qǐng)求,然后創(chuàng)建User對(duì)象,保存在 session 中,接著重定向到主頁(yè)面:
func DoLogin(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
username := r.Form.Get("username")
password := r.Form.Get("password")
if username != "darjun" || password != "handsome" {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
SaveSessionUser(w, r, &User{Username: username})
http.Redirect(w, r, "/", http.StatusFound)
}下面是主頁(yè)面的處理,我們可以從 session 中取出保存的User對(duì)象,根據(jù)是否有User對(duì)象顯示不同的頁(yè)面:
// home.tpl
{% if . %}
<p>Hi, {% .Username %}</p><br>
<a href="/secret" rel="external nofollow" >Goto secret?</a>
{% else %}
<p>Hi, stranger</p><br>
<a href="/login" rel="external nofollow" >Goto login?</a>
{% end %}HomeHandler代碼如下:
func HomeHandler(w http.ResponseWriter, r *http.Request) {
u := GetSessionUser(r)
ptTemplate.ExecuteTemplate(w, "home.tpl", u)
}最后是 secret 頁(yè)面:
// secret.tpl
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit.
Inventore a cumque sunt pariatur nihil doloremque tempore,
consectetur ipsum sapiente id excepturi enim velit,
quis nisi esse doloribus aliquid. Incidunt, dolore.
</p>
<p>You have visited this page {% .Count %} times.</p>顯示訪(fǎng)問(wèn)了該頁(yè)面多少次。
SecretHandler如下:
func SecretHandler(w http.ResponseWriter, r *http.Request) {
u := GetSessionUser(r)
if u == nil {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
u.Count++
SaveSessionUser(w, r, u)
ptTemplate.ExecuteTemplate(w, "secret.tpl", u)
}如果沒(méi)有 session,則重定向到登錄頁(yè)面。反之顯示該頁(yè)面。這里每次成功訪(fǎng)問(wèn) secret 頁(yè)面,都會(huì)增加計(jì)數(shù)器,保存在 session 中。
上面代碼中需要注意一點(diǎn),由于 session 內(nèi)容的序列化使用了標(biāo)準(zhǔn)庫(kù)中的encoding/gob,所以不支持直接序列化結(jié)構(gòu)體,我封裝了兩個(gè)函數(shù),將User對(duì)象序列化為 JSON,然后保存到 session 中和從 session 中取出字符串反序列化為User對(duì)象:
func GetSessionUser(r *http.Request) *User {
session, _ := store.Get(r, "user")
s, ok := session.Values["user"]
if !ok {
return nil
}
u := &User{}
json.Unmarshal([]byte(s.(string)), u)
return u
}
func SaveSessionUser(w http.ResponseWriter, r *http.Request, u *User) {
session, _ := store.Get(r, "user")
data, _ := json.Marshal(u)
session.Values["user"] = string(data)
store.Save(r, w, session)
}現(xiàn)在運(yùn)行我們的程序,首先訪(fǎng)問(wèn)localhost:8080,由于沒(méi)有登錄,顯示歡迎陌生人,去登錄:

點(diǎn)擊去登錄,跳轉(zhuǎn)到登錄界面,輸入用戶(hù)名和密碼:

點(diǎn)擊登錄,跳轉(zhuǎn)到主頁(yè),這時(shí)由于記錄了登錄狀態(tài),會(huì)顯示歡迎 darjun:

點(diǎn)擊去隱秘鏈接:

不停刷新頁(yè)面,發(fā)現(xiàn)訪(fǎng)問(wèn)次數(shù)一直累加。
如果未登錄時(shí),直接訪(fǎng)問(wèn)localhost:8080/secret,會(huì)直接重定向到登錄界面。
上面程序有一個(gè)缺點(diǎn),程序重啟啟動(dòng)后,就需要重新登錄。因?yàn)槊看螁?dòng)我們都重新隨機(jī) hashKey 和 blockKey,只需要固定這兩個(gè)值即可實(shí)現(xiàn)重啟也能保存登錄狀態(tài)。
登錄驗(yàn)證類(lèi)的功能非常適合放在中間件中處理,之前的文章已經(jīng)介紹過(guò)如何編寫(xiě)中間件了,這里就不贅述了。
第三方后端存儲(chǔ)
將 session 存儲(chǔ)在本地文件系統(tǒng),不利于水平擴(kuò)展。一般稍微上點(diǎn)規(guī)模的網(wǎng)站,Web 服務(wù)器都會(huì)部署很多個(gè)實(shí)例,請(qǐng)求通過(guò) Nginx 之類(lèi)的反向代理轉(zhuǎn)發(fā)到一個(gè)后端實(shí)例處理。不能保證后面的請(qǐng)求與之前的請(qǐng)求在同一個(gè)實(shí)例中處理,故 session 一般需要存儲(chǔ)在一個(gè)公共的地方,例如 redis。
sessions提供了擴(kuò)展接口,方便擴(kuò)展使用其他的后端存儲(chǔ) session 內(nèi)容。目前 GitHub 上已經(jīng)有很多的第三方后端擴(kuò)展了,詳細(xì) list 見(jiàn)sessions庫(kù)的 GitHub 首頁(yè):

我們只介紹基于 redis 的后端存儲(chǔ),其他的擴(kuò)展感興趣可自行研究。首先安裝擴(kuò)展:
$ go get gopkg.in/boj/redistore.v1
創(chuàng)建一個(gè) redistore 的實(shí)例:
store, _ = redistore.NewRediStore(10, "tcp", ":6379", "", []byte("redis-key"))參數(shù)依次為:
size:最大空閑連接數(shù);
network:連接類(lèi)型,一般是 TCP;
addr:網(wǎng)絡(luò)地址+端口;
password:redis 的密碼,如果未啟用,填空;
keyPairs:依次是 hashKey 和 blockKey(可省略),不再贅述。
為了驗(yàn)證,我們開(kāi)啟多個(gè)服務(wù)器,所以將端口通過(guò)命令行參數(shù)傳入,使用標(biāo)準(zhǔn)庫(kù)flag:
port = flag.Int("port", 8080, "port to listen")
func init() {
flag.Parse()
}
func main() {
// ...
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
}為了運(yùn)行服務(wù)器,我們需要先開(kāi)啟一個(gè) redis-server。redis 的安裝就不多說(shuō)了,在 windows 下,建議使用 chocolatey 安裝,chocolatey 類(lèi)似于 Ubutnu 的 apt-get,Mac 的 brew,非常方便,強(qiáng)烈推薦。

為了演示反向代理的效果,即通過(guò)一個(gè)地址可以隨機(jī)訪(fǎng)問(wèn)部署的多個(gè) Web 服務(wù)器,我們開(kāi)啟 3 個(gè) Web 服務(wù)器。終端1:
$ go build $ ./redis -port 8080
終端2:
$ ./redis -port 8081
終端3:
$ ./redis -port 8082
可以使用nginx做反向代理,安裝 nginx,配置:
upstream mysvr {
server localhost:8080;
server localhost:8081;
server localhost:8082;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://mysvr;
}
}這里表示將localhost隨機(jī)轉(zhuǎn)發(fā)到mysvr這個(gè)組中的 3 個(gè)服務(wù)器上,啟動(dòng) nginx:
$ nginx -c nginx.conf
萬(wàn)事俱備,現(xiàn)在使用瀏覽器訪(fǎng)問(wèn)localhost,通過(guò)控制臺(tái)日志發(fā)現(xiàn)是 server3 處理了這個(gè)請(qǐng)求:

點(diǎn)擊去登錄,server1 處理了展示頁(yè)面的請(qǐng)求:

點(diǎn)擊登錄,server3 處理了 POST 類(lèi)型的登錄請(qǐng)求:

登錄成功之后,重定向到主界面的請(qǐng)求又是 server1 處理的:

點(diǎn)擊私密鏈接,展示頁(yè)面的請(qǐng)求是 server2 處理的:

雖然每次處理的 server 不同,但是登錄狀態(tài)一直保存著。因?yàn)槲覀兪褂昧?redis 保存 session。
注意,我這里每次都是隨機(jī)一個(gè) server 去處理,你運(yùn)行的結(jié)果不一定一樣。
總結(jié)
session 為了解決存儲(chǔ)用戶(hù)大量數(shù)據(jù)和安全性的問(wèn)題。sessions庫(kù)為 Go Web 開(kāi)發(fā)中處理 session 提供了簡(jiǎn)單,靈活的方法。它依賴(lài)較少,可以即插即用,非常方便。
大家如果發(fā)現(xiàn)好玩、好用的 Go 語(yǔ)言庫(kù),歡迎到 Go 每日一庫(kù) GitHub 上提交 issue??
參考
gorilla/sessions GitHub:github.com/gorilla/sessions
Go 每日一庫(kù) GitHub:https://github.com/darjun/go-daily-lib
以上就是Go gorilla/sessions庫(kù)安裝使用的詳細(xì)內(nèi)容,更多關(guān)于Go gorilla/sessions庫(kù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go?語(yǔ)言進(jìn)階freecache源碼學(xué)習(xí)教程
這篇文章主要為大家介紹了Go?語(yǔ)言進(jìn)階freecache源碼學(xué)習(xí)教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04
Go語(yǔ)言中的自定義函數(shù)類(lèi)型的實(shí)現(xiàn)
在Go語(yǔ)言中,函數(shù)類(lèi)型是一種將函數(shù)作為值的數(shù)據(jù)類(lèi)型,本文主要介紹了Go語(yǔ)言中的自定義函數(shù)類(lèi)型,具有一定的參考價(jià)值,感興趣的可以了解一下2023-09-09
golang標(biāo)準(zhǔn)庫(kù)time時(shí)間包的使用
時(shí)間和日期是我們編程中經(jīng)常會(huì)用到的,本文主要介紹了golang標(biāo)準(zhǔn)庫(kù)time時(shí)間包的使用,具有一定的參考價(jià)值,感興趣的可以了解一下2023-10-10
golang簡(jiǎn)易實(shí)現(xiàn)?k8s?的yaml上傳并應(yīng)用示例方案
這篇文章主要為大家介紹了golang簡(jiǎn)易實(shí)現(xiàn)?k8s?的yaml上傳并應(yīng)用示例方案,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07
Go語(yǔ)言實(shí)現(xiàn)順序存儲(chǔ)的線(xiàn)性表實(shí)例
這篇文章主要介紹了Go語(yǔ)言實(shí)現(xiàn)順序存儲(chǔ)的線(xiàn)性表的方法,實(shí)例分析了Go語(yǔ)言實(shí)現(xiàn)線(xiàn)性表的定義、插入、刪除元素等的使用技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-03-03
Go語(yǔ)言之使用pprof工具查找goroutine(協(xié)程)泄漏
這篇文章主要介紹了Go語(yǔ)言之使用pprof工具查找goroutine(協(xié)程)泄漏,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01
本地使用Docker搭建go開(kāi)發(fā)環(huán)境的全過(guò)程
最近想學(xué)習(xí)一下golang,自己之前一直把環(huán)境全部安裝在docker上,所以這次也想把golang的環(huán)境安裝在docker上,下面這篇文章主要給大家介紹了關(guān)于本地使用Docker搭建go開(kāi)發(fā)環(huán)境的相關(guān)資料,需要的朋友可以參考下2022-07-07

