zoukankan      html  css  js  c++  java
  • 在 Go 语言中使用 Session(一)

    在上一篇博客 理解Cookie和Session 中,我们了解了 Cookie 和 Session 的一些基础知识,也知道了 Session 的基本原理是由服务端保存一份状态信息(以及它的唯一标识符),客户端会通过这个唯一标识符来访问这份状态信息数据。

    整个客户端和服务端的交互过程可以概括为以下三个步骤:

    • 客户端第一次发送请求时,服务端创建 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()
    	})
    }
    

    以上类似的解决方法可用于计算在线用户上。

    至此,我们实现了一个用来在 Web 应用中全局管理 Session 的 SessionManager,定义了用来提供 Session 存储实现 Provider 接口。

    接口的具体实现

    关于针对 Session 接口和 Provider 接口的具体实现,这里就不展开了,有兴趣的读者可参考《Go Web编程》第 6.3 小节的内容

    参考:
    《Go Web 编程》第6章

  • 相关阅读:
    Lampda或Linq中的SqlFunctions.StringConvert()
    Quartz.NET配置
    C#中TransactionScope的使用方法和原理
    .NET中使用Redis(二)
    曲苑杂坛--修改数据库名和文件组名
    曲苑杂坛--修改数据库服务器名称
    系统数据库--修改tempdb的位置
    系统数据库--恢复Master数据库
    TSQL--游标Dem
    TSQL--约束基础和Demo
  • 原文地址:https://www.cnblogs.com/liyutian/p/10287655.html
Copyright © 2011-2022 走看看