Go gorilla securecookie庫的安裝使用詳解
簡介
cookie 是用于在 Web 客戶端(一般是瀏覽器)和服務(wù)器之間傳輸少量數(shù)據(jù)的一種機制。由服務(wù)器生成,發(fā)送到客戶端保存,客戶端后續(xù)的每次請求都會將 cookie 帶上。cookie 現(xiàn)在已經(jīng)被多多少少地濫用了。很多公司使用 cookie 來收集用戶信息、投放廣告等。
cookie 有兩大缺點:
- 每次請求都需要傳輸,故不能用來存放大量數(shù)據(jù);
- 安全性較低,通過瀏覽器工具,很容易看到由網(wǎng)站服務(wù)器設(shè)置的 cookie。
gorilla/securecookie提供了一種安全的 cookie,通過在服務(wù)端給 cookie 加密,讓其內(nèi)容不可讀,也不可偽造。當(dāng)然,敏感信息還是強烈建議不要放在 cookie 中。
快速使用
本文代碼使用 Go Modules。
創(chuàng)建目錄并初始化:
$ mkdir gorilla/securecookie && cd gorilla/securecookie $ go mod init github.com/darjun/go-daily-lib/gorilla/securecookie
安裝gorilla/securecookie庫:
$ go get github.com/gorilla/securecookie
package main
import (
"fmt"
"github.com/gorilla/mux"
"github.com/gorilla/securecookie"
"log"
"net/http"
)
type User struct {
Name string
Age int
}
var (
hashKey = securecookie.GenerateRandomKey(16)
blockKey = securecookie.GenerateRandomKey(16)
s = securecookie.New(hashKey, blockKey)
)
func SetCookieHandler(w http.ResponseWriter, r *http.Request) {
u := &User {
Name: "dj",
Age: 18,
}
if encoded, err := s.Encode("user", u); err == nil {
cookie := &http.Cookie{
Name: "user",
Value: encoded,
Path: "/",
Secure: true,
HttpOnly: true,
}
http.SetCookie(w, cookie)
}
fmt.Fprintln(w, "Hello World")
}
func ReadCookieHandler(w http.ResponseWriter, r *http.Request) {
if cookie, err := r.Cookie("user"); err == nil {
u := &User{}
if err = s.Decode("user", cookie.Value, u); err == nil {
fmt.Fprintf(w, "name:%s age:%d", u.Name, u.Age)
}
}
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/set_cookie", SetCookieHandler)
r.HandleFunc("/read_cookie", ReadCookieHandler)
http.Handle("/", r)
log.Fatal(http.ListenAndServe(":8080", nil))
}首先需要創(chuàng)建一個SecureCookie對象:
var s = securecookie.New(hashKey, blockKey)
其中hashKey是必填的,它用來驗證 cookie 是否是偽造的,底層使用 HMAC(Hash-based message authentication code)算法。推薦hashKey使用 32/64 字節(jié)的 Key。
blockKey是可選的,它用來加密 cookie,如不需要加密,可以傳nil。如果設(shè)置了,它的長度必須與對應(yīng)的加密算法的塊大?。╞lock size)一致。例如對于 AES 系列算法,AES-128/AES-192/AES-256 對應(yīng)的塊大小分別為 16/24/32 字節(jié)。
為了方便也可以使用GenerateRandomKey()函數(shù)生成一個安全性足夠強的隨機 key。每次調(diào)用該函數(shù)都會返回不同的 key。上面代碼就是通過這種方式創(chuàng)建 key 的。
調(diào)用s.Encode("user", u)將對象u編碼成字符串,內(nèi)部實際上使用了標(biāo)準(zhǔn)庫encoding/gob。所以gob支持的類型都可以編碼。
調(diào)用s.Decode("user", cookie.Value, u)將 cookie 值解碼到對應(yīng)的u對象中。
運行:
$ go run main.go
首先使用瀏覽器訪問localhost:8080/set_cookie,這時可以在 Chrome 開發(fā)者工具的 Application 頁簽中看到 cookie 內(nèi)容:

訪問localhost:8080/read_cookie,頁面顯示name: dj age: 18。
使用 JSON
securecookie默認(rèn)使用encoding/gob編碼 cookie 值,我們也可以改用encoding/json。securecookie將編解碼器封裝成一個Serializer接口:
type Serializer interface {
Serialize(src interface{}) ([]byte, error)
Deserialize(src []byte, dst interface{}) error
}securecookie提供了GobEncoder和JSONEncoder的實現(xiàn):
func (e GobEncoder) Serialize(src interface{}) ([]byte, error) {
buf := new(bytes.Buffer)
enc := gob.NewEncoder(buf)
if err := enc.Encode(src); err != nil {
return nil, cookieError{cause: err, typ: usageError}
}
return buf.Bytes(), nil
}
func (e GobEncoder) Deserialize(src []byte, dst interface{}) error {
dec := gob.NewDecoder(bytes.NewBuffer(src))
if err := dec.Decode(dst); err != nil {
return cookieError{cause: err, typ: decodeError}
}
return nil
}
func (e JSONEncoder) Serialize(src interface{}) ([]byte, error) {
buf := new(bytes.Buffer)
enc := json.NewEncoder(buf)
if err := enc.Encode(src); err != nil {
return nil, cookieError{cause: err, typ: usageError}
}
return buf.Bytes(), nil
}
func (e JSONEncoder) Deserialize(src []byte, dst interface{}) error {
dec := json.NewDecoder(bytes.NewReader(src))
if err := dec.Decode(dst); err != nil {
return cookieError{cause: err, typ: decodeError}
}
return nil
}我們可以調(diào)用securecookie.SetSerializer(JSONEncoder{})設(shè)置使用 JSON 編碼:
var (
hashKey = securecookie.GenerateRandomKey(16)
blockKey = securecookie.GenerateRandomKey(16)
s = securecookie.New(hashKey, blockKey)
)
func init() {
s.SetSerializer(securecookie.JSONEncoder{})
}自定義編解碼
我們可以定義一個類型實現(xiàn)Serializer接口,那么該類型的對象可以用作securecookie的編解碼器。我們實現(xiàn)一個簡單的 XML 編解碼器:
package main
type XMLEncoder struct{}
func (x XMLEncoder) Serialize(src interface{}) ([]byte, error) {
buf := &bytes.Buffer{}
encoder := xml.NewEncoder(buf)
if err := encoder.Encode(buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (x XMLEncoder) Deserialize(src []byte, dst interface{}) error {
dec := xml.NewDecoder(bytes.NewBuffer(src))
if err := dec.Decode(dst); err != nil {
return err
}
return nil
}
func init() {
s.SetSerializer(XMLEncoder{})
}由于securecookie.cookieError未導(dǎo)出,XMLEncoder與GobEncoder/JSONEncoder返回的錯誤有些不一致,不過不影響使用。
Hash/Block 函數(shù)
securecookie默認(rèn)使用sha256.New作為 Hash 函數(shù)(用于 HMAC 算法),使用aes.NewCipher作為 Block 函數(shù)(用于加解密):
// securecookie.go
func New(hashKey, blockKey []byte) *SecureCookie {
s := &SecureCookie{
hashKey: hashKey,
blockKey: blockKey,
// 這里設(shè)置 Hash 函數(shù)
hashFunc: sha256.New,
maxAge: 86400 * 30,
maxLength: 4096,
sz: GobEncoder{},
}
if hashKey == nil {
s.err = errHashKeyNotSet
}
if blockKey != nil {
// 這里設(shè)置 Block 函數(shù)
s.BlockFunc(aes.NewCipher)
}
return s
}可以通過securecookie.HashFunc()修改 Hash 函數(shù),傳入一個func () hash.Hash類型:
func (s *SecureCookie) HashFunc(f func() hash.Hash) *SecureCookie {
s.hashFunc = f
return s
}通過securecookie.BlockFunc()修改 Block 函數(shù),傳入一個f func([]byte) (cipher.Block, error):
func (s *SecureCookie) BlockFunc(f func([]byte) (cipher.Block, error)) *SecureCookie {
if s.blockKey == nil {
s.err = errBlockKeyNotSet
} else if block, err := f(s.blockKey); err == nil {
s.block = block
} else {
s.err = cookieError{cause: err, typ: usageError}
}
return s
}更換這兩個函數(shù)更多的是處于安全性的考慮,例如選用更安全的sha512算法:
s.HashFunc(sha512.New512_256)
更換 Key
為了防止 cookie 泄露造成安全風(fēng)險,有個常用的安全策略:定期更換 Key。更換 Key,讓之前獲得的 cookie 失效。對應(yīng)securecookie庫,就是更換SecureCookie對象:
var (
prevCookie unsafe.Pointer
currentCookie unsafe.Pointer
)
func init() {
prevCookie = unsafe.Pointer(securecookie.New(
securecookie.GenerateRandomKey(64),
securecookie.GenerateRandomKey(32),
))
currentCookie = unsafe.Pointer(securecookie.New(
securecookie.GenerateRandomKey(64),
securecookie.GenerateRandomKey(32),
))
}程序啟動時,我們先生成兩個SecureCookie對象,然后每隔一段時間就生成一個新的對象替換舊的。
由于每個請求都是在一個獨立的 goroutine 中處理的(讀),更換 key 也是在一個單獨的 goroutine(寫)。為了并發(fā)安全,我們必須增加同步措施。但是這種情況下使用鎖又太重了,畢竟這里更新的頻率很低。
我這里將securecookie.SecureCookie對象存儲為unsafe.Pointer類型,然后就可以使用atomic原子操作來同步讀取和更新了:
func rotateKey() {
newcookie := securecookie.New(
securecookie.GenerateRandomKey(64),
securecookie.GenerateRandomKey(32),
)
atomic.StorePointer(&prevCookie, currentCookie)
atomic.StorePointer(&currentCookie, unsafe.Pointer(newcookie))
}rotateKey()需要在一個新的 goroutine 中定期調(diào)用,我們在main函數(shù)中啟動這個 goroutine
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go RotateKey(ctx)
}
func RotateKey(ctx context.Context) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
break
case <-ticker.C:
}
rotateKey()
}
}這里為了方便測試,我設(shè)置每隔 30s 就輪換一次。同時為了防止 goroutine 泄漏,我們傳入了一個可取消的Context。還需要注意time.NewTicker()創(chuàng)建的*time.Ticker對象不使用時需要手動調(diào)用Stop()關(guān)閉,否則會造成資源泄漏。
使用兩個SecureCookie對象之后,我們編解碼可以調(diào)用EncodeMulti/DecodeMulti這組方法,它們可以接受多個SecureCookie對象:
func SetCookieHandler(w http.ResponseWriter, r *http.Request) {
u := &User{
Name: "dj",
Age: 18,
}
if encoded, err := securecookie.EncodeMulti(
"user", u,
// 看這里 ??
(*securecookie.SecureCookie)(atomic.LoadPointer(¤tCookie)),
); err == nil {
cookie := &http.Cookie{
Name: "user",
Value: encoded,
Path: "/",
Secure: true,
HttpOnly: true,
}
http.SetCookie(w, cookie)
}
fmt.Fprintln(w, "Hello World")
}使用unsafe.Pointer保存SecureCookie對象后,使用時需要類型轉(zhuǎn)換。并且由于并發(fā)問題,需要使用atomic.LoadPointer()訪問。
解碼時調(diào)用DecodeMulti依次傳入currentCookie和prevCookie,讓prevCookie不會立刻失效:
func ReadCookieHandler(w http.ResponseWriter, r *http.Request) {
if cookie, err := r.Cookie("user"); err == nil {
u := &User{}
if err = securecookie.DecodeMulti(
"user", cookie.Value, u,
// 看這里 ??
(*securecookie.SecureCookie)(atomic.LoadPointer(¤tCookie)),
(*securecookie.SecureCookie)(atomic.LoadPointer(&prevCookie)),
); err == nil {
fmt.Fprintf(w, "name:%s age:%d", u.Name, u.Age)
} else {
fmt.Fprintf(w, "read cookie error:%v", err)
}
}
}運行程序:
$ go run main.go
先請求localhost:8080/set_cookie,然后請求localhost:8080/read_cookie讀取 cookie。等待 1 分鐘后,再次請求,發(fā)現(xiàn)之前的 cookie 失效了:
read cookie error:securecookie: the value is not valid (and 1 other error)
總結(jié)
securecookie為 cookie 添加了一層保護(hù)罩,讓 cookie 不能輕易地被讀取和偽造。還是需要強調(diào)一下:
敏感數(shù)據(jù)不要放在 cookie 中!敏感數(shù)據(jù)不要放在 cookie 中!敏感數(shù)據(jù)不要放在 cookie 中!敏感數(shù)據(jù)不要放在 cookie 中!
重要的事情說 4 遍。在使用 cookie 存放數(shù)據(jù)時需要仔細(xì)權(quán)衡。
大家如果發(fā)現(xiàn)好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue??
參考
gorilla/securecookie GitHub:github.com/gorilla/securecookie
Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib
以上就是Go gorilla securecookie庫的安裝使用詳解的詳細(xì)內(nèi)容,更多關(guān)于Go gorilla securecookie庫的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go gorilla securecookie庫的安裝使用詳解
這篇文章主要介紹了Go gorilla securecookie庫的安裝使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
golang高并發(fā)限流操作 ping / telnet
這篇文章主要介紹了golang高并發(fā)限流操作 ping / telnet,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12
golang進(jìn)程內(nèi)存控制避免docker內(nèi)oom
這篇文章主要為大家介紹了golang進(jìn)程內(nèi)存控制避免docker內(nèi)oom示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10
Golang的select多路復(fù)用及channel使用操作
這篇文章主要介紹了Golang的select多路復(fù)用及channel使用操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12

