Go Session

JunChow520 · · 1698 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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)
}

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:JunChow520

查看原文:Go Session

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1698 次点击  
加入收藏 微博
1 回复  |  直到 2021-11-22 17:25:24
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传