Go中gin框架中Session详解

2,818 阅读7分钟

前言

近期在学习Go相关知识体系,并且尝试写一些API,尽管已经学习过GO相关知识,并且可以照猫画虎,在真正设计组织代码时,感觉还是有所欠缺。 比如何时指针、何时用对象会有所犹豫。最终决定研究一些简单的框架,看他们是怎么组织代码的。

介绍的源码是 Go中gin框架session管理,下面是demo,看上去也是不复杂的。

代码地址是:github.com/gin-contrib/sessions/redisgithub.com/boj/redistore

Sessions :用来对请求Session 的crud进行定义,并不关心数据是怎么存的。具体的实现是由下一级的redis包负责实现,当然下一级可能是其他的数据库。

redistore is A session store backend for gorilla/sessions

简单Demo

package main

import (
   "fmt"
   "github.com/gin-contrib/sessions"
   "github.com/gin-contrib/sessions/redis"
   "github.com/gin-gonic/gin"
   "time"
)

func Logger() gin.HandlerFunc {
   return func(context *gin.Context) {
      host := context.Request.Host
      url := context.Request.URL
      method := context.Request.Method
      fmt.Printf("%s::%s \t %s \t %s\n ", time.Now(), host, url, method)
      context.Next()
      fmt.Printf("request end: %v\n", context.Writer.Status())
   }
}
func TestMiddleware() gin.HandlerFunc {
   return func(context *gin.Context) {
      fmt.Printf("TestMiddleware before.\n")
      context.Next()
      fmt.Printf("TestMIddleware end.\n")
   }
}

func main() {
   r := gin.Default()
   // 使用了上面定义的中间件
   r.Use(Logger(), TestMiddleware())
   // 初始化基于redis的存储引擎
   // 参数说明:
   //    第1个参数 - redis最大的空闲连接数
   //    第2个参数 - 数通信协议tcp或者udp
   //    第3个参数 - redis地址, 格式,host:port
   //    第4个参数 - redis密码
   //    第5个参数 - session加密密钥
   //这部分是我们后边要重点去探索的。 可以暂时不去理解
   store, _ := redis.NewStore(10, "tcp", "localhost:6379", "", []byte(""))
   r.Use(sessions.Sessions("mysession", store))

   r.GET("/incr", func(c *gin.Context) {
      session := sessions.Default(c)
      var count int
      v := session.Get("count")
      if v == nil {
         count = 0
      } else {
         count = v.(int)
         count++
      }
      session.Set("count", count)
      session.Save()
      c.JSON(200, gin.H{"count": count})
   })
   r.Run(":8000")
}

上面代码输出为:

2023-02-17 08:43:42.137111 +0800 CST m=+3.086237661::localhost:8000      /incr   GET
TestMiddleware before.
TestMIddleware end.
request end: 200

Go的中间件类似于Node.js 中koa框架洋葱模型。 context.Next() 之前的代码先被执行,之后的后被执行。

image

具体的编写方法以,可以参照上面Demo进行测试。

巨人肩膀

从sessions库的主要依赖开始介绍,主要用到下面几个:

1.Redis 操作库(缓存数据库Redis基本使用方法)

这部门我们先学习github.com/gomodule/redigo API方法,如果精力允许也可以看看里面实现,一般上这些库代码会比之上代码质量高一点。

API文档地址:pkg.go.dev/github.com/…

2.securecookie(对cookie进行加解密操作)

github.com/gorilla/sec…

库从12年开始第一行代码,作者目前已经不维护。 gorilla/securecookie提供了一种安全的 cookie,通过在服务端给 cookie 加密,让其内容不可读,也不可伪造。当然,敏感信息还是强烈建议不要放在 cookie 中。

3.gorilla/sessions (对session进行维护管理库)

github.com/gorilla/ses…

提供了基于 cookie 和本地文件系统的 session。同时预留扩展接口,可以使用其它的后端存储 session 数据。

包UML图

下面是session部分package主要UML图(go中以文件路径作为package,用UML表示有些时候有些不准确,可模块的依赖关系还是很清晰表达的)

UML diagram (4).jpg

主要代码

redis类是由 github.com/gin-contrib/sessions/redis 引入,其中还可以使用其他数据库类型作为session存储。

关于 github.com/gin-contrib/sessions/redis 源码分析。本质上是对Session中Store接口的实现。

package redis

import (
   "errors"
   "github.com/boj/redistore"
   "github.com/gin-contrib/sessions"
   "github.com/gomodule/redigo/redis"
)

type Store interface {
   sessions.Store
}

// size: redis链接的最大空闲数 network: 链接类型 tcp/udp, address: 链接地址 host:port, password: 密码redis-password
// 创建入口,其调用了 另一个库中 boj/redistore中的方法
func NewStore(size int, network, address, password string, keyPairs ...[]byte) (Store, error) {
   s, err := redistore.NewRediStore(size, network, address, password, keyPairs...)
   if err != nil {
      return nil, err
   }
   return &store{s}, nil
}

// NewStoreWithDB - like NewStore but accepts `DB` parameter to select
// redis DB instead of using the default one ("0")
//
// Ref: https://godoc.org/github.com/boj/redistore#NewRediStoreWithDB
func NewStoreWithDB(size int, network, address, password, DB string, keyPairs ...[]byte) (Store, error) {
   s, err := redistore.NewRediStoreWithDB(size, network, address, password, DB, keyPairs...)
   if err != nil {
      return nil, err
   }
   return &store{s}, nil
}

// NewStoreWithPool instantiates a RediStore with a *redis.Pool passed in.
//
// Ref: https://godoc.org/github.com/boj/redistore#NewRediStoreWithPool
func NewStoreWithPool(pool *redis.Pool, keyPairs ...[]byte) (Store, error) {
   s, err := redistore.NewRediStoreWithPool(pool, keyPairs...)
   if err != nil {
      return nil, err
   }
   return &store{s}, nil
}

type store struct {
   *redistore.RediStore
}

// GetRedisStore get the actual woking store.
// Ref: https://godoc.org/github.com/boj/redistore#RediStore
func GetRedisStore(s Store) (err error, rediStore *redistore.RediStore) {
   realStore, ok := s.(*store)
   if !ok {
      err = errors.New("unable to get the redis store: Store isn't *store")
      return
   }

   rediStore = realStore.RediStore
   return
}

// SetKeyPrefix sets the key prefix in the redis database.
func SetKeyPrefix(s Store, prefix string) error {
   err, rediStore := GetRedisStore(s)
   if err != nil {
      return err
   }

   rediStore.SetKeyPrefix(prefix)
   return nil
}

func (c *store) Options(options sessions.Options) {
   c.RediStore.Options = options.ToGorillaOptions()
}
  1. 外部首先调用(redisSotre 仅仅 是sessions 下的一种缓存存储方法,因此目录是子父关系)

store, \_ := redis.NewStore(10, "tcp", "localhost:6379", "", \[]byte(""))

注意这个函数定义的返回值 Store 接口:

func NewStore(size int, network, address, password string, keyPairs ...\[]byte) (Store, error)

  1. 关于Store Interface 定义 【这里比较绕,也比较关键,说明了作者设计的思路】
//redis store中 Store接口定义
type Store interface {
   sessions.Store  // sessions 是上一级中session
}

//上一级session中定义
type Store interface {
   sessions.Store   // Get, New, Save 接口来自 gorilla/session 中store 接口
   Options(Options) // cookie配置项  // 在seesion中定义的cookie配置
}

// gorilla/session 中store 
// See CookieStore and FilesystemStore for examples.
type Store interface {
   // Get should return a cached session.
   Get(r *http.Request, name string) (*Session, error)
   // New should create and return a new session.
   //
   // Note that New should never return a nil session, even in the case of
   // an error if using the Registry infrastructure to cache the session.
   New(r *http.Request, name string) (*Session, error)
   // Save should persist session to the underlying store implementation.
   Save(r *http.Request, w http.ResponseWriter, s *Session) error
}
  1. 实际上的返回值 是通过 对 RedisStore 类实例化来实现的。 ReidsStore实现了Store中的基本方法。

目前一个这样的Store,已经有了数据库连接池 redis.pool , 实现了将请求传入 并返回 Session对象的GET方法。

  1. 完成gin框架的中间件功能

r.Use(sessions.Sessions("mysession", store))

这个时候session相当是对Sotore的管理。详细的类设计见上图中 session 结构体设计。 简单介绍 Session是他的接口实现约束。 session中相当于把strore引用作为结构体一部分引入对象中使用。进而去操作Stroe对象。

  1. 在请求中使用

func Default(c *gin.Context) Session 在上下文中使用。

r.GET("/incr", func(c *gin.Context) {
   session := sessions.Default(c)
   var count int
   v := session.Get("count")
 })

从上下文中取得session对象,这样业务逻辑中就可以对session进行处理了。

相关知识

  1. 「Go工具箱」web中的session管理,推荐使用gorilla/sessions包 转载

  2. strcut 和 interface 区别

zhuanlan.zhihu.com/p/341353253

Interface是编程中的另一个强大概念。 Interface与struct类似,但只包含一些抽象方法。 在Go中,Interface定义了通用行为的抽象。

  1. go中什么时候用对象、什么时候用指针?

segmentfault.com/q/101000001…

  1. new(bytes.Buffer) 方法使用

bytes.Buffer 是 Golang 标准库中的缓冲区,具有读写方法和可变大小的字节存储功能。缓冲区的零值是一个待使用的空缓冲区。 new之后相当于是返回了一个buff变量。

gob是golang包自带的一个数据结构序列化的编码/解码工具。

gob的目的是什么?应用场景? 让数据结构能够在网络上传输或能够保存到文件中。gob可以通过json或gob来序列化struct对象,虽然json的序列化更为通用,但利用gob编码可以实现json所不能支持的struct方法序列化。 但是gob是golang提供的“私有”的编码方式,go服务之间通信可以使用gob传输。 一种典型的应用场景就是RPC。程序之间相互通信的。

总结思考

1.思考方式差异

一般上我写 功能逻辑,更喜欢从简单功能实现写起,然后再去思考向复杂功能写。例如实现一个简单的web服务: 先直接用gin实现一个简单api,然后 +session,然后+长链接,然后+upload,然后+数据库操作,然后+缓存等。 然后一系列操作。这种方式很容易陷入到面向过程中,导致越写越复杂。 或者说在后边再做简单的继承抽象。

源代码中学习中,看到这样的特点,大量通过Interface 用来约来资源方法。尤其是 sessions 和 sessions/redis 直接的依赖与定义,以及 resdisStore中 对redis数据查询封装。 封装让模块与模块之间的功能划分很清晰。 面向接口编程观念很强,如何锻炼这样的意识,感觉还需大量 阅读思考写代码。

  1. 学到知识点
  • 代理模式要注意熟悉多用。一般方法都是委托给类自己去处理,然后用的时候去实例化类,但是在redisStroe中将seesion作为参数进行操作。
  • 其他的与Go基础相关知识点(比较重要的在上文已经泄漏)。