Introduction
A cookie is a mechanism used to transfer small amounts of data between a Web client (usually a browser) and a server. It is generated by the server and sent to the client for storage. The client will bring the cookie with each subsequent request. Cookies are now somewhat abused. Many companies use cookies to collect user information, place advertisements, and so on.
Cookies have two major disadvantages:
- Each request needs to be transmitted, so it cannot be used to store large amounts of data;
- The security is low, and it is easy to see the cookies set by the website server through browser tools.
gorilla/securecookie
provides a secure cookie. By encrypting the cookie on the server, its content is unreadable and cannot be forged. Of course, it is strongly recommended not to put sensitive information in cookies.
Quick to use
The code in this article uses Go Modules.
Create a directory and initialize:
$ mkdir gorilla/securecookie && cd gorilla/securecookie
$ go mod init github.com/darjun/go-daily-lib/gorilla/securecookie
Install the gorilla/securecookie
library:
$ 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))
}
First, you need to create a SecureCookie
object:
var s = securecookie.New(hashKey, blockKey)
Among them, hashKey
is required. It is used to verify whether the cookie is forged. The bottom layer uses the HMAC (Hash-based message authentication code) algorithm. It is recommended to use a 32/64 byte key for hashKey
blockKey
is optional. It is used to encrypt cookies. If encryption is not required, you can pass nil
. If set, its length must be consistent with the block size of the corresponding encryption algorithm. For example, for the AES series of algorithms, the corresponding block sizes of AES-128/AES-192/AES-256 are 16/24/32 bytes respectively.
For convenience, you can also use the GenerateRandomKey()
function to generate a random key with sufficient security. Each time this function is called, a different key will be returned. The above code creates the key in this way.
Call s.Encode("user", u)
the object u
into a string, which actually uses the standard library encoding/gob
. Therefore, all types supported by gob
Call s.Decode("user", cookie.Value, u)
to decode the cookie value into the corresponding u
object.
run:
$ go run main.go
First use a browser to visit localhost:8080/set_cookie
, then you can see the content of the cookie in the Application tab of Chrome Developer Tools:
Visit localhost:8080/read_cookie
, the page shows name: dj age: 18
.
Use JSON
securecookie
uses encoding/gob
encode the cookie value by encoding/json
instead. securecookie
encapsulates the codec into a Serializer
interface:
type Serializer interface {
Serialize(src interface{}) ([]byte, error)
Deserialize(src []byte, dst interface{}) error
}
securecookie
provides the implementation GobEncoder
and 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
}
We can call securecookie.SetSerializer(JSONEncoder{})
set the JSON encoding:
var (
hashKey = securecookie.GenerateRandomKey(16)
blockKey = securecookie.GenerateRandomKey(16)
s = securecookie.New(hashKey, blockKey)
)
func init() {
s.SetSerializer(securecookie.JSONEncoder{})
}
Custom codec
We can define a type to implement the Serializer
interface, then the object of this type can be used as the codec of securecookie
We implement a simple XML codec:
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{})
}
Because securecookie.cookieError
not exported, XMLEncoder
and GobEncoder/JSONEncoder
are somewhat inconsistent, but it does not affect the use.
Hash/Block function
securecookie
uses sha256.New
as the Hash function (for HMAC algorithm) by aes.NewCipher
, and 0610339ff966ce as the Block function (for encryption and decryption):
// 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
}
You can modify the Hash function securecookie.HashFunc()
func () hash.Hash
type:
func (s *SecureCookie) HashFunc(f func() hash.Hash) *SecureCookie {
s.hashFunc = f
return s
}
Modify the Block function through securecookie.BlockFunc()
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
}
The replacement of these two functions is more due to security considerations. For example, use the more secure sha512
algorithm:
s.HashFunc(sha512.New512_256)
Change Key
In order to prevent the disclosure of cookies from causing security risks, there is a commonly used security strategy: replace the Key regularly. Change the Key to invalidate the previously obtained cookie. Corresponding to the securecookie
library, is to replace the SecureCookie
object:
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),
))
}
When the program starts, we first SecureCookie
objects, and then generate a new object to replace the old one at regular intervals.
Since each request is processed in a separate goroutine (read), the key replacement is also performed in a separate goroutine (write). For concurrency safety, we must increase synchronization measures. But in this case, the use of locks is too heavy, after all, the frequency of updates here is very low. Here I store the securecookie.SecureCookie
object as unsafe.Pointer
, and then I can use the atomic
atomic operation to read and update synchronously:
func rotateKey() {
newcookie := securecookie.New(
securecookie.GenerateRandomKey(64),
securecookie.GenerateRandomKey(32),
)
atomic.StorePointer(&prevCookie, currentCookie)
atomic.StorePointer(¤tCookie, unsafe.Pointer(newcookie))
}
rotateKey()
needs to be called regularly in a new goroutine, we start this goroutine main
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()
}
}
For the convenience of testing, I set it to rotate every 30s. At the same time, in order to prevent goroutine from leaking, we passed in a cancelable Context
. Also note time.NewTicker()
created *time.Ticker
objects need to manually invoke when not in use Stop()
closed, otherwise it will cause resource leaks.
After using two SecureCookie
objects, our codec can call EncodeMulti/DecodeMulti
, they can accept multiple SecureCookie
objects:
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")
}
After using unsafe.Pointer
save the SecureCookie
object, type conversion is required when using it. And due to concurrency issues, you need to use atomic.LoadPointer()
access.
When decoding, call DecodeMulti
pass in currentCookie
and prevCookie
, so that prevCookie
will not be invalid immediately:
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)
}
}
}
Run the program:
$ go run main.go
First request localhost:8080/set_cookie
, and then request localhost:8080/read_cookie
read the cookie. After waiting for 1 minute, I requested again and found that the previous cookie was invalid:
read cookie error:securecookie: the value is not valid (and 1 other error)
Summarize
securecookie
adds a protective cover to the cookie, so that the cookie cannot be easily read and forged. Still need to emphasize:
Do not put sensitive data in cookies! Do not put sensitive data in cookies! Do not put sensitive data in cookies! Do not put sensitive data in cookies!
The important thing is said 4 times. You need to weigh carefully when using cookies to store data.
If you find a fun and easy-to-use Go language library, welcome to submit an issue on the Go Daily Library GitHub😄
refer to
- gorilla/securecookie GitHub:github.com/gorilla/securecookie
- Go a library GitHub every day: https://github.com/darjun/go-daily-lib
I
My blog: https://darjun.github.io
Welcome to follow my WeChat public account [GoUpUp], learn together and make progress together~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。