扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
本文小编为大家详细介绍“如何用Go实现Session会话管理器”,内容详细,步骤清晰,细节处理妥当,希望这篇“如何用Go实现Session会话管理器”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。
马龙ssl适用于网站、小程序/APP、API接口等需要进行数据传输应用场景,ssl证书未来市场广阔!成为成都创新互联的ssl证书销售渠道,可以享受市场价格4-6折优惠!如果有意向欢迎电话联系或者加微信:18982081108(备注:SSL证书合作)期待与您的合作!
首先必须了解工作原理才能写代码,这里我就稍微说一下,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会话管理器”文章已经介绍完毕,想要掌握这篇文章的知识点还需要大家自己动手实践使用过才能领会,如果想了解更多相关内容的文章,欢迎关注创新互联行业资讯频道。
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流