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 的实现:
- 创立一个session对象(包含:request信息,Store 存储载体redis RediStore 对象...)
- 把创立的的session对象 set 到 *gin.Context 键值对中;key 为一个定值:DefaultKey(github.com/gin-gonic/contrib/sessions)
- 路由层只会在初始化的时候执行一次,而且 store 是捆绑在 Session 中,因而每一个 session 都会指向同一个store
获取session实现
咱们能够到在 router 中间件中曾经创立好 session 对象并且 set 到对应的 gin.Context 中,那么咱们只须要调用 sessions.Default(c) 进去即可;
// shortcut to get sessionfunc 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是 nilsessions.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 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 对象中;也是下面的最初一个办法,所以咱们重点看看最初一个办法:
- 如果没SessionID 就随机生成一个sessionID (并发来的时候是否会生成雷同SessionID)
- 将session的值写入redis
- cookie编码一下
- 依据session的属性,写入 cookie (SessionID, path, maxAge等)
根本实现:留一个简略问题,当申请并发的时候生成 SessionID 是否存在雷同?
倡议:联结gin-session中间件,跟着看。。
如果发现什么问题 请指出。。