共计 9559 个字符,预计需要花费 24 分钟才能阅读完成。
👨💻 如果对你有帮忙请你点个关注,按赞,谢谢,你们反对就是我更新的能源!
概 述
大家都晓得 session
是 web 利用
在服务器端实现的一种用户和服务器之间认证的解决方案,目前 Go
规范包没有为 session
提供任何反对,本文我将解说 session
的实现原理,和一些常见基于 session
平安产生的进攻问题。
当然有人可能看了会抬杠,说当初大部分不是前后端拆散架构吗?对,你能够应用 JWT
解决你的问题。然而也有一些一体化 web
利用须要 session
,所以我筹备造个轮子。本人造的轮子哪里出问题了,比他人更相熟,有bug
了,还不必求着他人修bug
, 本人修就好了,呵呵哈哈哈,当然这几句话有点皮😜。
需 求
我感觉一名好的程序员,在写程序之前应该列一下需要剖析,整顿一下思路,而后再去写代码。
- 反对内存存储会话数据
- 反对分布式
redis
会话存储 - 会话如果有心跳就主动续命
30
分钟(生命周期) - 提供进攻:
中间人
,会话劫持
,会话重放
等攻打
工作原理
首先必须理解工作原理能力写代码,这里我就略微说一下,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
中的字段不可能间接寻址,官网issue https://github.com/golang/go/issues/3117
其余几个函数:
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/sessionx"
)
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)
}
小 结
举荐还是应用 JWT
这种形式做鉴权,不过也有一体化 web 利用
的session
也不会被这么早淘汰,如果下面有问题,欢送大佬们pr
,还有局部代码没有列出,能够去仓库看看。
相干链接
代码仓库:https://github.com/higker/ses…