<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
上一篇文章《Go 每日一庫之 securecookie》中,我們介紹了 cookie。同時提到 cookie 有兩個缺點,一是資料不宜過大,二是安全問題。session 是伺服器端的儲存方案,可以儲存大量的資料,而且不需要向用戶端傳輸,從而解決了這兩個問題。
但是 session 需要一個能唯一標識使用者的 ID,這個 ID 一般存放在 cookie 中傳送到使用者端儲存,隨每次請求一起傳送到伺服器。cookie 和 session 通常配套使用。
gorilla/sessions是 gorilla web 開發工具包中管理 session 的庫。它提供了基於 cookie 和本地檔案系統的 session。同時預留擴充套件介面,可以使用其它的後端儲存 session 資料。
本文先介紹sessions
提供的兩種 session 儲存方式,然後通過第三方擴充套件介紹在多個 Web 伺服器範例間如何保持登入狀態。
本文程式碼使用 Go Modules。
建立目錄並初始化:
$ mkdir gorilla/sessions && cd gorilla/sessions $ go mod init github.com/darjun/go-daily-lib/gorilla/sessions
安裝gorilla/sessions
庫:
$ go get -u github.com/valyala/gorilla/sessions
現在我們實現在伺服器端通過 session 儲存一些資訊的功能:
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:%dn", 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)) }
整個程式邏輯比較清晰,分別在/set
和/read
路徑下掛上設定和讀取的處理常式。重點是變數store
。我們呼叫session.NewFilesystemStore()
方法建立了一個*sessions.FilesystemStore
型別的物件,它會將我們的 session 內容儲存到檔案系統(即本地磁碟上)。我們需要給NewFilesytemStore()
方法傳入至少 2 個引數,第一個引數指定 session 儲存的本地磁碟路徑。後續引數依次指定hashKey
和blockKey
(可省略),前者用於驗證,後者用於加密,我們可以使用securecookie
生成足夠隨機的 key,詳情見前一篇介紹securecookie
的文章。
sessions
為所有的 session 儲存抽象了一個介面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 }
實現這個介面可以自定義我們儲存 session 的位置和格式。
在set
處理常式中,我們呼叫store.Get(r, "user")
獲取名為user
的 session,如果 session 不存在,則建立一個新的。sessions
庫支援為同一個使用者建立多個 session,store.Get()
方法的第二個引數指定名字。獲取到的*Session
結構如下:
type Session struct { ID string Values map[interface{}]interface{} Options *Options IsNew bool store Store name string }
資料直接存放在Session.Values
欄位中,這是一個型別為map[interface{}]interface{}
的欄位,幾乎能儲存任何型別的資料(之所以我這裡要說幾乎,因為還要考慮序列化到儲存的限制,有些資料型別無法序列化為位元組流儲存,如chan
)。
在set
處理常式中,我們直接操作Values
欄位,最後我們呼叫store.Save(r, w, session)
將 session資料儲存到對應的儲存中。
在get
處理常式中,同樣地我們先呼叫store.Get(r, "user")
獲取*Session
物件,然後讀取裡面的name
和age
值。
執行:
$ go run main.go
首先存取localhost:8080/set
,通過瀏覽器的開發者工具Application
頁籤檢視 cookie:
我們發現 session 的名字會作為 cookie 名傳送到使用者端,session ID 被儲存為 cookie 的值。
然後我們存取localhost:8080/read
,讀取到 session 儲存的資料:
另前面說過FilesystemStore
資料是儲存在本地硬碟上的,在執行程式的本地目錄我們看到有以 session 開頭的檔案,檔名 session 後面的部分就是 session ID:
除了預設的將本地檔案系統作為儲存外,sessions
還支援將 cookie 作為儲存,也就是將 session 的資料直接通過 cookie 在使用者端和伺服器之間傳輸。cookie 儲存的建立方式與檔案系統儲存的建立方式類似:
var store = sessions.NewCookieStore(securecookie.GenerateRandomKey(32), securecookie.GenerateRandomKey(32))
sessions.NewCookieStore()
方法的第一個引數為 hashKey 用於驗證,第二個引數為 blockKey 用於加密,與sessions.NewFilesystemStore()
一樣。
其他部分的程式碼完全不用修改,執行程式的結果與上面的一致。session 資料儲存在 cookie 中,隨每次請求由使用者端傳給伺服器。這種方式其實就是之前文章中介紹的 cookie 用法。
之前我們介紹gorilla/mux
時介紹過使用 cookie 儲存登入狀態。當時將使用者名稱和密碼經過簡單的 Base64 編碼後就直接存放在 cookie 中了,基本處於“裸露”狀態。只要有意,很容易就能竊取使用者名稱和密碼。現在我們將使用者關鍵資訊儲存在 session 中,cookie 中只儲存一個 session ID。
首先,我們設計 3 個頁面,登入頁面,主頁面,授權才能存取的 secret 頁面。登入頁面只需要使用者名稱&密碼的輸入框和登入按鈕即可:
// 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>
登入請求根據方法不同需要執行不同的操作,GET 方法表示請求登入的頁面,POST 方法表示執行登入操作。我們使用handlers.MethodHandler
這個中介軟體來處理同一個路徑的不同方法的請求:
r.Handle("/login", handlers.MethodHandler{ "GET": http.HandlerFunc(Login), "POST": http.HandlerFunc(DoLogin), })
Login
處理常式很簡單,只是展示頁面:
func Login(w http.ResponseWriter, r *http.Request) { ptTemplate.ExecuteTemplate(w, "login.tpl", nil) }
這裡我使用 Go 標準庫html/template
模版庫來載入和管理各個頁面的模板:
var ( ptTemplate *template.Template ) func init() { template.Must(template.New("").ParseGlob("./tpls/*.tpl")) }
DoLogin
處理常式,需要驗證登入請求,然後建立User
物件,儲存在 session 中,接著重定向到主頁面:
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) }
下面是主頁面的處理,我們可以從 session 中取出儲存的User
物件,根據是否有User
物件顯示不同的頁面:
// 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 頁面:
// 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>
顯示存取了該頁面多少次。
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) }
如果沒有 session,則重定向到登入頁面。反之顯示該頁面。這裡每次成功存取 secret 頁面,都會增加計數器,儲存在 session 中。
上面程式碼中需要注意一點,由於 session 內容的序列化使用了標準庫中的encoding/gob
,所以不支援直接序列化結構體,我封裝了兩個函數,將User
物件序列化為 JSON,然後儲存到 session 中和從 session 中取出字串反序列化為User
物件:
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) }
現在執行我們的程式,首先存取localhost:8080
,由於沒有登入,顯示歡迎陌生人,去登入:
點選去登入,跳轉到登入介面,輸入使用者名稱和密碼:
點選登入,跳轉到主頁,這時由於記錄了登入狀態,會顯示歡迎 darjun:
點選去隱祕連結:
不停重新整理頁面,發現存取次數一直累加。
如果未登入時,直接存取localhost:8080/secret
,會直接重定向到登入介面。
上面程式有一個缺點,程式重啟啟動後,就需要重新登入。因為每次啟動我們都重新隨機 hashKey 和 blockKey,只需要固定這兩個值即可實現重啟也能儲存登入狀態。
登入驗證類的功能非常適合放在中介軟體中處理,之前的文章已經介紹過如何編寫中介軟體了,這裡就不贅述了。
將 session 儲存在本地檔案系統,不利於水平擴充套件。一般稍微上點規模的網站,Web 伺服器都會部署很多個範例,請求通過 Nginx 之類的反向代理轉發到一個後端範例處理。不能保證後面的請求與之前的請求在同一個範例中處理,故 session 一般需要儲存在一個公共的地方,例如 redis。
sessions
提供了擴充套件介面,方便擴充套件使用其他的後端儲存 session 內容。目前 GitHub 上已經有很多的第三方後端擴充套件了,詳細 list 見sessions
庫的 GitHub 首頁:
我們只介紹基於 redis 的後端儲存,其他的擴充套件感興趣可自行研究。首先安裝擴充套件:
$ go get gopkg.in/boj/redistore.v1
建立一個 redistore 的範例:
store, _ = redistore.NewRediStore(10, "tcp", ":6379", "", []byte("redis-key"))
引數依次為:
size
:最大空閒連線數;
network
:連線型別,一般是 TCP;
addr
:網路地址+埠;
password
:redis 的密碼,如果未啟用,填空;
keyPairs
:依次是 hashKey 和 blockKey(可省略),不再贅述。
為了驗證,我們開啟多個伺服器,所以將埠通過命令列引數傳入,使用標準庫flag
:
port = flag.Int("port", 8080, "port to listen") func init() { flag.Parse() } func main() { // ... log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil)) }
為了執行伺服器,我們需要先開啟一個 redis-server。redis 的安裝就不多說了,在 windows 下,建議使用 chocolatey 安裝,chocolatey 類似於 Ubutnu 的 apt-get,Mac 的 brew,非常方便,強烈推薦。
為了演示反向代理的效果,即通過一個地址可以隨機存取部署的多個 Web 伺服器,我們開啟 3 個 Web 伺服器。終端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
隨機轉發到mysvr
這個組中的 3 個伺服器上,啟動 nginx:
$ nginx -c nginx.conf
萬事俱備,現在使用瀏覽器存取localhost
,通過控制檯紀錄檔發現是 server3 處理了這個請求:
點選去登入,server1 處理了展示頁面的請求:
點選登入,server3 處理了 POST 型別的登入請求:
登入成功之後,重定向到主介面的請求又是 server1 處理的:
點選私密連結,展示頁面的請求是 server2 處理的:
雖然每次處理的 server 不同,但是登入狀態一直儲存著。因為我們使用了 redis 儲存 session。
注意,我這裡每次都是隨機一個 server 去處理,你執行的結果不一定一樣。
session 為了解決儲存使用者大量資料和安全性的問題。sessions
庫為 Go Web 開發中處理 session 提供了簡單,靈活的方法。它依賴較少,可以隨插即用,非常方便。
大家如果發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45