Go gorilla securecookie库的安装使用详解


Posted in Golang onAugust 14, 2022

简介

cookie 是用于在 Web 客户端(一般是浏览器)和服务器之间传输少量数据的一种机制。由服务器生成,发送到客户端保存,客户端后续的每次请求都会将 cookie 带上。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 内容:

Go gorilla securecookie库的安装使用详解

访问localhost:8080/read_cookie,页面显示name: dj age: 18

使用 JSON

securecookie默认使用encoding/gob编码 cookie 值,我们也可以改用encoding/jsonsecurecookie将编解码器封装成一个Serializer接口:

type Serializer interface {
  Serialize(src interface{}) ([]byte, error)
  Deserialize(src []byte, dst interface{}) error
}

securecookie提供了GobEncoderJSONEncoder的实现:

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未导出,XMLEncoderGobEncoder/JSONEncoder返回的错误有些不一致,不过不影响使用。

Hash/Block 函数

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)

更换 Key

为了防止 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,
    // 看这里 ?
    (*securecookie.SecureCookie)(atomic.LoadPointer(&currentCookie)),
  ); 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对象后,使用时需要类型转换。并且由于并发问题,需要使用atomic.LoadPointer()访问。

解码时调用DecodeMulti依次传入currentCookieprevCookie,让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(&currentCookie)),
      (*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 分钟后,再次请求,发现之前的 cookie 失效了:

read cookie error:securecookie: the value is not valid (and 1 other error)

总结

securecookie为 cookie 添加了一层保护罩,让 cookie 不能轻易地被读取和伪造。还是需要强调一下:

敏感数据不要放在 cookie 中!敏感数据不要放在 cookie 中!敏感数据不要放在 cookie 中!敏感数据不要放在 cookie 中!

重要的事情说 4 遍。在使用 cookie 存放数据时需要仔细权衡。

大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue?

参考

gorilla/securecookie GitHub:github.com/gorilla/securecookie

Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib

以上就是Go gorilla securecookie库的安装使用详解的详细内容,更多关于Go gorilla securecookie库的资料请关注三水点靠木其它相关文章!

Golang 相关文章推荐
golang中切片copy复制和等号复制的区别介绍
Apr 27 Golang
golang 实现Location跳转方式
May 02 Golang
goland设置颜色和字体的操作
May 05 Golang
golang elasticsearch Client的使用详解
May 05 Golang
golang 定时任务方面time.Sleep和time.Tick的优劣对比分析
May 05 Golang
go设置多个GOPATH的方式
May 05 Golang
Golang 实现获取当前函数名称和文件行号等操作
May 08 Golang
试了下Golang实现try catch的方法
Jul 01 Golang
Golang使用Panic与Recover进行错误捕获
Mar 22 Golang
Golang 对es的操作实例
Apr 20 Golang
实现GO语言对数组切片去重
Apr 20 Golang
GoFrame框架数据校验之校验结果Error接口对象
Jun 21 Golang
go goth封装第三方认证库示例详解
Aug 14 #Golang
基于Python实现西西成语接龙小助手
Aug 05 #Golang
Python测试框架pytest核心库pluggy详解
Aug 05 #Golang
Go结合Gin导出Mysql数据到Excel表格
Aug 05 #Golang
GO中sync包自由控制并发示例详解
Aug 05 #Golang
Go语言编译原理之源码调试
Aug 05 #Golang
Go语言编译原理之变量捕获
Aug 05 #Golang
You might like
PHP中通过HTTP_USER_AGENT判断是否为手机移动终端的函数代码
2013/02/14 PHP
探讨fckeditor在Php中的配置详解
2013/06/08 PHP
PHP YII框架开发小技巧之模型(models)中rules自定义验证规则
2015/11/16 PHP
ThinkPHP模板标签eq if 中区分0,null,false的方法
2017/03/24 PHP
Save a File Using a File Save Dialog Box
2007/06/18 Javascript
对setInterval在火狐和chrome切换标签产生奇怪的效果之探索,与解决方案!
2011/10/29 Javascript
关于js内存泄露的一个好例子
2013/12/09 Javascript
js处理php输出时间戳对不上号的解决方法
2014/06/20 Javascript
微信小程序 购物车简单实例
2016/10/24 Javascript
mui上拉加载功能实例详解
2017/04/13 Javascript
详解Vue2.X的路由管理记录之 钩子函数(切割流水线)
2017/05/02 Javascript
javascript获取指定区间范围随机数的方法
2017/09/08 Javascript
Vue组件开发之LeanCloud带图形校验码的短信发送功能
2017/11/07 Javascript
基于jQuery实现的设置文本区域的光标位置
2018/06/15 jQuery
nodejs 十六进制字符串型数据与btye型数据相互转换
2018/07/30 NodeJs
Vue.js实现备忘录功能
2019/06/26 Javascript
微信小程序实现Swiper轮播图效果
2019/11/22 Javascript
ES6实现图片切换特效代码
2020/01/14 Javascript
详解三种方式在React中解决绑定this的作用域问题并传参
2020/08/18 Javascript
uniapp微信小程序实现一个页面多个倒计时
2020/11/01 Javascript
深入Python函数编程的一些特性
2015/04/13 Python
Python实现简单的文件传输与MySQL备份的脚本分享
2016/01/03 Python
mysql 之通过配置文件链接数据库
2017/08/12 Python
Python安装Numpy和matplotlib的方法(推荐)
2017/11/02 Python
Python实现的根据文件名查找数据文件功能示例
2018/05/02 Python
Python读取excel指定列生成指定sql脚本的方法
2018/11/28 Python
python使用正则表达式(Regular Expression)方法超详细
2019/12/30 Python
编译 pycaffe时报错:fatal error: numpy/arrayobject.h没有那个文件或目录
2020/11/29 Python
css3实现图片遮罩效果鼠标hover以后出现文字
2013/11/05 HTML / CSS
英国建筑用品在线:Building Supplies Online(BSO)
2018/04/30 全球购物
介绍一下.NET构架下remoting和webservice
2014/05/08 面试题
给民警的表扬信
2014/01/08 职场文书
C++程序员求职信范文
2014/04/14 职场文书
生日赠语
2015/06/23 职场文书
2015团员个人年度总结
2015/11/24 职场文书
商业计划书格式、范文
2019/03/21 职场文书