web中的session管理,推荐使用gorilla/sessions包
大家好,我是渔夫子。本号新推出「Go工具箱」系列,意在给大家分享使用go语言编写的、实用的、好玩的工具。同时了解其底层的实现原理,以便更深入地了解Go语言。
在web开发中,大家一定会使用到session。在go的很多web框架中并没有集成session管理的中间件。要想使用session功能,我推荐大家使用这个包:gorilla/sessions。以下是该包的基本情况:
sessions小档案 | |||
---|---|---|---|
star | 2.5k | used by | 11.5k |
contributors | 50 | 作者 | gorilla |
功能简介 | 该包提供了web开发中对session的实现。session的数据能够存储在cookie和文件系统中。同时该包还支持自定义的存储扩展。比如redis、mysql等。且常用的存储已经实现。见下文中详细介绍。 | ||
项目地址 | https://github.com/gorilla/sessions | ||
相关知识 | session |
一、什么是session
session就是用来在服务端存储相关数据的,以便在同一个用户的多次请求之间保存用户的状态,比如登录的状态。 因为HTTP协议是无状态的,要想让客户端(一般浏览器代指一个客户端或用户)的前、后请求关联在一起,就需要给客户端一个唯一的标识来告诉服务端请求是来自于同一个用户,这个标识就是所谓的sessionid。该sessionid由服务端生成,并存储客户端(cookie、url)中。 当客户端再次发起请求的时候,就会携带该标识,服务端根据该标识就能查找到存在服务端上的相关数据。其工作原理如下:
二、gorilla/sessions包
2.1 简介
gorilla/sessions包提供了将session数据存储于cookie和文件中的功能。同时还支持自定义的后端存储,比如将session数据存储于redis、mysql等。目前已基于该包实现的后端存储如下:
可以说基本上常用的存储方式都已经有对应的实现了,完全满足日常的需求。
2.2 安装
通过go get命令安装该包,如下:
go get github.com/gorilla/sessions
2.3 基本使用
该包的使用可以分5步:定义存储session的变量、程序启动时实例化具体的session存储类型、在handler中获取session、读取或存储数据到session、持久化session。
下面是使用示例,该示例以文件存储类型为例,即将session的数据存储到指定文件中。
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gorilla/sessions"
"net/http"
"os"
)
// 第一步 定义全局的存储session数据的变量,
var Store sessions.Store
func main() {
r := gin.Default()
// 第二步,程序启动后,指定具体的存储类型:redis、mysql还是本地文件
Store = sessions.NewFilesystemStore("/tm//godemo", []byte("Hello"))
r.GET("/sigin", func(ctx *gin.Context){
// 第三步,在具体的handler中获取session
session, _ := Store.Get(ctx.Request, "sessionid")
//第四步,从session中读取或存储数据。
session.Values["userid"] = "123456"
userid := session.Values["userid"]
fmt.Println("userid:", userid)
//第五步,保存session数据。本质上是将内存中的数据持久化到存储介质中。本例是存储到文件中
session.Save(ctx.Request, ctx.Writer)
ctx.Writer.Write([]byte("Hello World"))
})
r.Run(":8080")
}
在该示例中,第一步中的sessions.Store本质上是一个接口类型,只要实现了该接口,就可以存储session的数据。所以我们在第二步中就指定了具体的存储类型:文件存储。当然也可以是mysql或redis都可以。
在第三步获取session时,Store.Get有两个参数,一个是请求参数Request,一个是session-name。这个session-name是存储session-id的变量名,存储于cookie或url的query中,当然也可以是在Header头中。服务端从Request中通过该参数名获取session-id,再根据该session-id从后端存储中(文件、redis或mysql等)获取对应的数据,如果有已经存在的数据,则读取出来并解析到session对象中,否则就初始化一个新的session对象。
第五步的操作本质上是持久化。因为在第四步的复制只是把数据存储在了内存中,需要调用Save才能将数据持久化到对应的存储介质上。
2.4 实现原理
session的存储本质上就是在服务端给每一个用户存储一行记录。服务端给每个用户分配一个唯一的session-id,以session-id为主键,存储对应的值。如果存储在mysql中,sessioin-id就是主键;如果存储在redis中,session-id就是key;如果存储在文件中,session-id就是对应的文件名,文件内容就是存储的session数据。
2.4.1 在内存中存储session数据
我们以最简单的将session存储在内存中为例一步一步实现。首先定义一个session对象,用于存储session数据:
type Session struct {
// session-id,每个用户具有唯一的id
ID string
// 存储在session中的数据,key-value形式
Values map[interface{}]interface{}
}
好了,现在我们可以在服务端存储session数据了。如下:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gorilla/sessions"
"net/http"
"os"
)
func main() {
r := gin.Default()
r.GET("/sigin", func(ctx *gin.Context){
// 初始化一个session对象,Values用于保存数据
session := &Session{
Values: make(map[string]interface{}),
}
session.Values["userid"] = "123456"
userid := session.Values["userid"]
fmt.Println("userid:", userid)
ctx.Writer.Write([]byte("Hello World"))
})
r.Run(":8080")
}
2.4.2 如何存储不同用户的session
这是最简单的在服务端存储数据的方式。同时只有一个session对象,不能区分不同用户的数据。所以,需要给session一个唯一的标识。唯一的标识有不同的算法,可以使用数据库中的自增字段,也可能使用uuid。我们这里使用go标准库中的读取随机值的方式,如下:
func GenerateRandomKey(length int) []byte {
k := make([]byte, length)
if _, err := io.ReadFull(rand.Reader, k); err != nil {
return nil
}
return k
}
那么,初始化session的代码演变成如下:
func main() {
r := gin.Default()
r.GET("/sigin", func(ctx *gin.Context){
// 初始化一个session对象,Values用于保存数据
session := &Session{
ID:base64.RawStdEncoding.EncodeToString(GenerateRandomKey(32)), //初始化session-id
Values: make(map[string]interface{}),
}
session.Values["userid"] = "123456"
userid := session.Values["userid"]
fmt.Println("userid:", userid)
ctx.Writer.Write([]byte("Hello World"))
})
}
因为产生的随机数是字节序列,而非可见字符,所以需要使用base64编码将其变成可见字符。现在session的唯一标识有了,那在服务端如何存储所有用户的session呢?使用map。在map中以sessionid为key,Session中的Values作为值。所以我们定义一个全局的SessionMap对象。如下:
// 存储所有用户的session数据
var SessionMap map[string]*Session
func main() {
r := gin.Default()
r.GET("/sigin", func(ctx *gin.Context){
sessionId := base64.RawStdEncoding.EncodeToString(GenerateRandomKey(32))
var session *Session
var ok bool
if session, ok = SessionMap[sessionId]; !ok {
// 初始化一个session对象,Values用于保存数据
session = &Session{
ID:, sessionId//初始化session-id
Values: make(map[string]interface{}),
}
}
session.Values["userid"] = "123456"
userid := session.Values["userid"]
fmt.Println("userid:", userid)
// 将session存储到SessionMap中
SessionMap[sessionId] = session
ctx.Writer.Write([]byte("Hello World"))
})
}
func GenerateRandomKey(length int) []byte {
k := make([]byte, length)
if _, err := io.ReadFull(rand.Reader, k); err != nil {
return nil
}
return k
}
2.4.3 cookie中持久保存sessionid
目前 服务端虽然可以存储所有用户的session数据了。但这里还有一个问题就每次请求sigin接口的时候都会重新生成一个sessionId。那如何将一个用户的前后请求关联起来呢? 没错,就是让用户请求的时候在cookie或url的query中携带sessionid。该sessionid是由服务端在第一次生成的时候下发给客户端的。 我们以下发给cookie为例。
// 存储所有用户的session数据
var SessionMap map[string]*Session
func main() {
r := gin.Default()
r.GET("/sigin", func(ctx *gin.Context){
var sessionId string
// 从cookie中获取sessionid
cookie, err := ctx.Request.Cookie("session-id")
if err != nil {
sessionId = base64.RawStdEncoding.EncodeToString(GenerateRandomKey(32))
}else {
//cookie
sessionId = cookie.Value)
}
var session *Session
var ok bool
if session, ok = SessionMap[sessionId]; !ok {
// 初始化一个session对象,Values用于保存数据
session = &Session{
ID:, sessionId//初始化session-id
Values: make(map[string]interface{}),
}
}
session.Values["userid"] = "123456"
userid := session.Values["userid"]
fmt.Println("userid:", userid)
// 将session存储到SessionMap中
SessionMap[sessionId] = session
// 将sessionId写到cookie中
http.SetCookie(ctx.Writer, &http.Cookie{
Name: "session-id",
Value: sessionId,
Path: "/",
Domain: "",
Expires: time.Now().Add(24*time.Hour),
})
ctx.Writer.Write([]byte("Hello World"))
})
}
此时,就可以先从cookie中获取session-id的值,如果存在,则直接使用之前的session-id,这样就能从服务端获取到已经存在的session数据。如果从cookie中没获取到session-id,则生成一个新的ID,并下发给客户端。
这样,我们就可以区分不同用户、并能根据session-id获取用户之前存储在服务端上的session数据了。
2.4.4 session包中Store的抽象
当然,如果是需要持久化存储到mysql、redis或文件中时,则需要将session.Value中的数据以及ID存储到对应的介质中即可。这也是在使用session包时最后需要使用session.Save方法的原因。
在session包中,实质上是对存储进行了抽象。不同的存储实例需要实现该抽象接口。在程序入口启动处就指定具体的存储对象,然后调用相同的操作接口。如开始实例中初始化Store的代码:
// 这里的[]byte("Hello")实际上是用于数据存储加密的秘钥。
Store = sessions.NewFilesystemStore("/tm//godemo", []byte("Hello"))
如果我们将session存储在内存中的方式 以Store扩展的形式进行改写,则只要实现Store的三个接口即可。如下:
package main
import (
"crypto/rand"
"encoding/base64"
"fmt"
"github.com/gin-gonic/gin"
"github.com/gorilla/sessions"
"io"
"net/http"
"os"
"time"
)
type MemoryStore struct {
Options *sessions.Options // 用于设置cookie的属性
Cache map[string]*sessions.Session
}
func NewMemoryStore() *MemoryStore {
ms := &MemoryStore{
Options: &sessions.Options{
Path: "/",
MaxAge: 86400 * 30,
},
Cache: make(map[string]*sessions.Session, 0),
}
ms.MaxAge(ms.Options.MaxAge)
return ms
}
func (m *MemoryStore) Get(r *http.Request, name string) (*sessions.Session, error) {
return sessions.GetRegistry(r).Get(m, name)
}
func (m *MemoryStore) New(r *http.Request, name string) (*sessions.Session, error) {
session := sessions.NewSession(m, name)
options := *m.Options
session.Options = &options
session.IsNew = true
c, err := r.Cookie(name)
if err != nil {
// Cookie not found, this is a new session
return session, nil
}
if err != nil {
return session, err
}
session.ID = c.Value
v, ok := m.Cache[session.ID]
if !ok {
return session, nil
}
session = v
session.IsNew = false
return session, nil
}
func (m *MemoryStore) Save(r *http.Request, w http.ResponseWriter,
s *sessions.Session) error {
var cookieValue string
if s.Options.MaxAge < 0 {
cookieValue = ""
delete(m.Cache, s.ID)
for k := range s.Values {
delete(s.Values, k)
}
} else {
if s.ID == "" {
s.ID = base64.RawStdEncoding.EncodeToString(GenerateRandomKey(32))
}
cookieValue = s.ID
m.Cache[s.ID] = s
}
http.SetCookie(w, &http.Cookie{
Name: s.Name(),
Value: cookieValue,
Path: "/",
Domain: "",
Expires: time.Now().Add(24*time.Hour),
})
return nil
}
func (m *MemoryStore) MaxAge(age int) {
m.Options.MaxAge = age
}
// 第一步 定义全局的存储session数据的变量,
var Store sessions.Store
func main() {
r := gin.Default()
Store = NewMemoryStore()
r.GET("/sigin", func(ctx *gin.Context){
session, _ := Store.Get(ctx.Request, "session-id2")
session.Values["userid"] = 456789
session.Save(ctx.Request, ctx.Writer)
ctx.Writer.Write([]byte("Hello World"))
})
r.Run(":8080")
}
func GenerateRandomKey(length int) []byte {
k := make([]byte, length)
if _, err := io.ReadFull(rand.Reader, k); err != nil {
return nil
}
return k
}
3 总结
通过阅读session包,我们可以了解到服务端session实现的底层逻辑。session的实现本质上就是通过给用户分配一个唯一的ID,以该ID为主键,然后将数据存储到不同的介质中。最后再将该ID下发给cookie,当客户端后续发送请求时,服务端就可以通过cookie中的ID获取到对应的session数据了。