Session 和 Cookie Cookie Cookie 是由浏览器 维持的,保存在客户端 的一段文本信息,伴随着用户请求和页面在 Web 服务器和浏览器之间传递。通过 Cookie,服务器就可以验证 Cookie 信息、记录用户状态。
Cookie 是有时间限制的,根据生命周期不同分为会话 Cookie 和持久 Cookie:
会话 Cookie :不设置生命周期的默认值,表示这个 Cookie 的生命周期到浏览器关闭为止 ,会话 Cookie 一般保存在内存 中。
持久 Cookie :设置了过期时间时,浏览器就会将 Cookie 保存在硬盘 上,有效期直到超过了过期时间。
Go 设置 Cookie Go 语言中,通过 net/http 包中的 SetCookie 来设置:
1 func SetCookie (w ResponseWriter, cookie *Cookie)
Cookie 结构体结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type Cookie struct { Name string Value string Path string Domain string Expires time.Time RawExpires string MaxAge int Secure bool HttpOnly bool Raw string Unparsed []string }
设置 Cookie 例子:
1 2 3 4 5 6 7 8 9 10 11 expiration := time.Now() expiration = expiration.AddDate(1 , 0 , 0 ) cookie := &http.Cookie{ Name: "username" , Value: "dawn" , Expires: expiration, } http.SetCookie(w, cookie)
Go 读取 Cookie Go 语言中,使用 r.Cookie 根据 name 读取 Cookie:
1 func (r *Request) Cookie (name string ) (*Cookie, error)
还可以通过 r.Cookies 一次性得到所有的 Cookie 信息:
1 func (r *Request) Cookies () []*Cookie
Session Session 是一种服务器端 的机制,服务器使用一种类似于散列表的结构来保存信息。服务器使用 Session id 来标识 Session 信息,它由服务器端产生,相当于一个密钥。可以借助 Cookie 或者 GET 请求 方式传输 Session id。
总结 :
Session 和 Cookie 机制都是为了克服 HTTP 协议的无状态缺陷,用于记录用户状态。
Session 依托 Cookie 实现,将 Session id 保存在 Cookie 中。
Cookie 因为将所有信息都保存在客户端中,发起请求时会携带这些信息,所以有一定的安全隐患。
Go 实现 Session Session 创建过程 服务器端在创建 Session 时会分为三个步骤:
生成一个全局唯一 的标识符 Session id 。
开辟数据存储空间 来存储 Session 信息:
内存 :速度快,系统一旦掉电,所有的会话数据就会丢失。
文件或者数据库 :增加 I/O 开销,但是可以实现某种程度上的 Session 持久化,以及 Session 的共享。
将 Session id 发送给客户端 :
Cookie :服务端通过设置 Set-cookie 头就可以将 Session id 发送给客户端,而客户端此后的每一次请求都会带上这个 Session id。一般情况下,会将包含 Session id 的 Cookie 的过期时间设置为 0。
URL 重写 :返回给用户的页面里的所有的 URL 后面追加 Session id,这样用户在收到响应之后,无论点击响应页面里的哪个链接或提交表单,都会自动带上 Session id。如果客户端禁用了 Cookie,这种方案就是首选。
Go 实现 Session 管理 目前 Go 标准包没有为 Session 提供任何支持,下面手动实现 Session 的管理和创建。
Session 管理器 定义一个全局的 Session 管理器,用于 Session 的管理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type Manager struct { cookieName string lock sync.Mutex provider Provider maxLifeTime int64 } 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{ cookieName: cookieName, provider: provider, maxLifeTime: maxLifeTime, }, nil }
Session 是保存在服务器端的数据,它可以以任何的方式存储,比如存储在内存、数据库或者文件中。因此我们抽象出一个 Provider 接口 ,用以表征 Session 管理器底层存储结构 :
SessionInit
实现 Session 的初始化。
SessionRead
返回 sid 所代表的 Session 变量,如果不存在,那么将以 sid 为参数调用 SessionInit 函数创建并返回一个新的 Session 变量。
SessionDestroy
销毁 sid 对应的 Session 变量。
SessionGC
根据 maxLifeTime 来删除过期的数据。
1 2 3 4 5 6 7 8 9 10 type Provider interface { SessionInit(sid string ) (Session, error) SessionRead(sid string ) (Session, error) SessionDestroy(sid string ) error SessionGC(maxLifeTime int64 ) }
对 Session 的处理基本上就是设置值、读取值、删除值、获取当前 Session id 四种操作,所以 Session 接口 定义如下:
1 2 3 4 5 6 type Session interface { Set(key, value interface {}) error Get(key interface {}) interface {} Delete(key interface {}) error SessionID() string }
全局唯一的 Session id Session id 用来标识每一个用户以及对应的 Session,所以必须是全局唯一的:
1 2 3 4 5 6 7 8 9 func (manager *Manager) sessionId () string { b := make ([]byte , 32 ) if _, err := rand.Read(b); err != nil { return "" } return base64.URLEncoding.EncodeToString(b) }
Session 的创建 SessionStart
用来检测是否已经有某个 Session 与当前来访用户发生了关联,如果没有则创建之
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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 }
Session 销毁 在销毁 Session 时,除了从存储结构中删除 Session 信息之外,还需要设置 Cookie过期。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func (manager *Manager) SessionDestroy (w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie(manager.cookieName) if err != nil || cookie.Value == "" { return } manager.lock.Lock() defer manager.lock.Unlock() manager.provider.SessionDestroy(cookie.Value) expiration := time.Now() cookie = &http.Cookie{ Name: manager.cookieName, Path: "/" , HttpOnly: true , Expires: expiration, MaxAge: -1 , } http.SetCookie(w, cookie) }
过期 Session 清理 GC 利用了 time 包的定时器功能 ,当超时之后调用 GC 清理过期数据。
1 2 3 4 5 6 7 8 9 func (manager *Manager) GC () { manager.lock.Lock() defer manager.lock.Unlock() manager.provider.SessionGC(manager.maxLifeTime) time.AfterFunc(time.Duration(manager.maxLifeTime), func () { manager.GC() }) }
Go 实现 Session 存储 在上一节中,定义了 Session 管理器 session.Manager ,以及两个接口 session.Provider 和 session.Session ,这两个接口用于存储 Session 数据。本节将实现一个基于内存的 Session 存储方式。
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 72 73 74 75 76 77 78 79 80 81 82 package memoryimport ( "container/list" "go-web-demo/ch6/session" "sync" "time" ) type Provider struct { lock sync.Mutex sessions map [string ]*list.Element list *list.List } func (p *Provider) SessionInit (sid string ) (session.Session, error) { p.lock.Lock() defer p.lock.Unlock() v := make (map [interface {}]interface {}, 0 ) newSess := &SessionStore{sid: sid, timeAccessed: time.Now(), value: v} element := p.list.PushFront(newSess) p.sessions[sid] = element return newSess, nil } func (p *Provider) SessionRead (sid string ) (session.Session, error) { if element, ok := p.sessions[sid]; ok { return element.Value.(*SessionStore), nil } sess, err := p.SessionInit(sid) return sess, err } func (p *Provider) SessionDestroy (sid string ) error { if element, ok := p.sessions[sid]; ok { delete (p.sessions, sid) p.list.Remove(element) return nil } return nil } func (p *Provider) SessionGC (maxLifeTime int64 ) { p.lock.Lock() defer p.lock.Unlock() for { element := p.list.Back() if element.Value.(*SessionStore).timeAccessed.Unix()+maxLifeTime < time.Now().Unix() { delete (p.sessions, element.Value.(*SessionStore).sid) p.list.Remove(element) } } } func (p *Provider) SessionUpdate (sid string ) error { p.lock.Lock() defer p.lock.Unlock() if element, ok := p.sessions[sid]; ok { element.Value.(*SessionStore).timeAccessed = time.Now() p.list.MoveToFront(element) return nil } return nil }
Session 接口实现 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 package memoryimport ( "container/list" "time" ) type SessionStore struct { sid string timeAccessed time.Time value map [interface {}]interface {} } var ( p = &Provider{list: list.New()} ) func (st *SessionStore) Set (key, value interface {}) error { st.value[key] = value return p.SessionUpdate(st.sid) } func (st *SessionStore) Get (key interface {}) interface {} { p.SessionUpdate(st.sid) if v, ok := st.value[key]; ok { return v } return nil } func (st *SessionStore) Delete (key interface {}) error { delete (st.value, key) return p.SessionUpdate(st.sid) } func (st *SessionStore) SessionID () string { return st.sid }
初始化以及注册 Provider 初始化以及在 session.Manager 中注册 该基于内存的 Provider。
1 2 3 4 5 6 7 8 9 10 11 package memoryimport ( "container/list" "go-web-demo/ch6/session" ) func init () { p.sessions = make (map [string ]*list.Element, 0 ) session.Register("memory" , p) }
调用 当 import 的时候已经执行了 memory 函数里面的 init 函数,这样就已经注册到 session 管理器中,我们就可以使用了:
1 2 3 4 5 6 7 8 9 10 11 12 import ( "go-web-demo/ch6/session" _ "go-web-demo/ch6/session/providers/memory" ) var globalSessions *session.Managerfunc init () { globalSessions, _ = session.NewManager("memory" , "gosessionid" , 3600 ) go globalSessions.GC() }
防范 Session 劫持 httponly 和 token 第一种方案通过 httponly + token 来预防 Session 劫持:
可以设置 Session id 的值只能由 Cookie 进行设置,同时设置 httponly 为 true,这个属性可以防止通过客户端脚本访问到 Cookie,进而防止攻击者读取到 Cookie 中的 Session id。
在每个请求里面加上 token,然后每一次验证 token,从而保证用户的请求都是唯一性。
间隔生成新的 Sesseion id 第二种方案是给 Session 额外设置一个创建时间 的值,一旦过了一定的时间,就销毁这个 Session id,重新生成 Session。
1 2 3 4 5 6 7 8 9 10 11 expires := 60 createTime := sess.Get("createtime" ) if createTime == nil { sess.Set("createtime" , time.Now().Unix()) } else if createTime.(int64 ) + expires < time.Now().Unix() { globalSessions.SessionDestroy(w, r) sess = globalSessions.SessionStart(w, r) }
这里为 Session 设置了一个值,用于记录生成 Session id 的时间。每次请求都判断是否过期,如果过期,则分配新的 Session id。
总结 上面两个手段的组合可以在实践中消除 session 劫持的风险:
Session id 的频繁改变 ,使得攻击者很难获取到有效的 Session id。
Session 只在 Cookie 中传递,并且设置了 httponly 选项,可以有效阻止通过客户端脚本访问到 Cookie,也可以预防 XSS 攻击获取 Cookie。
还可以设置 MaxAge=0 ,这样 Cookie 就不会存储在浏览器的记录中,随着浏览器关闭 Cookie 也随机消失。