开发者

gin session中间件使用及源码流程分析

目录
  • 概述
  • Gin中的 Session
    • 简单调用
  • Gin-session 源码流程
    • 简单调用
    • 创建 store 对象
      • 底层的 store 创建
    • 作为中间件在 gin router层调用
      • 获取session实现
        • 读取session值
          • 写入 session 值

          概述

          一般PC 端网站开发都会谈到Session,服务端开启Session机制,客户端在第一次访问服务端时,服务端会生成sessionId通过cookie 机制回传到客户端保存,之后每次客户端访问服务端时都会通过cookie机制带sessionId到服务端,服务端通过解析SessionID找到请求的Session会话信息;

          Session信息都是保存在服务器中的,类似:SessionID =》 session信息;至于session信息具体内容是什么,这个要根据具体的业务逻辑来确定,但是普遍是用户信息;

          服务端保存Session的方式很多:文件,缓存,数据库等,所以衍生出来的session的载体也有很多:Redis,文件,mysql,memcached 等等;其中每一种载体都有着自己的优劣,根据不同的业务场景可以选取合适的载体;

          下面我们主要介绍 redis 作为载体:

          Gin中的 Session

          gin中Session的实现主要依赖于Gin-session中间件实现 (https://github.com/gin-contri... 通过注入不同的 store 从wxCieT而实现不同的载体保存Session信息 :

          主要包括:

          • cookie-based
          • Redis
          • memcached
          • MongoDB
          • memstore

          简单调用

          创建一个新的store并将中间件注入到gin的路由器中。需要使用的时候在HandlerFunc内部用 sessions.Default(c)即可获取到session

          // 创建载体方式对象(cookie-based)
          store := cookie.NewStore([]byte("secret"))
          r.Use(sessions.Sessions("sessionId", store))
          r.GET("/hello", func(c *gin.Context) {
              // session中间使用
              session := sessions.Default(c)
              if session.Get("hello") != "world" {
                  session.Set("hello", "world")
                  session.Save()
              }
              ....
          })

          Gin-session 源码流程

          下面我们以使用频率较高的 redis 作为 store 来看看 Gin-session 主要工作流程

          简单调用

          router := gin.Default()
          // @Todo 创建store对象
          store, err := sessions.NewRedisStore(10, "tcp", "localhost:6379", "", []byte("secret"))
              if err != nil {log.Fatal(" sessions.NewRedisStore err is :%v", err)}
          router.GET("/admin", 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})
              })

          创建 store 对象

          底层的 store 创建

          func NewRediStore(size int, network, address, password string, keyPairs ...[]byte) (*RediStore, error) {
              return NewRediStoreWithPool(&redis.Pool{
                  MaxIdle:     size,
                  IdleTimeout: 240 * time.Second,
                  TestOnBorrow: func(c redis.Conn, t time.Time) error {
                      _, err := c.Do("PING")
                      return err
                  },
                  Dial: func() (redis.Conn, error) {
                      return dial(network, address, password)
                  },
              }, keyPairs...)
          }
          // NewRediStoreWithPool instantiates a RediStore with a *redis.Pool passed in.
          func NewRediStoreWithPool(pool *redis.Pool, keyPairs ...[]byte) (*RediStore, error) {
              rs := &RediStore{
                  // http://godoc.org/github.com/gomodule/redigo/redis#Pool
                  Pool:   pool,
                  Codecs: securecookie.CodecsFromPairs(keyPairs...),
                  Options: &sessions.Options{
                      Path:   "/", // 客户端的 Path
                      MaxAge: sessionExpire, // 客户端的Expires/MaxAge
                  },
                  DefaultMaxAge: 60 * 20, // 过期时间是20分钟
                  maxLength:     4096, // 最大长度
                  keyPrefix:     "session_", // key 前缀
                  serializer:    GobSerializer{}, // 内部序列化采用了Gob库
              }
              // @Todo 尝试创建连接
              _, err := rs.ping()
              return rs, err
          }

          根据函数可以看到,根据传入参数(size:连接数, network:连接协议,address:服务地址,password:密码)初始化一个 redis.Pool 对象,通过传入 redis.Pool 对象 和 一些默认的参数初始化 RediStore 对象

          作为中间件在 gin router层调用

          作为 gin 中间件使用并不复杂,就是把HandlerFunc放到group.Handlers数组后面;

          // Use adds middleware to the group, see example code in GitHub.
          func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
              group.Handlers = append(group.Handlers, middleware...)
              return group.returnObj()
          }

           下面看看这个中间件的方法实现了什么: sessions.Sessions("sessionId", store),

          const (
              DefaultKey  = "github.com/gin-gonic/contrib/sessions"
              errorFormat = "[sessions] ERROR! %s\n"
          )
          
          
          func Sessions(name string, store Store) gin.HandlerFunc {
              return func(c *gin.Context) {
                  s := &session{name, c.Request, store, nil, false, c.Writer}
                  c.Set(DefaultKey, s)
                  defer context.Clear(c.Request)
                  c.Next()
              }
          }
          
          type session struct {
              name    string
              request *http.Request
              store   Store
              session *sessions.Session
              written bool
              writer  http.ResponseWriter
          }

          我们可以看到他 HandlerFunc 的实现:

          • 创建一个session对象(包括:request信息,Store 存储载体redis RediStore 对象...)
          • 把创建的的session对象 set 到 *gin.Context 键值对中;key 为一个定值:DefaultKey(github.com/gi编程客栈n-gonic/contrib/sessions)
          • 路由层只会在初始化的时候执行一次,而且 store 是捆绑在 Session 中,因此每一个 session 都会指向同一个store

          获取session实现

          我们可以到在 router 中间件中已经创建好 session 对象并且 set 到对应的 gin.Context 中,那么我们只需要调用 sessions.Default(c) 出来即可;

          // shortcut to get session
          func Default(c *gin.Context) Session {
              return c.MustGet(DefaultKey).(Session)
          }

          注意:返回的类型是Session的接口定义,gin.Context 中 set 的是session具体实现;

          读取session值

          通过简单的代码获取session的值

          // @Todo SessionKey 中获取用户信息
          session := sessions.Default(c)
          sessionInfo := session.Get(public.AdminSessionInfoKey)

          上面已经有提到过了,sessions.Default 返回的 Session 的接口定义类,其定义了Get()这个方法接口,实际的方法实现还在session中。

          type session struct {
              name    string
              request *http.Request
              store   Store
              session *sessions.Session // 实际内部数据交换
              written bool
              writer  http.ResponseWriter
          }
          func (s *session) Get(key interface{}) interface{} {
              // 通过s.Session() 获取具体的session;具体的值保存在 Values 这个map 中
              return s.Session().Values[key]
          }
          func (s *session) Session() *sessions.Session {
              if s.session == nil {
                  var err error
                  s.session, err = s.store.Get(s.request, s.name)
                  if err != nil {
                      log.Printf(errorFormat, err)
                  }
              }
              return s.session
          }

          通过观察 sessandroidion 的结构体, 里面包含着 session sessions.Session 对象,这个要跟 之前的 Session 接口定义区分开;这里的 sessions.Session 是真正保存的session 对象;其结构体如下:(gorilla/sessions库)

          // Session stores the values and optional configuration for a session.
          type Session struct {
              // The ID of the session, generated by stores. It should not be used for
              // user data.
              ID string
              // Values contains the user-data for the session.
              Values  map[interface{}]interface{}
              Options *Options
              IsNew   bool
              store   Store
              name    string
          }

          OK! 知道 s.session 是什么后,那么 s.Session().Values[key] 也变得非常好理解了,其实 Values 这个属性其实是个map,其中保存的就是我们 set 在session中的具体值;我们继续往下走。。。

          当 s.session 是空的时候,我们就通过 s.store.Get(s.request, s.name) 获取;

          // s.request 请求
          // s.name session名
          s.session, err = s.store.Get(s.request, s.name)

          注意:s.request: 请求 和 s.name: session名 什么时候注入的呢? 其实我们这里可以回顾下上面:

          // @Todo 在路由层注入session的时候 Seesions 方法其实就初始化了这个session的name 和其他值,只是保存的session是 nil
          sessions.Sessions("sessionId", store)

          言归正传,我们继续往下走,上面通过 store.Get 来获取 session;因为这里的我们分析的是redis载体,所以 store 是 RediStore 我们看下他的GET方法:

          // Get returns a session for the given name after adding it to the registry.
          //
          // See gorilla/sessions FilesystemStore.Get().
          func (s *RediStore) Get(r *http.Request, name string) (*sessions.Session, error) {
              return sessions.GetRegistry(r).Get(s, name)
          }

          我们可以看到:通过 sessions.GetRegistry(r) 获取到一个 Registry ;然后通过 Registry 的 GET方法获取一个session;

          我们来看看他的实现:

          // GetRegistry 本质就是返回一个 Registry 的一个结构体
          func GetRegistry(r *http.Request) *Registry {
              registry := context.Get(r, registryKey)
              if registry != nil {
                  return registry.(*Registry)
              }
              newRegistry := &Registry{
                  request:  r,
                  sessions: make(map[string]sessionInfo),
              }
              context.Set(r, registryKey, newRegistry)
              return newRegistry
          }
          // Registry stores sessions used during a request.
          type Registry struct {
              request  *http.Request
              sessions map[string]sessionInfo
          }
          // Get registers and returns a session for the given name and session store.
          //
          // It returns a new session if there are no sessions registered for the name.
          func (s *Registry) Get(store Store, name string) (session *Session, err error) {
              if !isCookieNameValid(name) {
                  return nil, fmt.Errorf("sessions: invalid character in cookie name: %s", name)
              }
              if info, ok := s.sessions[name]; ok {
                  session, err = info.s, info.e
              } else {
                  session, err = store.New(s.request, name)
                  session.name = name
                  s.sessions[name] = sessionInfo{s: session, e: err}
              }
              session.store = store
              return
          }

          其实不难看出 GetRegistry 本质上就是返回了一个 Registry 结构体;然后结合 Get 方法我们可以看出其实 Registry 结构体本质上是维护着一个 key -》 value 的映射关系; 而其中的 key 就是我们 开始在路由注入的 session name , value 就是我们保存的 sessionInfo;

          所以我们也可以理解:Registry 的作用就是维护一个业务session名到对应session的映射,隔离了session。当session不存在时,需要调用 store.New(s.request, name) 来新建一个session:

          // New returns javascripta session for the given name without adding it to the registry.
          //
          // See gorilla/sessions FilesystemStore.New().
          func (s *RediStore) New(r *http.Request, name string) (*sessions.Session, error) {
              var (
                  err error
                  ok  bool
              )
              // @Todo 初始化一个业务的session 
              session := sessions.NewSession(s, name)
              // make a copy
              options := *s.Options
              session.Options = &options
              session.IsNew = true
              // @Todo 根据session_name读取cookie中的sessionID
              if c, errCookie := r.Cookie(name); errCookie == nil {
                  // @Todo 编解码器对cookie值进行解码
                  err = securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...)
                  if err == nil {
                      // @Todo redis中根据sessionID 获取具体的 sessionInfo
                      ok, err = s.load(session)
                      session.IsNew = !(err == nil && ok) // not new if no error and data available
                  }
              }
              return session, err
          }

          跑了这么久。。终于看到从 cookie 中读取 sessionID,然后 根据SessionID 从 redis 中把我们的session加载出来;

          写入 session 值

          其实 写入 和 读取 差别不是很大:

          // @Todo 写值入口 (其实就是session map 中赋值一下)
          func (s *session) Set(key interface{}, val interface{}) {
              s.Session().Values[key] = val
              s.written = true
          }
          func (s *session) Save() error {
              if s.Written() {
                  e := s.Session().Save(s.request, s.writer)
                  if e == nil {
                      s.written = false
                  }
                  return e
              }
              return nil
          }
          // Save is a convenience method tojavascript save this session. It is the same as calling
          // store.Save(request, response, session). You should call Save before writing to
          // the response or returning from the handler.
          func (s *Session) Save(r *http.Request, w http.ResponseWriter) error {
              return s.store.Save(r, w, s)
          }
          // Save adds a single session to the response.
          func (s *RediStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
              // @Todo 删除session并把cookie中也强制过期
              if session.Options.MaxAge <= 0 {
                  if err := s.delete(session); err != nil {
                      return err
                  }
                  http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options))
              } else {
                  // @Todo 如果没SessionID 就随机生成一个sessionID (并发来的时候是否会生成相同SessionID)
                  if session.ID == "" {
                      session.ID = strings.TrimRight(base32.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(32)), "=")
                  }
                  // @Todo 将session的值写入redis
                  if err := s.save(session); err != nil {
                      return err
                  }
                  // @Todo cookie编码一下
                  encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, s.Codecs...)
                  if err != nil {
                      return err
                  }
                  // @Todo 根据session的属性,写入 cookie (SessionID, path, maxAge等)
                  http.SetCookie(w, sessions.NewCookie(session.Name(), encoded, session.Options))
              }
              return nil
          }

          其实我们可以看到最后执行 save 的最终实现还是放在了 RediStore 对象中;也是上面的最后一个方法,所以我们重点看看最后一个方法:

          • 如果没SessionID 就随机生成一个sessionID (并发来的时候是否会生成相同SessionID)
          • 将session的值写入redis
          • cookie编码一下
          • 根据session的属性,写入 cookie (SessionID, path, maxAge等)

          基本完成:留一个简单问题,当请求并发的时候生成 SessionID 是否存在相同?

          建议:联合gin-session中间件,跟着看。。

          以上就是gin session中间件使用及源码分析的详细内容,更多关于gin session中间件的资料请关注编程客栈(www.devze.com)其它相关文章!

          0

          上一篇:

          下一篇:

          精彩评论

          暂无评论...
          验证码 换一张
          取 消

          最新开发

          开发排行榜