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 相关文章推荐
win10下go mod配置方式
Apr 25 Golang
golang在GRPC中设置client的超时时间
Apr 27 Golang
基于Go Int转string几种方式性能测试
Apr 28 Golang
Go 实现英尺和米的简单单位换算方式
Apr 29 Golang
使用Golang的channel交叉打印两个数组的操作
Apr 29 Golang
go语言中fallthrough的用法说明
May 06 Golang
如何利用golang运用mysql数据库
Mar 13 Golang
Go语言实现一个简单的并发聊天室的项目实战
Mar 18 Golang
golang使用map实现去除重复数组
Apr 14 Golang
golang用type-switch判断interface的实际存储类型
Apr 14 Golang
Golang map映射的用法
Apr 22 Golang
Golang并发工具Singleflight
May 06 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分页函数
2006/07/08 PHP
php set_time_limit(0) 设置程序执行时间的函数
2010/05/26 PHP
jQuery向下滚动即时加载内容实现的瀑布流效果
2016/01/07 PHP
php中__toString()方法用法示例
2016/12/07 PHP
PHP多维数组排序array详解
2017/11/21 PHP
tp5框架基于ajax实现异步删除图片的方法示例
2020/02/10 PHP
PHPstorm启用自动换行的方法详解(IDE)
2020/09/17 PHP
JavaScript 事件属性绑定带参数的函数
2009/03/13 Javascript
Javascript this指针
2009/07/30 Javascript
js完美的div拖拽实例代码
2014/01/22 Javascript
JavaScript利用正则表达式去除日期中的“-”
2014/07/01 Javascript
jQuery实现tab选项卡效果的方法
2015/07/08 Javascript
JS简单限制textarea内输入字符数量的方法
2015/10/14 Javascript
很酷的星级评分系统原生JS实现
2016/08/25 Javascript
JavaScript中的 attribute 和 jQuery中的 attr 方法浅析
2017/01/04 Javascript
vue 计时器组件的实现代码
2017/09/14 Javascript
浅谈Angular7 项目开发总结
2018/12/19 Javascript
element-ui中Table表格省市区合并单元格的方法实现
2019/08/07 Javascript
在Chrome DevTools中调试JavaScript的实现
2020/04/07 Javascript
[01:29:42]Liquid vs VP Supermajor决赛 BO 第一场 6.10
2018/07/05 DOTA
python批量修改文件后缀示例代码分享
2013/12/24 Python
Python去除字符串前后空格的几种方法
2019/03/04 Python
澳大利亚头发和美容产品购物网站:OZ Hair & Beauty
2020/03/27 全球购物
PHP高级工程师面试问题推荐
2013/01/18 面试题
中软Java笔试题
2012/11/11 面试题
如何利用XMLHTTP检测URL及探测服务器信息
2013/11/10 面试题
技术总监管理岗位职责
2014/03/09 职场文书
终止劳动合同协议书
2014/04/14 职场文书
白岩松演讲
2014/05/21 职场文书
旅游饭店管理专业自荐书
2014/06/28 职场文书
优秀本科毕业生自荐信
2014/07/04 职场文书
感谢信模板大全
2015/01/23 职场文书
因家庭原因离职的辞职信范文
2015/05/12 职场文书
2016年“5.12”护士节致辞
2015/07/31 职场文书
我对PyTorch dataloader里的shuffle=True的理解
2021/05/20 Python
JS class语法糖的深入剖析
2022/07/07 Javascript