在上一篇博客 理解Cookie和Session 中,咱们了解了 Cookie 和 Session 的一些基础知识,也知道了 Session 的基本原理是由服务端保存一份状态信息(以及它的惟一标识符),客户端会经过这个惟一标识符来访问这份状态信息数据。html
整个客户端和服务端的交互过程能够归纳为如下三个步骤:数据库
在 Go 的标准库中并无提供对 Sessoin 的实现,因此下面咱们经过分析《Go Web编程》一书中的示例来学习一下如何自行实现一个 Session 的功能。 (ps:虽然标准库中没有实现 session,可是有不少 Web 框架都提供了 session 的实现)编程
实现 Session 主要须要考虑如下几点:安全
下面跟着相应的 go 代码示例分析一下整个设计思路:cookie
Session 使用的是一种相似散列表的结构(也可能就是散列表)来保存的信息。若是您有任何 Web 开发经验,您应该知道 Session 只有四个操做:设置值,获取值,删除值和获取当前的 SessionID。 所以 Session 接口应该有四种方法来执行这种操做: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 是保存在服务端的数据,所以咱们能够抽象出一个 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) }
定义好了 Provider 接口以后,咱们再写一个注册方法,使得咱们能够根据 provider 管理器的名称就能找到其对应的 provider 管理器ide
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是用来识别访问 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 信息来验证操做。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中 } }
在 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。
//在启动函数中开启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() }) }
以上相似的解决方法可用于计算在线用户上。
至此,咱们实现了一个用来在 Web 应用中全局管理 Session 的 SessionManager,定义了用来提供 Session 存储实现 Provider 接口。
关于针对 Session 接口和 Provider 接口的具体实现,这里就不展开了,有兴趣的读者可参考《Go Web编程》第 6.3 小节的内容
参考: 《Go Web 编程》第6章