Go gorilla/sessions库安装使用


Posted in Golang onAugust 14, 2022

简介

上一篇文章《Go 每日一库之 securecookie》中,我们介绍了 cookie。同时提到 cookie 有两个缺点,一是数据不宜过大,二是安全问题。session 是服务器端的存储方案,可以存储大量的数据,而且不需要向客户端传输,从而解决了这两个问题。

但是 session 需要一个能唯一标识用户的 ID,这个 ID 一般存放在 cookie 中发送到客户端保存,随每次请求一起发送到服务器。cookie 和 session 通常配套使用。

gorilla/sessions是 gorilla web 开发工具包中管理 session 的库。它提供了基于 cookie 和本地文件系统的 session。同时预留扩展接口,可以使用其它的后端存储 session 数据。

本文先介绍sessions提供的两种 session 存储方式,然后通过第三方扩展介绍在多个 Web 服务器实例间如何保持登录状态。

快速使用

本文代码使用 Go Modules。

创建目录并初始化:

$ mkdir gorilla/sessions && cd gorilla/sessions
$ go mod init github.com/darjun/go-daily-lib/gorilla/sessions

安装gorilla/sessions库:

$ go get -u github.com/valyala/gorilla/sessions

现在我们实现在服务器端通过 session 存储一些信息的功能:

package main
import (
  "fmt"
  "github.com/gorilla/mux"
  "github.com/gorilla/sessions"
  "log"
  "net/http"
  "os"
)
var (
  store = sessions.NewFilesystemStore("./", securecookie.GenerateRandomKey(32), securecookie.GenerateRandomKey(32))
)
func set(w http.ResponseWriter, r *http.Request) {
  session, _ := store.Get(r, "user")
  session.Values["name"] = "dj"
  session.Values["age"] = 18
  err := sessions.Save(r, w)
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
  }
  fmt.Fprintln(w, "Hello World")
}
func read(w http.ResponseWriter, r *http.Request) {
  session, _ := store.Get(r, "user")
  fmt.Fprintf(w, "name:%s age:%d\n", session.Values["name"], session.Values["age"])
}
func main() {
  r := mux.NewRouter()
  r.HandleFunc("/set", set)
  r.HandleFunc("/read", read)
  log.Fatal(http.ListenAndServe(":8080", r))
}

整个程序逻辑比较清晰,分别在/set/read路径下挂上设置和读取的处理函数。重点是变量store。我们调用session.NewFilesystemStore()方法创建了一个*sessions.FilesystemStore类型的对象,它会将我们的 session 内容存储到文件系统(即本地磁盘上)。我们需要给NewFilesytemStore()方法传入至少 2 个参数,第一个参数指定 session 存储的本地磁盘路径。后续参数依次指定hashKeyblockKey(可省略),前者用于验证,后者用于加密,我们可以使用securecookie生成足够随机的 key,详情见前一篇介绍securecookie的文章。

sessions为所有的 session 存储抽象了一个接口Store

type Store interface {
  Get(r *http.Request, name string) (*Session, error)
  New(r *http.Request, name string) (*Session, error)
  Save(r *http.Request, w http.ResponseWriter, s *Session) error
}

实现这个接口可以自定义我们存储 session 的位置和格式。

set处理函数中,我们调用store.Get(r, "user")获取名为user的 session,如果 session 不存在,则创建一个新的。sessions库支持为同一个用户创建多个 session,store.Get()方法的第二个参数指定名字。获取到的*Session结构如下:

type Session struct {
  ID string
  Values  map[interface{}]interface{}
  Options *Options
  IsNew   bool
  store   Store
  name    string
}

数据直接存放在Session.Values字段中,这是一个类型为map[interface{}]interface{}的字段,几乎能保存任何类型的数据(之所以我这里要说几乎,因为还要考虑序列化到存储的限制,有些数据类型无法序列化为字节流保存,如chan)。

set处理函数中,我们直接操作Values字段,最后我们调用store.Save(r, w, session)将 session数据保存到对应的存储中。

get处理函数中,同样地我们先调用store.Get(r, "user")获取*Session对象,然后读取里面的nameage值。

运行:

$ go run main.go

首先访问localhost:8080/set,通过浏览器的开发者工具Application页签查看 cookie:

Go gorilla/sessions库安装使用

我们发现 session 的名字会作为 cookie 名发送到客户端,session ID 被保存为 cookie 的值。

然后我们访问localhost:8080/read,读取到 session 保存的数据:

Go gorilla/sessions库安装使用

另前面说过FilesystemStore数据是存储在本地硬盘上的,在运行程序的本地目录我们看到有以 session 开头的文件,文件名 session 后面的部分就是 session ID:

Go gorilla/sessions库安装使用

cookie 存储

除了默认的将本地文件系统作为存储外,sessions还支持将 cookie 作为存储,也就是将 session 的数据直接通过 cookie 在客户端和服务器之间传输。cookie 存储的创建方式与文件系统存储的创建方式类似:

var store = sessions.NewCookieStore(securecookie.GenerateRandomKey(32), securecookie.GenerateRandomKey(32))

sessions.NewCookieStore()方法的第一个参数为 hashKey 用于验证,第二个参数为 blockKey 用于加密,与sessions.NewFilesystemStore()一样。

其他部分的代码完全不用修改,运行程序的结果与上面的一致。session 数据保存在 cookie 中,随每次请求由客户端传给服务器。这种方式其实就是之前文章中介绍的 cookie 用法。

记录登录状态

之前我们介绍gorilla/mux时介绍过使用 cookie 保存登录状态。当时将用户名和密码经过简单的 Base64 编码后就直接存放在 cookie 中了,基本处于“裸露”状态。只要有意,很容易就能窃取用户名和密码。现在我们将用户关键信息存储在 session 中,cookie 中只存储一个 session ID。

首先,我们设计 3 个页面,登录页面,主页面,授权才能访问的 secret 页面。登录页面只需要用户名&密码的输入框和登录按钮即可:

// login.tpl
<form action="/login" method="post">
  <label>Username:</label>
  <input name="username"><br>
  <label>Password:</label>
  <input name="password" type="password"><br>
  <button type="submit">登录</button>
</form>

登录请求根据方法不同需要执行不同的操作,GET 方法表示请求登录的页面,POST 方法表示执行登录操作。我们使用handlers.MethodHandler这个中间件来处理同一个路径的不同方法的请求:

r.Handle("/login", handlers.MethodHandler{
  "GET":  http.HandlerFunc(Login),
  "POST": http.HandlerFunc(DoLogin),
})

Login处理函数很简单,只是展示页面:

func Login(w http.ResponseWriter, r *http.Request) {
  ptTemplate.ExecuteTemplate(w, "login.tpl", nil)
}

这里我使用 Go 标准库html/template模版库来加载和管理各个页面的模板:

var (
  ptTemplate *template.Template
)
func init() {
  template.Must(template.New("").ParseGlob("./tpls/*.tpl"))
}

DoLogin处理函数,需要验证登录请求,然后创建User对象,保存在 session 中,接着重定向到主页面:

func DoLogin(w http.ResponseWriter, r *http.Request) {
  r.ParseForm()
  username := r.Form.Get("username")
  password := r.Form.Get("password")
  if username != "darjun" || password != "handsome" {
    http.Redirect(w, r, "/login", http.StatusFound)
    return
  }
  SaveSessionUser(w, r, &User{Username: username})
  http.Redirect(w, r, "/", http.StatusFound)
}

下面是主页面的处理,我们可以从 session 中取出保存的User对象,根据是否有User对象显示不同的页面:

// home.tpl
{% if . %}
<p>Hi, {% .Username %}</p><br>
<a href="/secret" rel="external nofollow" >Goto secret?</a>
{% else %}
<p>Hi, stranger</p><br>
<a href="/login" rel="external nofollow" >Goto login?</a>
{% end %}

HomeHandler代码如下:

func HomeHandler(w http.ResponseWriter, r *http.Request) {
  u := GetSessionUser(r)
  ptTemplate.ExecuteTemplate(w, "home.tpl", u)
}

最后是 secret 页面:

// secret.tpl
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit.
Inventore a cumque sunt pariatur nihil doloremque tempore,
consectetur ipsum sapiente id excepturi enim velit,
quis nisi esse doloribus aliquid. Incidunt, dolore.
</p>
<p>You have visited this page {% .Count %} times.</p>

显示访问了该页面多少次。

SecretHandler如下:

func SecretHandler(w http.ResponseWriter, r *http.Request) {
  u := GetSessionUser(r)
  if u == nil {
    http.Redirect(w, r, "/login", http.StatusFound)
    return
  }
  u.Count++
  SaveSessionUser(w, r, u)
  ptTemplate.ExecuteTemplate(w, "secret.tpl", u)
}

如果没有 session,则重定向到登录页面。反之显示该页面。这里每次成功访问 secret 页面,都会增加计数器,保存在 session 中。

上面代码中需要注意一点,由于 session 内容的序列化使用了标准库中的encoding/gob,所以不支持直接序列化结构体,我封装了两个函数,将User对象序列化为 JSON,然后保存到 session 中和从 session 中取出字符串反序列化为User对象:

func GetSessionUser(r *http.Request) *User {
  session, _ := store.Get(r, "user")
  s, ok := session.Values["user"]
  if !ok {
    return nil
  }
  u := &User{}
  json.Unmarshal([]byte(s.(string)), u)
  return u
}
func SaveSessionUser(w http.ResponseWriter, r *http.Request, u *User) {
  session, _ := store.Get(r, "user")
  data, _ := json.Marshal(u)
  session.Values["user"] = string(data)
  store.Save(r, w, session)
}

现在运行我们的程序,首先访问localhost:8080,由于没有登录,显示欢迎陌生人,去登录:

Go gorilla/sessions库安装使用

点击去登录,跳转到登录界面,输入用户名和密码:

Go gorilla/sessions库安装使用

点击登录,跳转到主页,这时由于记录了登录状态,会显示欢迎 darjun:

Go gorilla/sessions库安装使用

点击去隐秘链接:

Go gorilla/sessions库安装使用

不停刷新页面,发现访问次数一直累加。

如果未登录时,直接访问localhost:8080/secret,会直接重定向到登录界面。

上面程序有一个缺点,程序重启启动后,就需要重新登录。因为每次启动我们都重新随机 hashKey 和 blockKey,只需要固定这两个值即可实现重启也能保存登录状态。

登录验证类的功能非常适合放在中间件中处理,之前的文章已经介绍过如何编写中间件了,这里就不赘述了。

第三方后端存储

将 session 存储在本地文件系统,不利于水平扩展。一般稍微上点规模的网站,Web 服务器都会部署很多个实例,请求通过 Nginx 之类的反向代理转发到一个后端实例处理。不能保证后面的请求与之前的请求在同一个实例中处理,故 session 一般需要存储在一个公共的地方,例如 redis。

sessions提供了扩展接口,方便扩展使用其他的后端存储 session 内容。目前 GitHub 上已经有很多的第三方后端扩展了,详细 list 见sessions库的 GitHub 首页:

Go gorilla/sessions库安装使用

我们只介绍基于 redis 的后端存储,其他的扩展感兴趣可自行研究。首先安装扩展:

$ go get gopkg.in/boj/redistore.v1

创建一个 redistore 的实例:

store, _ = redistore.NewRediStore(10, "tcp", ":6379", "", []byte("redis-key"))

参数依次为:

size:最大空闲连接数;

network:连接类型,一般是 TCP;

addr:网络地址+端口;

password:redis 的密码,如果未启用,填空;

keyPairs:依次是 hashKey 和 blockKey(可省略),不再赘述。

为了验证,我们开启多个服务器,所以将端口通过命令行参数传入,使用标准库flag

port = flag.Int("port", 8080, "port to listen")
func init() {
  flag.Parse()
}
func main() {
  // ...
  log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
}

为了运行服务器,我们需要先开启一个 redis-server。redis 的安装就不多说了,在 windows 下,建议使用 chocolatey 安装,chocolatey 类似于 Ubutnu 的 apt-get,Mac 的 brew,非常方便,强烈推荐。

Go gorilla/sessions库安装使用

为了演示反向代理的效果,即通过一个地址可以随机访问部署的多个 Web 服务器,我们开启 3 个 Web 服务器。终端1:

$ go build 
$ ./redis -port 8080

终端2:

$ ./redis -port 8081

终端3:

$ ./redis -port 8082

可以使用nginx做反向代理,安装 nginx,配置:

upstream mysvr {
  server localhost:8080;
  server localhost:8081;
  server localhost:8082;
}
server {
  listen       80;
  server_name  localhost;
  location / {
    proxy_pass http://mysvr;
  }
}

这里表示将localhost随机转发到mysvr这个组中的 3 个服务器上,启动 nginx:

$ nginx -c nginx.conf

万事俱备,现在使用浏览器访问localhost,通过控制台日志发现是 server3 处理了这个请求:

Go gorilla/sessions库安装使用

点击去登录,server1 处理了展示页面的请求:

Go gorilla/sessions库安装使用

点击登录,server3 处理了 POST 类型的登录请求:

Go gorilla/sessions库安装使用

登录成功之后,重定向到主界面的请求又是 server1 处理的:

Go gorilla/sessions库安装使用

点击私密链接,展示页面的请求是 server2 处理的:

Go gorilla/sessions库安装使用

虽然每次处理的 server 不同,但是登录状态一直保存着。因为我们使用了 redis 保存 session。

注意,我这里每次都是随机一个 server 去处理,你运行的结果不一定一样。

总结

session 为了解决存储用户大量数据和安全性的问题。sessions库为 Go Web 开发中处理 session 提供了简单,灵活的方法。它依赖较少,可以即插即用,非常方便。

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

参考

gorilla/sessions GitHub:github.com/gorilla/sessions

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

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

Golang 相关文章推荐
go原生库的中bytes.Buffer用法
Apr 25 Golang
golang正则之命名分组方式
Apr 25 Golang
Golang 编译成DLL文件的操作
May 06 Golang
解决golang 关于全局变量的坑
May 06 Golang
Golang 获取文件md5校验的方法以及效率对比
May 08 Golang
详解Go语言Slice作为函数参数的使用
Jul 02 Golang
Golang流模式之grpc的四种数据流
Apr 13 Golang
Go语言 详解net的tcp服务
Apr 14 Golang
Golang 字符串的常见操作
Apr 19 Golang
Golang 遍历二叉树
Apr 19 Golang
Golang 结构体数据集合
Apr 22 Golang
Go微服务项目配置文件的定义和读取示例详解
Jun 21 Golang
Go gorilla securecookie库的安装使用详解
Aug 14 #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
You might like
php异常:Parse error: syntax error, unexpected T_ENCAPSED_AND_WHITESPACE  eval()'d code error
2011/05/19 PHP
浅谈Laravel队列实现原理解决问题记录
2017/08/19 PHP
PHP中散列密码的安全性分析
2019/07/26 PHP
jQuery动画效果-slideUp slideDown上下滑动示例代码
2013/08/28 Javascript
extjs表格文本启用选择复制功能具体实现
2013/10/11 Javascript
jquery中的过滤操作详细解析
2013/12/02 Javascript
jQuery.parseJSON(json)将JSON字符串转换成js对象
2014/07/27 Javascript
理解JavaScript中worker事件api
2015/12/25 Javascript
原生JS封装ajax 传json,str,excel文件上传提交表单(推荐)
2016/06/21 Javascript
Angular1.x复杂指令实例详解
2017/03/01 Javascript
javascript函数的节流[throttle]与防抖[debounce]
2017/11/15 Javascript
基于vue 实现token验证的实例代码
2017/12/14 Javascript
全面介绍vue 全家桶和项目实例
2017/12/27 Javascript
解决微信浏览器缓存站点入口文件(IIS部署Vue项目)
2019/06/17 Javascript
ES6 proxy和reflect的使用方法与应用实例分析
2020/02/15 Javascript
[05:08]第一届“网鱼杯”DOTA2比赛精彩集锦
2014/09/05 DOTA
python抓取豆瓣图片并自动保存示例学习
2014/01/10 Python
Python数据结构与算法之常见的分配排序法示例【桶排序与基数排序】
2017/12/15 Python
Python实现简易Web爬虫详解
2018/01/03 Python
numpy基础教程之np.linalg
2019/02/12 Python
python 字段拆分详解
2019/12/17 Python
如何用tempfile库创建python进程中的临时文件
2021/01/28 Python
matplotlib grid()设置网格线外观的实现
2021/02/22 Python
详解css3自定义滚动条样式写法
2017/12/25 HTML / CSS
党员学习正风肃纪思想汇报
2014/09/12 职场文书
2015年世界环境日活动总结
2015/02/11 职场文书
医生个人年终总结
2015/02/28 职场文书
2015年政风行风工作总结
2015/04/21 职场文书
辩护词范文大全
2015/05/21 职场文书
2015年英语教研组工作总结
2015/05/23 职场文书
写给医护人员的一封感谢信
2019/09/16 职场文书
在Python中如何使用yield
2021/06/07 Python
Python中的套接字编程是什么?
2021/06/21 Python
Nginx+Windows搭建域名访问环境的操作方法
2022/03/17 Servers
Python实现将多张图片合成MP4视频并加入背景音乐
2022/04/28 Python
python实现双向链表原理
2022/05/25 Python