〇、前言 众所周知,http协议是无状态的 ,这对于服务器确认是哪一个客户端在发请求是不可能的,因此为了能确认到,通常方法是让客户端发送请求时带上身份信息。容易想到的方法就是客户端在提交信息时,带上自己的账户和密码。但是这样存在着严重的安全问题,可以改进的方法就是,服务器给一个确定的客户端返回一个唯一 id,客户端将这个 id 保存在本地,每次发送请求时只需要携带着这个 id,就可以做到较好的验证(当然也存在着安全问题,这个后面再说)。
这个方法就是 现今很成熟的 session、cookie 技术。session和cookie的目的相同,都是为了克服http协议无状态的缺陷,但完成的方法不同。session通过cookie ,在客户端保存session id,而将用户的其他会话消息保存在服务端的session对象中。与此相对的,cookie需要将所有信息都保存在客户端 。因此cookie存在着一定的安全隐患,例如本地cookie中保存的用户名密码被破译,或cookie被其他网站收集。
本文将尝试着实现一个成熟的 go session,从而实现会话保持。思维导图如下:
一、架构设计 0、思维导图
1、管理器 1 2 3 4 5 6 7 type Manager struct { cookieName string lock sync.Mutex provider Provider maxLifeTime int64 }
其中 Provider 是一个接口:
1 2 3 4 5 6 7 type Provider interface { SessionInit(sid string ) (Session, error ) SessionRead(sid string ) (Session, error ) SessionDestroy(sid string ) error SessionGC(maxLifeTime int64 ) }
这里又定义了一个Provider 结构体,它实现了 Provider 接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 func (pder *Provider) SessionInit(sid string ) (session.Session, error ) { pder.lock.Lock() defer pder.lock.Unlock() v := make (map [interface {}]interface {}) newsess := &SessionStore{sid: sid, timeAccessed: time.Now(), value: v} element := pder.list.PushBack(newsess) pder.sessions[sid] = element return newsess, nil }func (pder *Provider) SessionRead(sid string ) (session.Session, error ) { if element, ok := pder.sessions[sid]; ok { return element.Value.(*SessionStore), nil } else { sess, err := pder.SessionInit(sid) return sess, err } }func (pder *Provider) SessionDestroy(sid string ) error { if element, ok := pder.sessions[sid]; ok { delete (pder.sessions, sid) pder.list.Remove(element) return nil } return nil }func (pder *Provider) SessionGC(maxlifetime int64 ) { pder.lock.Lock() defer pder.lock.Unlock() for { element := pder.list.Back() if element == nil { break } if (element.Value.(*SessionStore).timeAccessed.Unix() + maxlifetime) < time.Now().Unix() { pder.list.Remove(element) delete (pder.sessions, element.Value.(*SessionStore).sid) } else { break } } }func (pder *Provider) SessionUpdate(sid string ) error { pder.lock.Lock() defer pder.lock.Unlock() if element, ok := pder.sessions[sid]; ok { element.Value.(*SessionStore).timeAccessed = time.Now() pder.list.MoveToFront(element) return nil } return nil }
管理器 Manager 实现的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) { manager.lock.Lock() defer manager.lock.Unlock() cookie, err := r.Cookie(manager.cookieName) if err != nil || cookie.Value == "" { sid := manager.sessionID() session, _ = manager.provider.SessionInit(sid) cookie := http.Cookie{Name: manager.cookieName, Value: url.QueryEscape(sid), Path: "/" , HttpOnly: true , MaxAge: int (manager.maxLifeTime)} http.SetCookie(w, &cookie) } else { sid, _ := url.QueryUnescape(cookie.Value) session, _ = manager.provider.SessionRead(sid) } return }func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie(manager.cookieName) if err != nil || cookie.Value == "" { return } else { manager.lock.Lock() defer manager.lock.Unlock() err := manager.provider.SessionDestroy(cookie.Value) if err != nil { return } expiration := time.Now() cookie := http.Cookie{Name: manager.cookieName, Path: "/" , HttpOnly: true , Expires: expiration, MaxAge: -1 } http.SetCookie(w, &cookie) } }func (manager *Manager) GC() { manager.lock.Lock() defer manager.lock.Unlock() manager.provider.SessionGC(manager.maxLifeTime) time.AfterFunc(time.Second*20 , func () { manager.GC() }) }
2、sessions存放 在 Provider 结构体中:
1 2 sessions map [string ]*list.Element list *list.List
sessions 中存放不同客户端的 session,而 list 中也会同时刷新,它用来回收过期的 session。 每一个session用 SessionStore 结构体来存储。
Session 接口:
1 2 3 4 5 6 7 type Session interface { Set(key, value interface {}) error Get(key interface {}) interface {} Delete(key interface {}) error SessionID() string }
这个接口,由 SessionStore 实现:
1 2 3 4 5 6 7 type SessionStore struct { sid string timeAccessed time.Time value map [interface {}]interface {} }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 func (st *SessionStore) Set(key, value interface {}) error { st.value[key] = value err := pder.SessionUpdate(st.sid) if err != nil { return err } return nil }func (st *SessionStore) Get(key interface {}) interface {} { err := pder.SessionUpdate(st.sid) if err != nil { return nil } if v, ok := st.value[key]; ok { return v } else { return nil } }func (st *SessionStore) Delete(key interface {}) error { delete (st.value, key) err := pder.SessionUpdate(st.sid) if err != nil { return err } return nil }func (st *SessionStore) SessionID() string { return st.sid }
二、实现细节 1、provider 注册表 1 2 var provides = make (map [string ]Provider)
任何一个 Maneger 在创建之前,都需要在 provider 注册表中注册。因此在创建一个全局注册表pder,并注册,这应该是 init 的:
1 2 3 4 5 6 var pder = &Provider{list: list.New()}func init () { pder.sessions = make (map [string ]*list.Element) session.Register("memory" , pder) }
注册器:
1 2 3 4 5 6 7 8 9 func Register (name string , provider Provider) { if provider == nil { panic ("session: Register provide is nil" ) } if _, dup := provides[name]; dup { panic ("session: Register called twice for provide " + name) } provides[name] = provider }
2、全局管理器 1 2 3 4 5 var globalSessions *session.Managerfunc init () { globalSessions, _ = session.NewManager("memory" , "gosessionid" , 3600 ) go globalSessions.GC() }
这个管理器就是一个 cookie 管理器,它只对cookie名字为gosessionid
的 cookie 负责。
1 2 3 4 5 6 7 func NewManager (provideName, cookieName string , maxlifetime int64 ) (*Manager, error ) { provider, ok := provides[provideName] if !ok { return nil , fmt.Errorf("session: unknown provide %q (forgotten import?)" , provideName) } return &Manager{provider: provider, cookieName: cookieName, maxLifeTime: maxlifetime}, nil }
3、案例演示 现在已经初始化好了,就等着客户端访问了。 现在我们写一个很简单的计数器,前端访问的时候,自动+1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func count (c *gin.Context) { sess := globalSessions.SessionStart(c.Writer, c.Request) ct := sess.Get("countnum" ) if ct == nil { err := sess.Set("countnum" , 1 ) if err != nil { return } } else { err := sess.Set("countnum" , ct.(int )+1 ) if err != nil { return } } t, err := template.ParseFiles("template/count.html" ) if err != nil { fmt.Println(err) } c.Writer.Header().Set("Content-Type" , "text/html" ) err = t.Execute(c.Writer, sess.Get("countnum" )) if err != nil { return } }
当中的count.html
这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Count</title > </head > <body > <h1 > Hi. Now count:{{.}}</h1 > </body > </html >
main.go
这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package mainimport ( _ "Go_Web/memory" "Go_Web/session" "fmt" "github.com/gin-gonic/gin" "html/template" "net/http" )var globalSessions *session.Managerfunc init () { globalSessions, _ = session.NewManager("memory" , "gosessionid" ,20 ) go globalSessions.GC() }func count (c *gin.Context) { sess := globalSessions.SessionStart(c.Writer, c.Request) ct := sess.Get("countnum" ) if ct == nil { err := sess.Set("countnum" , 1 ) if err != nil { return } } else { err := sess.Set("countnum" , ct.(int )+1 ) if err != nil { return } } t, err := template.ParseFiles("template/count.html" ) if err != nil { fmt.Println(err) } c.Writer.Header().Set("Content-Type" , "text/html" ) err = t.Execute(c.Writer, sess.Get("countnum" )) if err != nil { return } }func main () { r := gin.Default() r.GET("/count" , count) err := r.Run(":9000" ) if err != nil { return } }
我们把 session 的过期时间设为 20 秒,这样可以 更快的看到过期效果。 现在把服务器启动,来看看整个过程。 编译运行之后,在浏览器访问 count:
看下 cookie: 可以继续点击,这个只要在 20 秒之内点击,cookie 就不回过期,因为每次发送请求都会更新 sessionStore:
1 err := sess.Set("countnum" , ct.(int )+1 )
1 2 3 4 5 6 7 8 9 10 func (st *SessionStore) Set(key, value interface {}) error { st.value[key] = value err := pder.SessionUpdate(st.sid) if err != nil { return err } return nil }
1 2 3 4 5 6 7 8 9 10 11 func (pder *Provider) SessionUpdate(sid string ) error { pder.lock.Lock() defer pder.lock.Unlock() if element, ok := pder.sessions[sid]; ok { element.Value.(*SessionStore).timeAccessed = time.Now() pder.list.MoveToFront(element) return nil } return nil }
不要点击等 20 秒等它过期,再点一下: 可以看到已经过期了,再查看下 cookie: 可以看到 sessionId 并没有变,这是因为就算本地 cookie过期,当发送请求时,服务器依然会拿到这个 cookie。 session 过期的时候,服务器会执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func (pder *Provider) SessionGC(maxlifetime int64 ) { pder.lock.Lock() defer pder.lock.Unlock() for { element := pder.list.Back() if element == nil { break } if (element.Value.(*SessionStore).timeAccessed.Unix() + maxlifetime) < time.Now().Unix() { pder.list.Remove(element) delete (pder.sessions, element.Value.(*SessionStore).sid) } else { break } } }
这意味着,pder 中的list 和 sessions 中都不存在 键为countnum
的sessionStore
。但是依然会执行:
1 2 sid, _ := url.QueryUnescape(cookie.Value) session, _ = manager.provider.SessionRead(sid)
SessionRead():
1 2 3 4 5 6 7 8 func (pder *Provider) SessionRead(sid string ) (session.Session, error ) { if element, ok := pder.sessions[sid]; ok { return element.Value.(*SessionStore), nil } else { sess, err := pder.SessionInit(sid) return sess, err } }
执行SessionRead()的时候,由于 session 已经被删除,只能执行pder.SessionInit(sid)
了,因此,服务器会创建一个和原来一样的 sessionId。之后count()自然就会执行err := sess.Set("countnum", 1)
。
1 2 3 4 5 6 7 8 9 10 11 12 13 ct := sess.Get("countnum" ) if ct == nil { err := sess.Set("countnum" , 1 ) if err != nil { return } } else { err := sess.Set("countnum" , ct.(int )+1 ) if err != nil { return } }
至此,整个过程就完了。
二、session 劫持 session劫持是一种广泛存在的比较严重的安全威胁,在session技术中,客户端和服务端通过session的标识符来维护会话, 但这个标识符很容易就能被嗅探到,从而被其他人利用。它是中间人攻击的一种类型。
这个服务是靠着 sessionid维持的,所以一旦这个 sessionid 泄露,被另一个客户端获取,就可以冒名顶替干一些操作(把过期时间设置长一点)。 首先在 Chrome 中访问服务器的服务,点击到随便一个数字:
然后打开 cookie,复制: 再打开FireFox,随便找一个 cookie 管理器,创建一个 cookie: 保存,直接访问服务器count 服务: 可以看到已经实现了“冒名顶替”。
全文完,感谢阅读。