在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 mainimport ("fmt""github.com/gin-gonic/gin""github.com/gorilla/sessions""net/http""os"
)// 第一步 定义全局的存储session数据的变量,
var Store sessions.Storefunc main() {r := gin.Default()// 第二步,程序启动后,指定具体的存储类型:redis、mysql还是本地文件Store = sessions.NewFilesystemStore("/tm//godemo", []byte("Hello"))r.GET("/sigin", func(ctx *gin.Context){// 第三步,在具体的handler中获取sessionsession, _ := 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,每个用户具有唯一的idID string// 存储在session中的数据,key-value形式Values  map[interface{}]interface{}}

好了,现在我们可以在服务端存储session数据了。如下:

package mainimport ("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-idValues: 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]*Sessionfunc main() {r := gin.Default()r.GET("/sigin", func(ctx *gin.Context){sessionId := base64.RawStdEncoding.EncodeToString(GenerateRandomKey(32))var session *Sessionvar ok boolif session, ok = SessionMap[sessionId]; !ok {// 初始化一个session对象,Values用于保存数据session = &Session{ID:, sessionId//初始化session-idValues: make(map[string]interface{}),}}session.Values["userid"] = "123456"userid := session.Values["userid"]fmt.Println("userid:", userid)// 将session存储到SessionMap中SessionMap[sessionId] = sessionctx.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]*Sessionfunc main() {r := gin.Default()r.GET("/sigin", func(ctx *gin.Context){var sessionId string// 从cookie中获取sessionidcookie, err := ctx.Request.Cookie("session-id")if err != nil {sessionId = base64.RawStdEncoding.EncodeToString(GenerateRandomKey(32))}else {//cookiesessionId = cookie.Value)}var session *Sessionvar ok boolif session, ok = SessionMap[sessionId]; !ok {// 初始化一个session对象,Values用于保存数据session = &Session{ID:, sessionId//初始化session-idValues: 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"))

image.png

如果我们将session存储在内存中的方式 以Store扩展的形式进行改写,则只要实现Store的三个接口即可。如下:

package mainimport ("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.Optionssession.Options = &optionssession.IsNew = truec, err := r.Cookie(name)if err != nil {// Cookie not found, this is a new sessionreturn session, nil}if err != nil {return session, err}session.ID = c.Valuev, ok := m.Cache[session.ID]if !ok {return session, nil}session = vsession.IsNew = falsereturn session, nil
}func (m *MemoryStore) Save(r *http.Request, w http.ResponseWriter,s *sessions.Session) error {var cookieValue stringif 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.IDm.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.Storefunc main() {r := gin.Default()Store = NewMemoryStore()r.GET("/sigin", func(ctx *gin.Context){session, _ := Store.Get(ctx.Request, "session-id2")session.Values["userid"] = 456789session.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数据了。

资料下载

点击下方卡片关注公众号,发送特定关键字获取对应精品资料!

  • 回复「电子书」,获取入门、进阶 Go 语言必看书籍。

  • 回复「视频」,获取价值 5000 大洋的视频资料,内含实战项目(不外传)!

  • 回复「路线」,获取最新版 Go 知识图谱及学习、成长路线图。

  • 回复「面试题」,获取四哥精编的 Go 语言面试题,含解析。

  • 回复「后台」,获取后台开发必看 10 本书籍。

对了,看完文章,记得点击下方的卡片。关注我哦~ 

「Go工具箱」web中的session管理,推荐使用gorilla/sessions包相关推荐

  1. 「前端进阶」JS中的内存管理

    前言: 像C语言这样的底层语言一般都有底层的内存管理接口,比如 malloc()和free()用于分配内存和释放内存. 而对于JavaScript来说,会在创建变量(对象,字符串等)时分配内存,并且在 ...

  2. 「Excel技巧」Excel中根据某列的值去汇总另外一列的值

    「Excel技巧」Excel中根据某列的值去汇总另外一列的值 在Excel表格中输入数据,需要根据component列的值分别汇总July列的值. 1.选中单元格区域并点击"insert&q ...

  3. python对excel某一列去重-「总结篇」Python中所有的Excel操作技巧

    原标题:「总结篇」Python中所有的Excel操作技巧 Python对于Excel的操作是多种多样的,掌握了相关用法就可以随心所欲的操作数据了! 操作xls文件 xlrd(读操作): import ...

  4. 「分布式专题」分布式系统中一致性hash算法

    近年来B2C.O2O等商业概念的提出和移动端的发展,使得分布式系统流行了起来.分布式系统相对于单系统,解决了流量大.系统高可用和高容错等问题.功能强大也意味着实现起来需要更多技术的支持.例如系统访问层 ...

  5. 全文解析:面向基于区块链的「机器人经济学」概念中,如何验证自主智能体的行为?...

    原文来源:arXiv 作者:Konstantin Danilov.Ruslan Rezin.Alexander Kolotov. Ilya Afanasyev 「雷克世界」编译:嗯~是阿童木呀.KAB ...

  6. DotNetCore Web应用程序中的Session管理

    原文来自互联网,由长沙DotNET技术社区编译.如译文侵犯您的署名权或版权,请联系小编,小编将在24小时内删除.限于译者的能力有限,个别语句翻译略显生硬,还请见谅. 作者简介:Jon(Jonathan ...

  7. web中的cookie管理

    本篇是以JSP为背景介绍,但是在web开发中也是相同的原理. 什么是cookie 由于http是一种无状态的协议,因此服务器收到请求后,只会当做一次新的请求.即便你重复发送了1000次同样的请求,这1 ...

  8. 大型web工程的session管理器构想

    2019独角兽企业重金招聘Python工程师标准>>> **声明:**抛砖引玉,期望讨论. 如果已经有类似功能的开源框架等工具,请告知,俺做鬼也不会忘记你滴 (注:Servlet有监 ...

  9. tomcat中的session管理

    Session的管理 当一个sesson开始时,Servlet容器会创建一个HttpSession对象,在某些情况下把这些Httpsession对象从内存中转移到文件系统中或数据库中,需要访问的时候在 ...

最新文章

  1. [华为机试真题][2014]62.去除重复字符并排序
  2. Lua脚本语言快速入门手册
  3. 【业务知识】企业数字档案馆总体架构图
  4. Cortex-A 的内核寄存器组
  5. Spring Data JPA的持久层
  6. window两个窗口上下摆放_滑动窗口技巧
  7. sql server2008如何修改mac地址_QCC304x/QCC514x:修改蓝牙MAC地址及名称
  8. 逆向破解flash视频url
  9. LoadRunner 快速生成手写脚本
  10. 大疆无人机无图像传输_无人机短距离图像传输与接收原理
  11. DOX-HMDN-PEI 阿霉素-二氧化锰-聚乙烯亚胺/PEI-g-PLO(DCA) 聚鸟氨酸-聚乙烯亚胺
  12. 卡拉赞服务器延迟,卡拉赞开荒详细功略(前门)
  13. 基于点锐度的清晰度算法 EAV
  14. 2022学术道德与学术规范教育【研究生】SPOC课程答案
  15. imp-00003:oracle error 959 encountered
  16. 短信的独特优势以及如何选择国际短信平台?
  17. UI设计需要学会哪些软件?
  18. java判断文件路径的状况
  19. 转贴点关于娜娜的文章
  20. EMNLP 2020 | Facebook稠密向量召回方案

热门文章

  1. matlab2014 整数规划,整数规划模型的Matlab程序实现
  2. Cocos Creator教程:Shader与材质
  3. 透镜分离相位检测对焦的原理
  4. 江蘇省單招c語言技能編程詳解
  5. JSP:response对象
  6. C语言float变量精度,float部类的精度究竟是多少
  7. 道法自然《三》:框架设计要提供机制
  8. Android 在Service中使用bindService
  9. 4G模块WH-LTE-7S4网络AT指令使用实例
  10. LINUX ULE112 系列筆記