美文网首页
Go Session

Go Session

作者: JunChow520 | 来源:发表于2021-04-04 16:29 被阅读0次

    Golang官方没有提供Session标准库,但net/http包存在函数可以方便地使用。

    实现Session功能

    • 服务端可通过内存、Redis、数据库等存储Seesion数据
    • 可以通过Cookie将唯一SessionID发送给客户端
    • Session支持常用的增删改查操作
    • 支持单机内存存储
    • 支持分布式存储
    • 支持分布式Redis存储

    Cookie虽然一定程度上解决了“状态保持”的需求,但由于Cookie本身最大仅支持4096字节的内容,同时Cookie是保存在客户端的,存在被拦截或窃取的可能。因此,需要一种新的方式,能够支持更多内容,保存在服务器以提高安全性,这就是Session。


    Session和Cookie的目的相同,都是为了弥补HTTP协议无状态的缺陷。

    Session和Cookie都是用来保存客户端状态信息的手段,不同之处在于Cookie是存储在客户端浏览器方便客户端请求时使用,Session是存储在服务端用于存储客户端连接状态信息的。从存储的数据类型来看,Cookie仅支持存储字符串,Session可支持多种数据类型。

    Session会话的原意是指有始有终的一系列动作或消息。

    Session的基本原理是由服务器为每个会话维护一份信息数据,客户端和服务端依靠一个全局的唯一标识来访问这份数据,以达到交互的目的。当用户访问Web应用时,服务端程序会随着需要创建Session,此过程可归纳为三个步骤:

    1. 服务端生成全局唯一的标识符即SessionID
    2. 服务端开辟数据存储空间
    3. 将Session全局唯一标识发送给客户端

    Session在服务端是如何存储的呢?

    服务端可采用哈希表来保存Session内容,一般而言可在内存中创建相应的数据结构,不过一旦断电内存中所有的会话数据将会丢失。因此可将会话数据写入到文件或保存到数据库,虽然会增加I/O开销,但可以实现某种程序的持久化,也更有利于共享。

    如何发送SessionID给客户端呢?

    可采用两种方式,分别是Cookie和URL重写。

    1. Cookie

    服务端通过设置Set-Cookie头将SessionID发送到客户端,此后客户端每次请求都会携带此标识符。同时将含有会话数据的Cookie的失效时间设置为0,即浏览器进程有效时间。这种方式的缺陷是如果浏览器禁用了Cookie则无法使用。

    Golang中可使用net/http包提供的SetCookie函数来设置Cookie。

    http.SetCookie(w ResponseWriter, cookie *Cookie)
    
    1. URL重写

    URL重写是在返回给用户页面的URL后追加SessionID,当客户端接收到响应后无论是点击链接还是提交表单都会自动携带SessionID,从而实现会话的保持。这种做法虽然麻烦,如果客户端仅用了Cookie则是首选。


    Session设计的核心对象和职责

    对象 职责
    SessionID 负责表示客户端或用户
    Server 服务保存Session内容
    HTTP 负责传递SessionID
    Cookie 负责保存SessionID

    Session设计关注点

    • 全局的Session管理器
    • 保存SessionID全局唯一性
    • 为每个客户关联一个SessionID
    • Session内容的存储方式
    • Session的过期处理

    创建Session操作接口,统计对Session数据的增删改查操作。

    $ vim ./session/session.go
    
    package session
    
    //ISession 操作接口
    //Session数据结构为散列表kv
    type ISession interface {
        //Set 设置
        Set(key, value interface{}) error
        //Get 获取
        Get(key interface{}) interface{}
        //Delete 删除
        Delete(key interface{}) error
        //SessionID
        SessionID() string
    }
    

    创建Session存储接口,由于Session是保存在服务端的数据,可存储在内存、数据库、文件中。

    $ vim ./session/provider.go
    
    package session
    
    //IProvider Session管理接口
    //提供 Session 存储,Session存储方式接口
    type IProvider interface {
        //初始化:Session初始化以获取Session
        Init(sid string) (ISession, error)
        //读取:根据SessionID获取Session内容
        Read(sid string) (ISession, error)
        //销毁:根据SessionID删除Session内容
        Destroy(sid string) error
        //回收:根据过期时间删除Session
        GC(maxAge int64)
    }
    
    //providers Provider管理器集合
    var providers = make(map[string]IProvider)
    
    //Register 根绝Provider管理器名称获取Provider管理器
    func Register(name string, provider IProvider) {
        if provider == nil {
            panic("provider register: provider is nil")
        }
        if _, ok := providers[name]; ok {
            panic("provider register: provider already exists")
        }
        providers[name] = provider
    }
    

    创建Session管理器

    $ vim ./session/manager.go
    
    package session
    
    import (
        "crypto/rand"
        "encoding/base64"
        "io"
        "log"
        "net/http"
        "net/url"
        "sync"
        "time"
    )
    
    //Manager 封装Provider
    type Manager struct {
        mutex      sync.Mutex //互斥锁
        provider   IProvider  //Session存储方式
        cookieName string     //Cookie名称
        maxAge     int64      //过期时间
    }
    
    //NewManager 实例化Session管理器
    func NewManager(providerName, cookieName string, maxAge int64) *Manager {
        provider, ok := providers[providerName]
        if !ok {
            return nil
        }
        return &Manager{provider: provider, cookieName: cookieName, maxAge: maxAge}
    }
    
    //SessionID 生成全局唯一Session标识用于识别每个用户
    func (m *Manager) SessionID() string {
        buf := make([]byte, 32)
    
        _, err := io.ReadFull(rand.Reader, buf)
        if err != nil {
            return ""
        }
    
        return base64.URLEncoding.EncodeToString(buf)
    }
    
    //Start 根据当前请求中的COOKIE判断是否存在有效的Session,不存在则创建。
    func (m *Manager) Start(w http.ResponseWriter, r *http.Request) ISession {
        //添加互斥锁
        m.mutex.Lock()
        defer m.mutex.Unlock()
        //获取Cookie
        cookie, err := r.Cookie(m.cookieName)
        log.Printf("%v", cookie)
        if err != nil || cookie.Value == "" {
            //创建SessionID
            sid := m.SessionID()
            //Session初始化
            session, _ := m.provider.Init(sid)
            //设置Cookie到Response
            http.SetCookie(w, &http.Cookie{
                Name:     m.cookieName,
                Value:    url.QueryEscape(sid),
                Path:     "/",
                HttpOnly: true,
                MaxAge:   int(m.maxAge),
            })
            return session
        } else {
            //从Cookie获取SessionID
            sid, _ := url.QueryUnescape(cookie.Value)
            //获取Session
            session, _ := m.provider.Read(sid)
            return session
        }
    }
    
    //Destroy 注销Session
    func (m *Manager) Destroy(w http.ResponseWriter, r *http.Request) {
        //从请求中读取Cookie值
        cookie, err := r.Cookie(m.cookieName)
        if err != nil || cookie.Value == "" {
            return
        }
        //添加互斥锁
        m.mutex.Lock()
        defer m.mutex.Unlock()
        //销毁Session内容
        m.provider.Destroy(cookie.Value)
        //设置客户端Cookie立即过期
        http.SetCookie(w, &http.Cookie{
            Name:     m.cookieName,
            Path:     "/",
            HttpOnly: true,
            MaxAge:   -1,
            Expires:  time.Now(),
        })
    }
    
    //GC 销毁Session
    func (m *Manager) GC() {
        //添加互斥锁
        m.mutex.Lock()
        defer m.mutex.Unlock()
        //设置过期时间销毁Seesion
        m.provider.GC(m.maxAge)
        //添加计时器当Session超时自动销毁
        time.AfterFunc(time.Duration(m.maxAge), func() {
            m.GC()
        })
    }
    

    实现基于内存的Session操作接口

    $ vim ./session/memory/store.go
    
    package memory
    
    import "time"
    
    //Store
    type Store struct {
        sid  string                      //Store唯一标识StoreID
        data map[interface{}]interface{} //Store存储的值
        time time.Time                   //最后访问时间
    }
    
    //Set
    func (s *Store) Set(key, value interface{}) error {
        s.data[key] = value
        memory.Update(s.sid)
        return nil
    }
    
    //Get
    func (s *Store) Get(key interface{}) interface{} {
        memory.Update(s.sid)
        value, ok := s.data[key]
        if ok {
            return value
        }
        return nil
    }
    
    //Delete
    func (s *Store) Delete(key interface{}) error {
        delete(s.data, key)
        memory.Update(s.sid)
        return nil
    }
    
    //SessionID
    func (s *Store) SessionID() string {
        return s.sid
    }
    

    实现基于内存的Session存储接口

    $ vim ./session/memory/memory.go
    
    package memory
    
    import (
        "container/list"
        "gfw/session"
        "sync"
        "time"
    )
    
    //Memory Session内存存储实现的Memory
    type Memory struct {
        mutex sync.Mutex               //互斥锁
        list  *list.List               //用于GC
        data  map[string]*list.Element //用于存储在内存
    }
    
    func (m *Memory) Init(sid string) (session.ISession, error) {
        //加锁
        m.mutex.Lock()
        defer m.mutex.Unlock()
        //创建Session
        store := &Store{sid: sid, data: make(map[interface{}]interface{}, 0), time: time.Now()}
        elem := m.list.PushBack(store)
        m.data[sid] = elem
        return store, nil
    }
    
    func (m *Memory) Read(sid string) (session.ISession, error) {
        ele, ok := m.data[sid]
        if ok {
            return ele.Value.(*Store), nil
        }
        return m.Init(sid)
    }
    
    func (m *Memory) Destroy(sid string) error {
        ele, ok := m.data[sid]
        if ok {
            delete(m.data, sid)
            m.list.Remove(ele)
        }
        return nil
    }
    
    func (m *Memory) GC(maxAge int64) {
        m.mutex.Lock()
        defer m.mutex.Unlock()
        for {
            ele := m.list.Back()
            if ele == nil {
                break
            }
            session := ele.Value.(*Store)
            if session.time.Unix()+maxAge >= time.Now().Unix() {
                break
            }
            m.list.Remove(ele)
            delete(m.data, session.sid)
        }
    }
    
    func (m *Memory) Update(sid string) error {
        m.mutex.Lock()
        defer m.mutex.Unlock()
    
        ele, ok := m.data[sid]
        if ok {
            ele.Value.(*Store).time = time.Now()
            m.list.MoveToFront(ele)
        }
    
        return nil
    }
    
    var memory = &Memory{list: list.New()}
    
    func init() {
        memory.data = make(map[string]*list.Element, 0)
        session.Register("memory", memory)
    }
    

    相关文章

      网友评论

          本文标题:Go Session

          本文链接:https://www.haomeiwen.com/subject/zsxxkltx.html