gin?session中間件使用及源碼流程分析
概述
一般PC 端網(wǎng)站開(kāi)發(fā)都會(huì)談到Session,服務(wù)端開(kāi)啟Session機(jī)制,客戶(hù)端在第一次訪問(wèn)服務(wù)端時(shí),服務(wù)端會(huì)生成sessionId通過(guò)cookie 機(jī)制回傳到客戶(hù)端保存,之后每次客戶(hù)端訪問(wèn)服務(wù)端時(shí)都會(huì)通過(guò)cookie機(jī)制帶sessionId到服務(wù)端,服務(wù)端通過(guò)解析SessionID找到請(qǐng)求的Session會(huì)話(huà)信息;
Session信息都是保存在服務(wù)器中的,類(lèi)似:SessionID =》 session信息;至于session信息具體內(nèi)容是什么,這個(gè)要根據(jù)具體的業(yè)務(wù)邏輯來(lái)確定,但是普遍是用戶(hù)信息;
服務(wù)端保存Session的方式很多:文件,緩存,數(shù)據(jù)庫(kù)等,所以衍生出來(lái)的session的載體也有很多:redis,文件,mysql,memcached 等等;其中每一種載體都有著自己的優(yōu)劣,根據(jù)不同的業(yè)務(wù)場(chǎng)景可以選取合適的載體;
下面我們主要介紹 redis 作為載體:
Gin中的 Session
gin中Session的實(shí)現(xiàn)主要依賴(lài)于Gin-session中間件實(shí)現(xiàn) (https://github.com/gin-contri... 通過(guò)注入不同的 store 從而實(shí)現(xiàn)不同的載體保存Session信息 :
主要包括:
- cookie-based
- Redis
- memcached
- MongoDB
- memstore
簡(jiǎn)單調(diào)用
創(chuàng)建一個(gè)新的store并將中間件注入到gin的路由器中。需要使用的時(shí)候在HandlerFunc內(nèi)部用 sessions.Default(c)即可獲取到session
// 創(chuàng)建載體方式對(duì)象(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 來(lái)看看 Gin-session 主要工作流程
簡(jiǎn)單調(diào)用
router := gin.Default() // @Todo 創(chuàng)建store對(duì)象 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}) })
創(chuàng)建 store 對(duì)象
底層的 store 創(chuàng)建
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: "/", // 客戶(hù)端的 Path MaxAge: sessionExpire, // 客戶(hù)端的Expires/MaxAge }, DefaultMaxAge: 60 * 20, // 過(guò)期時(shí)間是20分鐘 maxLength: 4096, // 最大長(zhǎng)度 keyPrefix: "session_", // key 前綴 serializer: GobSerializer{}, // 內(nèi)部序列化采用了Gob庫(kù) } // @Todo 嘗試創(chuàng)建連接 _, err := rs.ping() return rs, err }
根據(jù)函數(shù)可以看到,根據(jù)傳入?yún)?shù)(size:連接數(shù), network:連接協(xié)議,address:服務(wù)地址,password:密碼)初始化一個(gè) redis.Pool 對(duì)象,通過(guò)傳入 redis.Pool 對(duì)象 和 一些默認(rèn)的參數(shù)初始化 RediStore 對(duì)象
作為中間件在 gin router層調(diào)用
作為 gin 中間件使用并不復(fù)雜,就是把HandlerFunc放到group.Handlers數(shù)組后面;
// 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() }
下面看看這個(gè)中間件的方法實(shí)現(xiàn)了什么: 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 的實(shí)現(xiàn):
- 創(chuàng)建一個(gè)session對(duì)象(包括:request信息,Store 存儲(chǔ)載體redis RediStore 對(duì)象...)
- 把創(chuàng)建的的session對(duì)象 set 到 *gin.Context 鍵值對(duì)中;key 為一個(gè)定值:DefaultKey(github.com/gin-gonic/contrib/sessions)
- 路由層只會(huì)在初始化的時(shí)候執(zhí)行一次,而且 store 是捆綁在 Session 中,因此每一個(gè) session 都會(huì)指向同一個(gè)store
獲取session實(shí)現(xiàn)
我們可以到在 router 中間件中已經(jīng)創(chuàng)建好 session 對(duì)象并且 set 到對(duì)應(yīng)的 gin.Context 中,那么我們只需要調(diào)用 sessions.Default(c) 出來(lái)即可;
// shortcut to get session func Default(c *gin.Context) Session { return c.MustGet(DefaultKey).(Session) }
注意:返回的類(lèi)型是Session的接口定義,gin.Context 中 set 的是session具體實(shí)現(xiàn);
讀取session值
通過(guò)簡(jiǎn)單的代碼獲取session的值
// @Todo SessionKey 中獲取用戶(hù)信息 session := sessions.Default(c) sessionInfo := session.Get(public.AdminSessionInfoKey)
上面已經(jīng)有提到過(guò)了,sessions.Default 返回的 Session 的接口定義類(lèi),其定義了Get()這個(gè)方法接口,實(shí)際的方法實(shí)現(xiàn)還在session中。
type session struct { name string request *http.Request store Store session *sessions.Session // 實(shí)際內(nèi)部數(shù)據(jù)交換 written bool writer http.ResponseWriter } func (s *session) Get(key interface{}) interface{} { // 通過(guò)s.Session() 獲取具體的session;具體的值保存在 Values 這個(gè)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 }
通過(guò)觀察 session 的結(jié)構(gòu)體, 里面包含著 session sessions.Session 對(duì)象,這個(gè)要跟 之前的 Session 接口定義區(qū)分開(kāi);這里的 sessions.Session 是真正保存的session 對(duì)象;其結(jié)構(gòu)體如下:(gorilla/sessions庫(kù))
// 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] 也變得非常好理解了,其實(shí) Values 這個(gè)屬性其實(shí)是個(gè)map,其中保存的就是我們 set 在session中的具體值;我們繼續(xù)往下走。。。
當(dāng) s.session 是空的時(shí)候,我們就通過(guò) s.store.Get(s.request, s.name) 獲?。?/p>
// s.request 請(qǐng)求 // s.name session名 s.session, err = s.store.Get(s.request, s.name)
注意:s.request: 請(qǐng)求 和 s.name: session名 什么時(shí)候注入的呢? 其實(shí)我們這里可以回顧下上面:
// @Todo 在路由層注入session的時(shí)候 Seesions 方法其實(shí)就初始化了這個(gè)session的name 和其他值,只是保存的session是 nil sessions.Sessions("sessionId", store)
言歸正傳,我們繼續(xù)往下走,上面通過(guò) store.Get 來(lái)獲取 session;因?yàn)檫@里的我們分析的是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) }
我們可以看到:通過(guò) sessions.GetRegistry(r) 獲取到一個(gè) Registry ;然后通過(guò) Registry 的 GET方法獲取一個(gè)session;
我們來(lái)看看他的實(shí)現(xiàn):
// GetRegistry 本質(zhì)就是返回一個(gè) Registry 的一個(gè)結(jié)構(gòu)體 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 }
其實(shí)不難看出 GetRegistry 本質(zhì)上就是返回了一個(gè) Registry 結(jié)構(gòu)體;然后結(jié)合 Get 方法我們可以看出其實(shí) Registry 結(jié)構(gòu)體本質(zhì)上是維護(hù)著一個(gè) key -》 value 的映射關(guān)系; 而其中的 key 就是我們 開(kāi)始在路由注入的 session name , value 就是我們保存的 sessionInfo;
所以我們也可以理解:Registry 的作用就是維護(hù)一個(gè)業(yè)務(wù)session名到對(duì)應(yīng)session的映射,隔離了session。當(dāng)session不存在時(shí),需要調(diào)用 store.New(s.request, name) 來(lái)新建一個(gè)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 初始化一個(gè)業(yè)務(wù)的session session := sessions.NewSession(s, name) // make a copy options := *s.Options session.Options = &options session.IsNew = true // @Todo 根據(jù)session_name讀取cookie中的sessionID if c, errCookie := r.Cookie(name); errCookie == nil { // @Todo 編解碼器對(duì)cookie值進(jìn)行解碼 err = securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...) if err == nil { // @Todo redis中根據(jù)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,然后 根據(jù)SessionID 從 redis 中把我們的session加載出來(lái);
寫(xiě)入 session 值
其實(shí) 寫(xiě)入 和 讀取 差別不是很大:
// @Todo 寫(xiě)值入口 (其實(shí)就是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中也強(qiáng)制過(guò)期 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 如果沒(méi)SessionID 就隨機(jī)生成一個(gè)sessionID (并發(fā)來(lái)的時(shí)候是否會(huì)生成相同SessionID) if session.ID == "" { session.ID = strings.TrimRight(base32.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(32)), "=") } // @Todo 將session的值寫(xiě)入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 根據(jù)session的屬性,寫(xiě)入 cookie (SessionID, path, maxAge等) http.SetCookie(w, sessions.NewCookie(session.Name(), encoded, session.Options)) } return nil }
其實(shí)我們可以看到最后執(zhí)行 save 的最終實(shí)現(xiàn)還是放在了 RediStore 對(duì)象中;也是上面的最后一個(gè)方法,所以我們重點(diǎn)看看最后一個(gè)方法:
- 如果沒(méi)SessionID 就隨機(jī)生成一個(gè)sessionID (并發(fā)來(lái)的時(shí)候是否會(huì)生成相同SessionID)
- 將session的值寫(xiě)入redis
- cookie編碼一下
- 根據(jù)session的屬性,寫(xiě)入 cookie (SessionID, path, maxAge等)
基本完成:留一個(gè)簡(jiǎn)單問(wèn)題,當(dāng)請(qǐng)求并發(fā)的時(shí)候生成 SessionID 是否存在相同?
建議:聯(lián)合gin-session中間件,跟著看。。
以上就是gin session中間件使用及源碼分析的詳細(xì)內(nèi)容,更多關(guān)于gin session中間件的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章

go select編譯期的優(yōu)化處理邏輯使用場(chǎng)景分析

Go語(yǔ)言?xún)?yōu)雅實(shí)現(xiàn)單例模式的多種方式

8種超簡(jiǎn)單的Golang生成隨機(jī)字符串方式分享

go實(shí)現(xiàn)自動(dòng)復(fù)制U盤(pán)小工具demo

Golang基于sync.Once實(shí)現(xiàn)單例的操作代碼