整个客户端和服务端的交互过程可以概括为以下三个步骤:
- 客户端第一次发送请求时,服务端创建 Session,并生成唯一标识符 SessionId
- 服务端将 SessionId 发送给客户端
(一般来说有两种常用的方式:Cookie 和 URL 重写) - 客户端再次向服务端发送请求时一并将 SessionId 发送给服务端。
Go 实现 session
在 Go 的标准库中并没有提供对 Sessoin 的实现,所以下面我们通过分析《Go Web编程》一书中的示例来学习一下如何自行实现一个 Session 的功能。
(ps:虽然标准库中没有实现 session,但是有很多 Web 框架都提供了 session 的实现)
实现 Session 主要需要考虑以下几点:
- Session 的创建
- 全局 Session 管理器
- SessionID 的全局唯一性
- Session 的存储(可以存储到内存、文件、数据库等)
- Session 过期处理
下面跟着相应的 go 代码示例分析一下整个设计思路:
定义 Session:
Session 使用的是一种类似散列表的结构(也可能就是散列表)来保存的信息。如果您有任何 Web 开发经验,您应该知道 Session 只有四个操作:设置值,获取值,删除值和获取当前的 SessionID。
因此 Session 接口应该有四种方法来执行这种操作:
type Session interface { Set(key, value interface{}) error //设置Session Get(key interface{}) interface{} //获取Session Delete(key interface{}) error //删除Session SessionID() string //当前SessionID }
可以通过多种方式保存 Session,包括内存,文件和数据库等,所以这里定义了一个 Session 操作接口,不同存储方式的 Session 操作有所不同,实现也不同。
Session 管理器:
我们知道 Session 是保存在服务端的数据,因此我们可以抽象出一个 Provider 接口来表示 Session 管理器的底层结构。Provider 将通过 SessionID 来访问和管理 Session。
type Provider interface { SessionInit(sid string) (Session, error) SessionRead(sid string) (Session, error) SessionDestroy(sid string) error SessionGC(maxLifeTime int64) }
- SessionInit 实现 Session 的初始化,如果成功则返回新的 Session
- SessionRead 返回由相应 sid 表示的 Session,如果不存在,那么将以 sid 为参数调用 SessionInit 方法创建并返回一个新的 Session 变量。
- SessionDestroy 给定一个 sid,删除相应的 Session。
- SessionGC 根据 maxLifeTime 删除过期的 Session 变量
定义好了 Provider 接口之后,我们再写一个注册方法,使得我们可以根据 provider 管理器的名称就能找到其对应的 provider 管理器
var providers = make(map[string]Provider) //注册一个能通过名称来获取的 session provider 管理器 func RegisterProvider(name string, provider Provider) { if provider == nil { panic("session: Register provider is nil") } if _, p := providers[name]; p { panic("session: Register provider is existed") } providers[name] = provider }
接着再把 Provider 封装一下,定义一个全局的 Session 的管理器
type Manager struct { cookieName string //cookie的名称 lock sync.Mutex //锁,保证并发时数据的安全一致 provider Provider //管理session maxLifeTime int64 //超时时间 } func NewManager(providerName, cookieName string, maxLifetime int64) (*Manager, error){ provider, ok := providers[providerName] if !ok { return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", providerName) } //返回一个 Manager 对象 return &Manager{ cookieName: cookieName, maxLifeTime: maxLifetime, provider: provider, }, nil }
然后在 main 包中创建一个全局的 Session 管理器
var globalSession *Manager func init() { globalSession, _ = NewManager("memory", "sessionid", 3600) }
唯一的 Session ID
Session ID是用来识别访问 Web 应用的每一个用户的,因此需要保证它是全局唯一的,示例代码如下:
func (manager *Manager) sessionId() string { b := make([]byte, 32) if _, err := io.ReadFull(rand.Reader, b); err != nil { return "" } return base64.URLEncoding.EncodeToString(b) }
创建 Session
我们需要为每个来访的用户分配或者获取与它相关连的 Session,以便后面根据 Session 信息来验证操作。SessionStart 这个函数就是用来检测是否已经有某个 Session 与当期来访用户发生了关联,如果没有则创建它。
//根据当前请求的cookie中判断是否存在有效的session, 不存在则创建 func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) { //为该方法加锁 manager.lock.Lock() defer manager.lock.Unlock() //获取 request 请求中的 cookie 值 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) //将新的cookie设置到响应中 } else { sid, _ := url.QueryUnescape(cookie.Value) session, _ = manager.provider.SessionRead(sid) } return }
现在我们已经可以通过 SessionStart 方法返回一个满足 Session 接口的变量了。下面通过一个例子来展示一下 Session 的读写操作:
//根据用户名判断是否存在该用户的session,不存在则创建 func login(w http.ResponseWriter, r *http.Request){ sess := globalSession.SessionStart(w, r) r.ParseForm() name := sess.Get("username") if name != nil { sess.Set("username", r.Form["username"]) //将表单提交的username值设置到session中 } }
注销 Session
在 Web 应用中通常有用户退出登录操作,那么当用户退出应用的时候,我们就可以对该用户的 session 数据进行注销。
// SessionDestroy 注销 Session 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) expiredTime := time.Now() newCookie := http.Cookie{ Name: manager.cookieName, Path: "/", HttpOnly: true, Expires: expiredTime, MaxAge: -1, //会话级cookie } http.SetCookie(w, &newCookie) }
现在我们有对 Session 进行 读(Get)、写(Set)、删除(Destroy)操作的方法了,下面结合这三个操作来展示一个示例:
//记录该session被访问的次数 func count(w http.ResponseWriter, r *http.Request) { sess := globalSession.SessionStart(w, r) //获取session实例 createTime := sess.Get("createTime") //获得该session的创建时间 if createTime == nil { sess.Set("createTime", time.Now().Unix()) } else if (createTime.(int64) + 360) < (time.Now().Unix()) { //已过期 //注销旧的session信息,并新建一个session globalSession.SessionDestroy(w, r) sess = globalSession.SessionStart(w, r) } count := sess.Get("countnum") if count == nil { sess.Set("countnum", 1) } else { sess.Set("countnum", count.(int) + 1) } }
删除 Session
接着再来看看如何让 Session 管理器删除 Session。
//在启动函数中开启GC func init() { go globalSession.SessionGC() } func (manager *Manager) SessionGC() { manager.lock.Lock() defer manager.lock.Unlock() manager.provider.SessionGC(manager.maxLifeTime) //使用time包中的计时器功能,它会在session超时时自动调用GC方法 time.AfterFunc(time.Duration(manager.maxLifeTime), func() { manager.SessionGC() }) }