本文讨论的Gin相关的session库为:https://github.com/gin-contrib/sessions
Redis-based session源码分析gin-contrib/sessions这个库支持多种session的实现,例如:cookie-based session / Redis-based session / memcached Session。这里我们主要分析Redis-based session的实现代码。cookie-based session和Redis-based session的优缺点比较:
优点 | 缺点 | |
---|---|---|
cookie-based | 简单,无需引入第三方存储,不占用服务器存储资源 | 不安全,受浏览器限制,控制起来不灵活(例如从服务端设置“登出”用户) |
Redis-based | 相对安全,控制更加灵活,适合存敏感信息 | 占用服务器资源 |
在Gin中,通常以中间件的方式进行session管理。中间件的使用方式,我们应该都已经比较熟悉了。相应的HandlerFunc在gin-contrib/sessionssessions.go中:
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() } }
这里就干了一件事情:把request和store信息Set到Gin context中的Keys里,方便Handler中进行使用。这里的store为redis的接口。由此可以判断,业务Handler中肯定是从context里取出session,进行应用,我们后面来验证。
Handler中获取session在Handler中,我们通过session := sessions.Default(c)来获取session,从而对session进行检查和处理。
sessions.go:
// shortcut to get session func Default(c *gin.Context) Session { return c.MustGet(DefaultKey).(Session) }Handler中Set & Save session
Set()和Save()分别是设置和保存session信息的方法。gin-contrib/sessions的底层引用的是gorilla/sessions,Session struct的结构如下,其中的Values这个map用于存储session信息的键值对。
// 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 }
所以Set()方法更新的是上面的Values:
func (s *session) Set(key interface{}, val interface{}) { s.Session().Values[key] = val s.written = true }
Save()方法是保存Values中的session信息。对于Redis-based session而言,是把session信息存入Redis。
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 }
再追一下s.Session().Save方法,它调用的是gorilla/sessions/sessions.go中的Save()方法:
// 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) }
这里的注释写的也比较清楚,在Handler response返回之前需要调用这个Save()方法保存session信息。而且我们看到这里的Save已经是store的save了,对于Redis-based session,此时进行的是Redis的写操作。
gin-contrib/sessions的底层引用的Redis strore为boj/redisstore。我们再看看store中的Save()方法:
// Save adds a single session to the response. func (s *RediStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error { // Marked for deletion. if session.Options.MaxAge <= 0 { if err := s.delete(session); err != nil { return err } http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options)) } else { // Build an alphanumeric key for the redis store. if session.ID == "" { session.ID = strings.TrimRight(base32.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(32)), "=") } if err := s.save(session); err != nil { return err } encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, s.Codecs...) if err != nil { return err } http.SetCookie(w, sessions.NewCookie(session.Name(), encoded, session.Options)) } return nil }
Save()方法做了这样几件事:
- 生成了一个session id,这个session id一定对应了Redis中的key
- s.save方法调用了Redis的SETEX命令,所以这里是可以设置过期时间的,我们后面再说
- 基于session name和session id,进行了编码,并进行SetCookie操作。这里也比较直观了,session id存在了cookie当中。
cookie信息中传入了一个参数Options,这个Options来自Session struct用于存放一些配置项,例如MaxAge。MaxAge既对应了Redis中Key的过期时间(就是上面第一条SETEX对应的过期时间),也对应了cookie的过期时间。
Handler中Get session在Handler中,实例化获取session的代码参看上文的sessions.Default(c)。
基于上面Set()方法和Save()方法的分析,不难猜出,Get()方法的功能就是基于cookie中的session id从Redis中获取session信息了。我们再看一下源码:
gin-contrib/sessions/sessions.go中的Get()方法是从Session的Values中获取信息:
func (s *session) Get(key interface{}) interface{} { return s.Session().Values[key] }
Session()是找到对应的session对象。由于中间件中的session为nil,所以会调用store中的Get方法来获取具体的session信息:
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 }
我们再追一下 boj/redistore store中的Get方法,其中如果没有已存在的session name就会New一个新的。然后将session info放入s.sessions中:
// 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 }
boj/redistore store中的New()方法做了如下几件事:
- 从Cookie中获取session id并解码为Redis中存储的id
- 调用RedisStore的load方法(Redis的GET命令)来从Redis中获取到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 ) session := sessions.NewSession(s, name) // make a copy options := *s.Options session.Options = &options session.IsNew = true if c, errCookie := r.Cookie(name); errCookie == nil { err = securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...) if err == nil { ok, err = s.load(session) session.IsNew = !(err == nil && ok) // not new if no error and data available } } return session, err }
以上就是对于session进行读/写/存的源码分析。至于用户鉴权(登录判断)需要业务代码来实现。
使用场景Redis-based session的一个常用的场景是:
- 用户Login后将用户敏感信息存入Redis Session,并将session id返回,存入浏览器的cookie中
- 随后用户访问其他页面,服务端通过Auth中间件根据cookie中的session id从Redis中查询相应的用户信息。若用户信息Key-Value值存在,说明用户已登录,并且登录未超时,此时允许用户进行后续操作;否则Auth中间件的handler直接Abort并返回401,告知用户未登录(未经授权)。
- https://github.com/gin-contrib/sessions
- https://github.com/gorilla/sessions
- https://github.com/boj/redistore