关于godailylib:Go-每日一库之-gorillasessions

12次阅读

共计 7657 个字符,预计需要花费 20 分钟才能阅读完成。

简介

上一篇文章《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:

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

而后咱们拜访localhost:8080/read,读取到 session 保留的数据:

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

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">Goto secret?</a>
{% else %}
<p>Hi, stranger</p><br>
<a href="/login">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,因为没有登录,显示 欢送陌生人,去登录

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

点击登录,跳转到主页,这时因为记录了登录状态,会显示 欢送 darjun

点击去隐秘链接:

不停刷新页面,发现拜访次数始终累加。

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

下面程序有一个毛病,程序重启启动后,就须要从新登录。因为每次启动咱们都从新随机 hashKey 和 blockKey,只须要固定这两个值即可实现重启也能保留登录状态。

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

第三方后端存储

将 session 存储在本地文件系统,不利于程度扩大。个别略微上点规模的网站,Web 服务器都会部署很多个实例,申请通过 Nginx 之类的反向代理转发到一个后端实例解决。不能保障前面的申请与之前的申请在同一个实例中解决,故 session 个别须要存储在一个公共的中央,例如 redis。

sessions提供了扩大接口,不便扩大应用其余的后端存储 session 内容。目前 GitHub 上曾经有很多的第三方后端扩大了,具体 list 见 sessions 库的 GitHub 首页:

咱们只介绍基于 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,十分不便,强烈推荐。

为了演示反向代理的成果,即通过一个地址能够随机拜访部署的多个 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 解决了这个申请:

点击去登录,server1 解决了展现页面的申请:

点击登录,server3 解决了 POST 类型的登录申请:

登录胜利之后,重定向到主界面的申请又是 server1 解决的:

点击私密链接,展现页面的申请是 server2 解决的:

尽管每次解决的 server 不同,然而登录状态始终保留着。因为咱们应用了 redis 保留 session。

留神,我这里每次都是随机一个 server 去解决,你运行的后果不肯定一样。

总结

session 为了解决存储用户大量数据和安全性的问题。sessions库为 Go Web 开发中解决 session 提供了简略,灵便的办法。它依赖较少,能够即插即用,十分不便。

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

参考

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

我的博客:https://darjun.github.io

欢送关注我的微信公众号【GoUpUp】,独特学习,一起提高~

正文完
 0