如何用Go實現Session會話管理器
本文小編為大家詳細介紹“如何用Go實現Session會話管理器”,內容詳細,步驟清晰,細節處理妥當,希望這篇“如何用Go實現Session會話管理器”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學習新知識吧。
工作原理
首先必須了解工作原理才能寫代碼,這里我就稍微說一下,session
是基于cookie
實現的,一個session
對應一個uuid
也是sessionid
,在服務器創建一個相關的數據結構,然后把這個sessionid
通過cookie
讓瀏覽器保存著,下次瀏覽器請求過來了就會有sessionid
,然后通過sessionid
獲取這個會話的數據。
代碼實現
依賴關系
上面是設計的相關依賴關系圖,session
是一個獨立的結構體,GlobalManager
是整體的會話管理器負責數據持久化,過期會話垃圾回收工作??,storage
是存儲器接口,因為我們要實現兩種方式存儲會話數據或者以后要增加其他持久化存儲,所以必須需要接口抽象支持,memory
和redis
是存儲的具體實現。
storage
接口
package?sessionx //?session?storage?interface type?storage?interface?{ ????Read(s?*Session)?error ????Create(s?*Session)?error ????Update(s?*Session)?error ????Remove(s?*Session)?error }
storage
就9行代碼,是具體的會話數據操作動作的抽象,全部參數使用的是session
這個結構的指針,如果處理異常了就即錯即返回
。
為什么把函數簽名的形參
使用指針類型的,這個我想看的懂人應該知道這是為什么了????
memoryStore
結構體
type?memoryStore?struct?{ ????sync.Map }
memoryStore
結構體里面就嵌入sync.Map
結構體,一開始是使用的map
這種,但是后面發現在并發讀寫然后加sync.Mutex
鎖????,性能還不如直接使用sync.Map
速度快。sync.Map
用來做K:V
存儲的,也就是sessionid
對應session data
的。
實現storage
具體方法如下:
func?(m?*memoryStore)?Read(s?*Session)?error?{ ????if?ele,?ok?:=?m.Load(s.ID);?ok?{ ??????//?bug?這個不能直接?s?=?ele? ??????s.Data?=?ele.(*Session).Data ??????return?nil ????} ????//?s?=?nil ????return?fmt.Errorf("id?`%s`?not?exist?session?data",?s.ID) }
讀取數據的時候先將持久化的數據讀出來然后賦值給本次會話的session
。
注意:在go的map
中的struct
中的字段不能夠直接尋址
其他幾個函數:
func?(m?*memoryStore)?Create(s?*Session)?error?{ ??????m.Store(s.ID,?s) ??????return?nil } func?(m?*memoryStore)?Remove(s?*Session)?error?{ ??????m.Delete(s.ID) ??????return?nil } func?(m?*memoryStore)?Update(s?*Session)?error?{ ??????if?ele,?ok?:=?m.Load(s.ID);?ok?{ ????????//?為什么是交換data?因為我們不確定上層是否擴容換了地址 ????????ele.(*Session).Data?=?s.Data ????????ele.(*Session).Expires?=?s.Expires ????????//m.sessions[s.ID]?=?ele ????????return?nil ??????} ??????return?fmt.Errorf("id?`%s`?updated?session?fail",?s.ID) }
這句話代碼沒有什么好說的,寫過go
都能看得懂。
垃圾回收:
func?(m?*memoryStore)?gc()?{ ????//?recycle?your?trash?every?10?minutes ????for?{ ????????time.Sleep(time.Minute?*?10) ????????m.Range(func(key,?value?interface{})?bool?{ ????????????if?time.Now().UnixNano()?>=?value.(*Session).Expires.UnixNano()?{ ??????????????m.Delete(key) ????????????} ????????????return?true ????????}) ????????runtime.GC() ????????//?log.Println("gc?running...") ????} }
比較會話過期時間,過期就刪除會話,以上就是內存存儲的實現。
redisStore
結構體
type?redisStore?struct?{ ????sync.Mutex ????sessions?*redis.Client } func?(rs?*redisStore)?Read(s?*Session)?error?{ ????sid?:=?fmt.Sprintf("%s:%s",?mgr.cfg.RedisKeyPrefix,?s.ID) ????bytes,?err?:=?rs.sessions.Get(ctx,?sid).Bytes() ????if?err?!=?nil?{ ??????return?err ????} ????if?err?:=?rs.sessions.Expire(ctx,?sid,?mgr.cfg.TimeOut).Err();?err?!=?nil?{ ??????return?err ????} ????if?err?:=?decoder(bytes,?s);?err?!=?nil?{ ??????return?err ????} ????//?log.Println("redis?read:",?s) ????return?nil } func?(rs?*redisStore)?Create(s?*Session)?error?{ ????return?rs.setValue(s) } func?(rs?*redisStore)?Update(s?*Session)?error?{ ????return?rs.setValue(s) } func?(rs?*redisStore)?Remove(s?*Session)?error?{ ????return?rs.sessions.Del(ctx,?fmt.Sprintf("%s:%s",?mgr.cfg.RedisKeyPrefix,?s.ID)).Err() } func?(rs?*redisStore)?setValue(s?*Session)?error?{ ????bytes,?err?:=?encoder(s) ????if?err?!=?nil?{ ????????return?err ????} ????err?=?rs.sessions.Set(ctx,?fmt.Sprintf("%s:%s",?mgr.cfg.RedisKeyPrefix,?s.ID),?bytes,?mgr.cfg.TimeOut).Err() ????return?err }
代碼也就50行左右,很簡單就是通過redis
客戶端對數據進行持久化操作,把本地的會話數據提供encoding/gob
序列化成二進制寫到redis
服務器上存儲,需要的時候再反序列化出來。
那么問題來了,會有人問了,redis沒有并發問題嗎?
??????????: 那我肯定會回答,你在問這個問題之前我不知道你有沒有了解過redis
???
Redis
并發競爭指的是多個Redis
客戶端同時set key
引起的并發問題,Redis
是一種單線程機制的NoSQL
數據庫,所以Redis
本身并沒有鎖的概念。
但是多客戶端同時并發寫同一個key
,一個key
的值是1
,本來按順序修改為2,3,4
,最后key
值是4
,但是因為并發去寫key
,順序可能就變成了4,3,2
,最后key
值就變成了2
。
我這個庫當前也就一個客戶端,如果你部署到多個機子,那就使用setnx(key, value)
來實現分布式鎖,我當前寫的這個庫沒有提供分布式鎖,具體請自行google
。
manager
結構體
type?storeType?uint8 const?( ????//?memoryStore?store?type ????M?storeType?=?iota ????//?redis?store?type ????R ????SessionKey?=?"session-id" ) //?manager?for?session?manager type?manager?struct?{ ????cfg???*Configs ????store?storage } func?New(t?storeType,?cfg?*Configs)?{ ????switch?t?{ ????case?M: ????????//?init?memory?storage ????????m?:=?new(memoryStore) ????????go?m.gc() ????????mgr?=?&manager{cfg:?cfg,?store:?m} ????case?R: ????????//?parameter?verify ????????validate?:=?validator.New() ????????if?err?:=?validate.Struct(cfg);?err?!=?nil?{ ????????????panic(err.Error()) ????????} ????????//?init?redis?storage ????????r?:=?new(redisStore) ????????r.sessions?=?redis.NewClient(&redis.Options{ ????????????Addr:?????cfg.RedisAddr, ????????????Password:?cfg.RedisPassword,?//?no?password?set ????????????DB:???????cfg.RedisDB,???????//?use?default?DB ????????????PoolSize:?int(cfg.PoolSize),?//?connection?pool?size ????????}) ????????//?test?connection ????????timeout,?cancelFunc?:=?context.WithTimeout(context.Background(),?8*time.Second) ????????defer?cancelFunc() ????????if?err?:=?r.sessions.Ping(timeout).Err();?err?!=?nil?{ ????????????panic(err.Error()) ????????} ????????mgr?=?&manager{cfg:?cfg,?store:?r} ????default: ??????panic("not?implement?store?type") ????} }
manager
結構體也就兩個字段,一個存放我們全局配置信息,一個我們實例化不同的持久化存儲的存儲器,其他代碼就是輔助性的代碼,不細說了。
Session
結構體
這個結構體是對應著瀏覽器會話的結構體,設計原則是一個id
對應一個session
結構體。
type?Session?struct?{ ????//?會話ID ????ID?string ????//?session超時時間 ????Expires?time.Time ????//?存儲數據的map ????Data?map[interface{}]interface{} ????_w???http.ResponseWriter ????//?每個session對應一個cookie ????Cookie?*http.Cookie }
具體操作函數:
//?Get?Retrieves?the?stored?element?data?from?the?session?via?the?key func?(s?*Session)?Get(key?interface{})?(interface{},?error)?{ ????err?:=?mgr.store.Read(s) ????if?err?!=?nil?{ ????????return?nil,?err ????} ????s.refreshCookie() ????if?ele,?ok?:=?s.Data[key];?ok?{ ????????return?ele,?nil ????} ????return?nil,?fmt.Errorf("key?'%s'?does?not?exist",?key) } //?Set?Stores?information?in?the?session func?(s?*Session)?Set(key,?v?interface{})?error?{ ????lock["W"](func()?{ ????????if?s.Data?==?nil?{ ??????????s.Data?=?make(map[interface{}]interface{},?8) ????????} ????????s.Data[key]?=?v ????}) ??s.refreshCookie() ??return?mgr.store.Update(s) } //?Remove?an?element?stored?in?the?session func?(s?*Session)?Remove(key?interface{})?error?{ ??s.refreshCookie() ????lock["R"](func()?{ ????????delete(s.Data,?key) ????}) ??return?mgr.store.Update(s) } //?Clean?up?all?data?for?this?session func?(s?*Session)?Clean()?error?{ ????s.refreshCookie() ????return?mgr.store.Remove(s) } //?刷新cookie?會話只要有操作就重置會話生命周期 func?(s?*Session)?refreshCookie()?{ ????s.Expires?=?time.Now().Add(mgr.cfg.TimeOut) ????s.Cookie.Expires?=?s.Expires ????//?這里不是使用指針 ????//?因為這里我們支持redis?如果web服務器重啟了 ????//?那么session數據在內存里清空 ????//?從redis讀取的數據反序列化地址和重新啟動的不一樣 ????//?所有直接數據拷貝 ????http.SetCookie(s._w,?s.Cookie) }
上面是幾個函數是,會話的數據操作函數,refreshCookie()
是用來刷新瀏覽器cookie
信息的,因為我在設計的時候只有瀏覽器有心跳也就是有操作數據的時候,管理器就默認為這個瀏覽器會話還是活著的,會自動同步更新cookie
過期時間,這個更新過程可不是光刷新cookie
就完事的了,持久化的話的數據過期時間也一樣更新了。
Handler方法
//?Handler?Get?session?data?from?the?Request func?Handler(w?http.ResponseWriter,?req?*http.Request)?*Session?{ ????//?從請求里面取session ????var?session?Session ????session._w?=?w ????cookie,?err?:=?req.Cookie(mgr.cfg.Cookie.Name) ????if?err?!=?nil?||?cookie?==?nil?||?len(cookie.Value)?<=?0?{ ????????return?createSession(w,?cookie,?&session) ????} ????//?ID通過編碼之后長度是73位 ????if?len(cookie.Value)?>=?73?{ ????????session.ID?=?cookie.Value ????????if?mgr.store.Read(&session)?!=?nil?{ ????????????return?createSession(w,?cookie,?&session) ????????} ????????//?防止web服務器重啟之后redis會話數據還在 ????????//?但是瀏覽器cookie沒有更新 ????????//?重新刷新cookie ????????//?存在指針一致問題,這樣操作還是一塊內存,所有我們需要復制副本 ????????_?=?session.copy(mgr.cfg.Cookie) ????????session.Cookie.Value?=?session.ID ????????session.Cookie.Expires?=?session.Expires ????????http.SetCookie(w,?session.Cookie) ????} ??????//?地址一樣不行?。?! ??????//?log.Printf("mgr.cfg.Cookie?pointer:%p?\n",?mgr.cfg.Cookie) ??????//?log.Printf("session.cookie?pointer:%p?\n",?session.Cookie) ??????return?&session } func?createSession(w?http.ResponseWriter,?cookie?*http.Cookie,?session?*Session)?*Session?{ ??????//?init?session?parameter ??????session.ID?=?generateUUID() ??????session.Expires?=?time.Now().Add(mgr.cfg.TimeOut) ??????_?=?mgr.store.Create(session) ??????//?重置配置cookie模板 ??????session.copy(mgr.cfg.Cookie) ??????session.Cookie.Value?=?session.ID ??????session.Cookie.Expires?=?session.Expires ??????http.SetCookie(w,?session.Cookie) ??????return?session }
Handler
函數是從http
請求里面讀取到sessionid
然后從持久化層讀取數據然后實例化一個session
結構體的函數,沒有啥好說的,注釋寫上面了。
安全防御問題
首先我還是那句話:不懂攻擊,怎么做防守
。
那我們先說說這個問題怎么產生的:
中間人攻擊
(Man-in-the-MiddleAttack
,簡稱MITM攻擊
)是一種間接
的入侵攻擊,這種攻擊模式是通過各種技術手段將受入侵者控制的一臺計算機虛擬放置在網絡連接中的兩臺通信計算機之間,這臺計算機就稱為中間人
。
這個過程,正常用戶在通過瀏覽器訪問我們編寫的網站,但是這個時候有個hack
通過arp
欺騙,把路由器的流量劫持到他的電腦上,然后黑客通過一些特殊的軟件抓包你的網絡請求流量信息,在這個過程中如果你sessionid
如果存放在cookie
中,很有可能被黑客提取處理,如果你這個時候登錄了網站,這是黑客就拿到你的登錄憑證,然后在登錄進行重放
也就是使用你的sessionid
,從而達到訪問你賬戶相關的數據目的。
func?(s?*Session)?MigrateSession()?error?{ ????//?遷移到新內存?防止會話一致引發安全問題 ????//?這個問題的根源在?sessionid?不變,如果用戶在未登錄時拿到的是一個?sessionid,登錄之后服務端給用戶重新換一個?sessionid,就可以防止會話固定攻擊了。 ????s.ID?=?generateUUID() ????newSession,?err?:=?deepcopy.Anything(s) ????if?err?!=?nil?{ ????????return?errors.New("migrate?session?make?a?deep?copy?from?src?into?dst?failed") ????} ????newSession.(*Session).ID?=?s.ID ????newSession.(*Session).Cookie.Value?=?s.ID ????newSession.(*Session).Expires?=?time.Now().Add(mgr.cfg.TimeOut) ????newSession.(*Session)._w?=?s._w ????newSession.(*Session).refreshCookie() ????//?新內存開始持久化 ????//?log.Printf("old?session?pointer:%p?\n",?s) ????//?log.Printf("new?session?pointer:%p?\n",?newSession.(*Session)) ????//log.Println("MigrateSession:",?newSession.(*Session)) ????return?mgr.store.Create(newSession.(*Session)) }
如果大家寫過Java
語言,都應該使用過springboot
這個框架,如果你看過源代碼,那就知道這個框架里面的session
安全策略有一個migrateSession
選項,表示在登錄成功之后,創建一個新的會話,然后講舊的session
中的信息復制到新的session
中。
我參照他的策略,也同樣在我這個庫里面實現了,在用戶匿名訪問的時候是一個sessionid
,當用戶成功登錄之后,又是另外一個sessionid
,這樣就可以有效避免會話固定攻擊。
使用的時候也可以隨時使用通過MigrateSession進行調用
,這個函數一但被調用,原始數據和id
全部被刷新了,內存地址也換了,可以看我的源代碼。
使用演示
package?main import?( ????"fmt" ????"log" ????"net/http" ????"time" ????sessionx?"github.com/higker/sesssionx" ) var?( ????//?配置信息 ????cfg?=?&sessionx.Configs{ ??????????TimeOut:????????time.Minute?*?30, ??????????RedisAddr:??????"127.0.0.1:6379", ??????????RedisDB:????????0, ??????????RedisPassword:??"redis.nosql", ??????????RedisKeyPrefix:?sessionx.SessionKey, ??????????PoolSize:???????100, ??????????Cookie:?&http.Cookie{ ????????????Name:?????sessionx.SessionKey, ????????????Path:?????"/", ????????????Expires:??time.Now().Add(time.Minute?*?30),?//?TimeOut ????????????Secure:???false, ????????????HttpOnly:?true, ????????}, ????} ) func?main()?{ ????//?R表示redis存儲?cfg是配置信息 ??sessionx.New(sessionx.R,?cfg) ????http.HandleFunc("/set",?func(writer?http.ResponseWriter,?request?*http.Request)?{ ????????session?:=?sessionx.Handler(writer,?request) ????????session.Set("K",?time.Now().Format("2006?01-02?15:04:05")) ????????fmt.Fprintln(writer,?"set?time?value?succeed.") ????}) ????http.HandleFunc("/get",?func(writer?http.ResponseWriter,?request?*http.Request)?{ ????????session?:=?sessionx.Handler(writer,?request) ????????v,?err?:=?session.Get("K") ????????if?err?!=?nil?{ ????????????fmt.Fprintln(writer,?err.Error()) ????????????return ????????} ????????fmt.Fprintln(writer,?fmt.Sprintf("The?stored?value?is?:?%s",?v)) ????}) ????http.HandleFunc("/migrate",?func(writer?http.ResponseWriter,?request?*http.Request)?{ ????????session?:=?sessionx.Handler(writer,?request) ????????err?:=?session.MigrateSession() ????????if?err?!=?nil?{ ????????????log.Println(err) ????????} ????????fmt.Fprintln(writer,?session) ????}) ????_?=?http.ListenAndServe(":8080",?nil) }
讀到這里,這篇“如何用Go實現Session會話管理器”文章已經介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領會,如果想了解更多相關內容的文章,歡迎關注蝸牛博客行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:niceseo99@gmail.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。
評論