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,此过程可归纳为三个步骤:
- 服务端生成全局唯一的标识符即SessionID
- 服务端开辟数据存储空间
- 将Session全局唯一标识发送给客户端
Session在服务端是如何存储的呢?
服务端可采用哈希表来保存Session内容,一般而言可在内存中创建相应的数据结构,不过一旦断电内存中所有的会话数据将会丢失。因此可将会话数据写入到文件或保存到数据库,虽然会增加I/O开销,但可以实现某种程序的持久化,也更有利于共享。
如何发送SessionID给客户端呢?
可采用两种方式,分别是Cookie和URL重写。
- Cookie
服务端通过设置Set-Cookie
头将SessionID发送到客户端,此后客户端每次请求都会携带此标识符。同时将含有会话数据的Cookie的失效时间设置为0,即浏览器进程有效时间。这种方式的缺陷是如果浏览器禁用了Cookie则无法使用。
Golang中可使用net/http
包提供的SetCookie
函数来设置Cookie。
http.SetCookie(w ResponseWriter, cookie *Cookie)
- 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)
}