<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
cookie 是用於在 Web 使用者端(一般是瀏覽器)和伺服器之間傳輸少量資料的一種機制。由伺服器生成,傳送到使用者端儲存,使用者端後續的每次請求都會將 cookie 帶上。cookie 現在已經被多多少少地濫用了。很多公司使用 cookie 來收集使用者資訊、投放廣告等。
cookie 有兩大缺點:
gorilla/securecookie提供了一種安全的 cookie,通過在伺服器端給 cookie 加密,讓其內容不可讀,也不可偽造。當然,敏感資訊還是強烈建議不要放在 cookie 中。
本文程式碼使用 Go Modules。
建立目錄並初始化:
$ 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)) }
首先需要建立一個SecureCookie
物件:
var s = securecookie.New(hashKey, blockKey)
其中hashKey
是必填的,它用來驗證 cookie 是否是偽造的,底層使用 HMAC(Hash-based message authentication code)演演算法。推薦hashKey
使用 32/64 位元組的 Key。
blockKey
是可選的,它用來加密 cookie,如不需要加密,可以傳nil
。如果設定了,它的長度必須與對應的加密演演算法的塊大小(block size)一致。例如對於 AES 系列演演算法,AES-128/AES-192/AES-256 對應的塊大小分別為 16/24/32 位元組。
為了方便也可以使用GenerateRandomKey()
函數生成一個安全性足夠強的隨機 key。每次呼叫該函數都會返回不同的 key。上面程式碼就是通過這種方式建立 key 的。
呼叫s.Encode("user", u)
將物件u
編碼成字串,內部實際上使用了標準庫encoding/gob
。所以gob
支援的型別都可以編碼。
呼叫s.Decode("user", cookie.Value, u)
將 cookie 值解碼到對應的u
物件中。
執行:
$ go run main.go
首先使用瀏覽器存取localhost:8080/set_cookie
,這時可以在 Chrome 開發者工具的 Application 頁籤中看到 cookie 內容:
存取localhost:8080/read_cookie
,頁面顯示name: dj age: 18
。
securecookie
預設使用encoding/gob
編碼 cookie 值,我們也可以改用encoding/json
。securecookie
將編解碼器封裝成一個Serializer
介面:
type Serializer interface { Serialize(src interface{}) ([]byte, error) Deserialize(src []byte, dst interface{}) error }
securecookie
提供了GobEncoder
和JSONEncoder
的實現:
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 }
我們可以呼叫securecookie.SetSerializer(JSONEncoder{})
設定使用 JSON 編碼:
var ( hashKey = securecookie.GenerateRandomKey(16) blockKey = securecookie.GenerateRandomKey(16) s = securecookie.New(hashKey, blockKey) ) func init() { s.SetSerializer(securecookie.JSONEncoder{}) }
我們可以定義一個型別實現Serializer
介面,那麼該型別的物件可以用作securecookie
的編解碼器。我們實現一個簡單的 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
未匯出,XMLEncoder
與GobEncoder/JSONEncoder
返回的錯誤有些不一致,不過不影響使用。
securecookie
預設使用sha256.New
作為 Hash 函數(用於 HMAC 演演算法),使用aes.NewCipher
作為 Block 函數(用於加解密):
// securecookie.go func New(hashKey, blockKey []byte) *SecureCookie { s := &SecureCookie{ hashKey: hashKey, blockKey: blockKey, // 這裡設定 Hash 函數 hashFunc: sha256.New, maxAge: 86400 * 30, maxLength: 4096, sz: GobEncoder{}, } if hashKey == nil { s.err = errHashKeyNotSet } if blockKey != nil { // 這裡設定 Block 函數 s.BlockFunc(aes.NewCipher) } return s }
可以通過securecookie.HashFunc()
修改 Hash 函數,傳入一個func () hash.Hash
型別:
func (s *SecureCookie) HashFunc(f func() hash.Hash) *SecureCookie { s.hashFunc = f return s }
通過securecookie.BlockFunc()
修改 Block 函數,傳入一個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 }
更換這兩個函數更多的是處於安全性的考慮,例如選用更安全的sha512
演演算法:
s.HashFunc(sha512.New512_256)
為了防止 cookie 洩露造成安全風險,有個常用的安全策略:定期更換 Key。更換 Key,讓之前獲得的 cookie 失效。對應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(寫)。為了並行安全,我們必須增加同步措施。但是這種情況下使用鎖又太重了,畢竟這裡更新的頻率很低。
我這裡將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 中定期呼叫,我們在main
函數中啟動這個 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() } }
這裡為了方便測試,我設定每隔 30s 就輪換一次。同時為了防止 goroutine 洩漏,我們傳入了一個可取消的Context
。還需要注意time.NewTicker()
建立的*time.Ticker
物件不使用時需要手動呼叫Stop()
關閉,否則會造成資源洩漏。
使用兩個SecureCookie
物件之後,我們編解碼可以呼叫EncodeMulti/DecodeMulti
這組方法,它們可以接受多個SecureCookie
物件:
func SetCookieHandler(w http.ResponseWriter, r *http.Request) { u := &User{ Name: "dj", Age: 18, } if encoded, err := securecookie.EncodeMulti( "user", u, // 看這裡
相關文章
<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