乐趣区

关于golang:ginsession使用以及源码分析

gin-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 从而实现不同的载体保留 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 的实现:

  1. 创立一个 session 对象(包含:request 信息,Store 存储载体 redis RediStore 对象 …)
  2. 把创立的的 session 对象 set 到 *gin.Context 键值对中;key 为一个定值:DefaultKey(github.com/gin-gonic/contrib/sessions)
  3. 路由层只会在初始化的时候执行一次,而且 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
}

通过观察 session 的构造体,外面蕴含着 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 载体,所以 storeRediStore 咱们看下他的 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;而后通过 RegistryGET办法获取一个 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 a 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 to 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 对象中;也是下面的最初一个办法,所以咱们重点看看最初一个办法:

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

根本实现:留一个简略问题,当申请并发的时候生成 SessionID 是否存在雷同?

倡议:联结 gin-session 中间件,跟着看。。

如果发现什么问题 请指出。。

退出移动版