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 相关文章推荐
golang http使用踩过的坑与填坑指南
Apr 27 Golang
解决golang post文件时Content-Type出现的问题
May 02 Golang
浅谈golang package中init方法的多处定义及运行顺序问题
May 06 Golang
解决Goland 同一个package中函数互相调用的问题
May 06 Golang
Go 自定义package包设置与导入操作
May 06 Golang
GoLang中生成UUID唯一标识的实现
May 08 Golang
Go timer如何调度
Jun 09 Golang
详解Go语言Slice作为函数参数的使用
Jul 02 Golang
Golang数据类型和相互转换
Apr 12 Golang
Golang 链表的学习和使用
Apr 19 Golang
GoFrame基于性能测试得知grpool使用场景
Jun 21 Golang
Go gorilla securecookie库的安装使用详解
Aug 14 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 破解防盗链图片函数
2008/12/09 PHP
用Json实现PHP与JavaScript间数据交换的方法详解
2013/06/20 PHP
php的XML文件解释类应用实例
2014/09/22 PHP
详解php比较操作符的安全问题
2015/12/03 PHP
thinkPHP分组后模板无法加载问题解决方法
2016/07/12 PHP
JQuery Study Notes 学习笔记(一)
2010/08/04 Javascript
基于jquery实现状态限定编辑的代码
2012/02/11 Javascript
鼠标移入移出事件改变图片的分辨率的两种方法
2013/12/17 Javascript
javascript屏蔽右键代码
2014/05/15 Javascript
浅谈javascript中createElement事件
2014/12/05 Javascript
详解JavaScript中setSeconds()方法的使用
2015/06/11 Javascript
jQuery实现CheckBox全选、全不选功能
2017/01/11 Javascript
jQuery EasyUI ProgressBar进度条组件
2017/02/28 Javascript
Ionic2开发环境搭建教程
2020/08/20 Javascript
jQuery+ajax实现局部刷新的两种方法
2017/06/08 jQuery
Vue-Access-Control 前端用户权限控制解决方案
2017/12/01 Javascript
JS实现点击复选框变更DIV显示状态的示例代码
2017/12/18 Javascript
关于单文件组件.vue的使用
2018/09/20 Javascript
JS实现的小火箭发射动画效果示例
2018/12/08 Javascript
vuex如何重置所有state(可定制)
2019/01/17 Javascript
微信h5静默和非静默授权获取用户openId的方法和步骤
2020/06/08 Javascript
Vue 实现对quill-editor组件中的工具栏添加title
2020/08/03 Javascript
vue+iview使用树形控件的具体使用
2020/11/02 Javascript
[46:43]DOTA2上海特级锦标赛主赛事日 - 1 胜者组第一轮#2LGD VS MVP.Phx第二局
2016/03/02 DOTA
Python实现根据指定端口探测服务器/模块部署的方法
2014/08/25 Python
Python静态类型检查新工具之pyright 使用指南
2019/04/26 Python
Python两台电脑实现TCP通信的方法示例
2019/05/06 Python
pycharm访问mysql数据库的方法步骤
2019/06/18 Python
基于Python生成个性二维码过程详解
2020/03/05 Python
浅谈Keras中shuffle和validation_split的顺序
2020/06/19 Python
如何配置、使用和清除Smarty缓存
2015/12/23 面试题
检讨书范文2000字
2015/01/28 职场文书
导游词300字
2015/02/13 职场文书
2015庆祝七一建党节94周年活动总结
2015/03/20 职场文书
python用字节处理文件实例讲解
2021/04/13 Python
Python实现将多张图片合成MP4视频并加入背景音乐
2022/04/28 Python