Go语言session的创建和管理
来源:云海天教程
时间:2022-12-28 21:35:40 151浏览 收藏
在Golang实战开发的过程中,我们经常会遇到一些这样那样的问题,然后要卡好半天,等问题解决了才发现原来一些细节知识点还是没有掌握好。今天golang学习网就整理分享《Go语言session的创建和管理》,聊聊网络编程,希望可以帮助到正在努力赚钱的你。
前面《Cookie设置与读取》一节我们介绍了 Cookie 的应用,本节我们将讲解 session 的应用,我们知道 session 是在服务器端实现的一种用户和服务器之间认证的解决方案,目前 Go语言标准包没有为 session 提供任何支持,接下来我们将会自己动手来实现 go 版本的 session 管理和创建。session 创建过程
session 的基本原理是由服务器为每个会话维护一份信息数据,客户端和服务端依靠一个全局唯一的标识来访问这份数据,以达到交互的目的。当用户访问 Web 应用时,服务端程序会随需要创建 session,这个过程可以概括为三个步骤:生成全局唯一标识符(sessionid);开辟数据存储空间。一般会在内存中创建相应的数据结构,但这种情况下,系统一旦掉电,所有的会话数据就会丢失,如果是电子商务类网站,这将造成严重的后果。所以为了解决这类问题,可以将会话数据写到文件里或存储在数据库中,当然这样会增加 I/O 开销,但是它可以实现某种程度的 session 持久化,也更有利于 session 的共享;将 session 的全局唯一标示符发送给客户端。
以上三个步骤中,最关键的是如何发送这个 session 的唯一标识这一步上。考虑到 HTTP 协议的定义,数据无非可以放到请求行、头域或 Body 里,所以一般来说会有两种常用的方式:cookie 和 URL 重写。
Cookie 服务端通过设置 Set-cookie 头就可以将 session 的标识符传送到客户端,而客户端此后的每一次请求都会带上这个标识符,另外一般包含 session 信息的 cookie 会将失效时间设置为 0(会话 cookie),即浏览器进程有效时间。至于浏览器怎么处理这个 0,每个浏览器都有自己的方案,但差别都不会太大(一般体现在新建浏览器窗口的时候)。
URL 重写,所谓 URL 重写,就是在返回给用户的页面里的所有的 URL 后面追加 session 标识符,这样用户在收到响应之后,无论点击响应页面里的哪个链接或提交表单,都会自动带上 session 标识符,从而就实现了会话的保持。虽然这种做法比较麻烦,但是,如果客户端禁用了 cookie 的话,此种方案将会是首选。
Go 实现 session 管理
通过上面 session 创建过程的讲解,大家应该对 session 有了一个大体的认识,但是具体到动态页面技术里面,又是怎么实现 session 的呢?下面我们将结合 session 的生命周期(lifecycle),来实现 Go语言版本的 session 管理。session 管理设计
我们知道 session 管理涉及到如下几个因素全局 session 管理器保证 sessionid 的全局唯一性为每个客户关联一个 sessionsession 的存储(可以存储到内存、文件、数据库等)session 过期处理
接下来将讲解一下关于 session 管理的整个设计思路以及相应的 go 代码示例:
session 管理器
定义一个全局的 session 管理器type Manager struct { cookieName string // private cookiename lock sync.Mutex // protects session provider Provider maxLifeTime int64}func NewManager(provideName, cookieName string, maxLifeTime int64) (*Manager, error) { provider, ok := provides[provideName] if !ok { return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName) } return &Manager{provider: provider, cookieName: cookieName, maxLifeTime: maxLifeTime}, nil}Go 实现整个的流程应该也是这样的,在 main 包中创建一个全局的 session 管理器。
var globalSessions *session.Manager
//然后在init函数中初始化
func init() {
globalSessions, _ = NewManager("memory", "gosessionid", 3600)
}
type Provider interface {
SessionInit(sid string) (Session, error)
SessionRead(sid string) (Session, error)
SessionDestroy(sid string) error
SessionGC(maxLifeTime int64)
}
那么 session 接口需要实现什么样的功能呢?有过 Web 开发经验的读者知道,对 Session 的处理基本就设置值、读取值、删除值以及获取当前 sessionID 这四个操作,所以我们的 session 接口也就实现这四个操作。
type Session interface {
Set(key, value interface{}) error // set session value
Get(key interface{}) interface{} // get session value
Delete(key interface{}) error // delete session value
SessionID() string // back current sessionID
}
var provides = make(map[string]Provider)//register 通过提供的名称提供会话。//如果用相同的名称调用两次 register,或者如果 driver 为 nil,它会恐慌。func Register(name string, provider Provider) { if provider == nil { panic("session: Register provider is nil") } if _, dup := provides[name]; dup { panic("session: Register called twice for provider " + name) } provides[name] = provider}
全局唯一的 session ID
session ID 是用来识别访问 Web 应用的每一个用户,因此必须保证它是全局唯一的(GUID),下面代码展示了如何满足这一需求:func (manager *Manager) sessionId() string { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "" } return base64.URLEncoding.EncodeToString(b)}
session 创建
我们需要为每个来访用户分配或获取与他相关连的 session,以便后面根据 session 信息来验证操作。SessionStart 这个函数就是用来检测是否已经有某个 session 与当前来访用户发生了关联,如果没有则创建之。func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) { manager.lock.Lock() defer manager.lock.Unlock() 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) } else { sid, _ := url.QueryUnescape(cookie.Value) session, _ = manager.provider.SessionRead(sid) } return}我们用前面 login 操作来演示 session 的运用:
func login(w http.ResponseWriter, r *http.Request) { sess := globalSessions.SessionStart(w, r) r.ParseForm() if r.Method == "GET" { t, _ := template.ParseFiles("login.gtpl") w.Header().Set("Content-Type", "text/html") t.Execute(w, sess.Get("username")) } else { sess.Set("username", r.Form["username"]) http.Redirect(w, r, "/", 302) }}
操作值:设置、读取和删除
SessionStart 函数返回的是一个满足 session 接口的变量,那么我们该如何用他来对 session 数据进行操作呢?上面的例子中的代码 session.Get("uid") 已经展示了基本的读取数据的操作,现在我们再来看一下详细的操作:
func count(w http.ResponseWriter, r *http.Request) { sess := globalSessions.SessionStart(w, r) createtime := sess.Get("createtime") if createtime == nil { sess.Set("createtime", time.Now().Unix()) } else if (createtime.(int64) + 360) 通过上面的例子可以看到,session 的操作和操作 key/value 数据库类似:Set、Get、Delete 等操作。
因为 session 有过期的概念,所以我们定义了 GC 操作,当访问过期时间满足 GC 的触发条件后将会引起 GC,但是当我们进行了任意一个 session 操作,都会对 session 实体进行更新,都会触发对最后访问时间的修改,这样当 GC 的时候就不会误删除还在使用的 session 实体。
session 重置
我们知道,Web 应用中有用户退出这个操作,那么当用户退出应用的时候,我们需要对该用户的 session 数据进行销毁操作,上面的代码已经演示了如何使用 session 重置操作,下面这个函数就是实现了这个功能://Destroy sessionidfunc (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request){ cookie, err := r.Cookie(manager.cookieName) if err != nil || cookie.Value == "" { return } else { manager.lock.Lock() defer manager.lock.Unlock() manager.provider.SessionDestroy(cookie.Value) expiration := time.Now() cookie := http.Cookie{Name: manager.cookieName, Path: "/", HttpOnly: true, Expires: expiration, MaxAge: -1} http.SetCookie(w, &cookie) }}
session 销毁
我们来看一下 session 管理器如何来管理销毁,只要我们在 Main 启动的时候启动:func init() { go globalSessions.GC()}func (manager *Manager) GC() { manager.lock.Lock() defer manager.lock.Unlock() manager.provider.SessionGC(manager.maxLifeTime) time.AfterFunc(time.Duration(manager.maxLifeTime), func() { manager.GC() })}我们可以看到 GC 充分利用了 time 包中的定时器功能,当超时 maxLifeTime 之后调用 GC 函数,这样就可以保证 maxLifeTime 时间内的 session 都是可用的,类似的方案也可以用于统计在线用户数之类的。
至此 我们实现了一个用来在 Web 应用中全局管理 session 的 SessionManager,定义了用来提供 session 存储实现 Provider 的接口,接下来,我们将通过接口定义来实现一些 Provider,供大家参考学习。
session 存储
上面我们介绍了 session 管理器的实现原理,定义了存储 session 的接口,接下来我们将示例一个基于内存的 session 存储接口的实现,其他的存储方式,大家可以自行参考示例来实现,内存的实现请看下面的例子代码。package memoryimport ( "container/list" "github.com/astaxie/session" "sync" "time")var pder = &Provider{list: list.New()}type SessionStore struct { sid string //session id唯一标示 timeAccessed time.Time //最后访问时间 value map[interface{}]interface{} //session里面存储的值}func (st *SessionStore) Set(key, value interface{}) error { st.value[key] = value pder.SessionUpdate(st.sid) return nil}func (st *SessionStore) Get(key interface{}) interface{} { pder.SessionUpdate(st.sid) if v, ok := st.value[key]; ok { return v } else { return nil }}func (st *SessionStore) Delete(key interface{}) error { delete(st.value, key) pder.SessionUpdate(st.sid) return nil}func (st *SessionStore) SessionID() string { return st.sid}type Provider struct { lock sync.Mutex //用来锁 sessions map[string]*list.Element //用来存储在内存 list *list.List //用来做gc}func (pder *Provider) SessionInit(sid string) (session.Session, error) { pder.lock.Lock() defer pder.lock.Unlock() v := make(map[interface{}]interface{}, 0) newsess := &SessionStore{sid: sid, timeAccessed: time.Now(), value: v} element := pder.list.PushBack(newsess) pder.sessions[sid] = element return newsess, nil}func (pder *Provider) SessionRead(sid string) (session.Session, error) { if element, ok := pder.sessions[sid]; ok { return element.Value.(*SessionStore), nil } else { sess, err := pder.SessionInit(sid) return sess, err } return nil, nil}func (pder *Provider) SessionDestroy(sid string) error { if element, ok := pder.sessions[sid]; ok { delete(pder.sessions, sid) pder.list.Remove(element) return nil } return nil}func (pder *Provider) SessionGC(maxlifetime int64) { pder.lock.Lock() defer pder.lock.Unlock() for { element := pder.list.Back() if element == nil { break } if (element.Value.(*SessionStore).timeAccessed.Unix() + maxlifetime) 上面这个代码实现了一个内存存储的 session 机制。通过 init 函数注册到 session 管理器中。这样就可以方便的调用了。我们如何来调用该引擎呢?请看下面的代码。
import (
"github.com/astaxie/session"
_ "github.com/astaxie/session/providers/memory"
)
var globalSessions *session.Manager//然后在init函数中初始化func init() { globalSessions, _ = session.NewManager("memory", "gosessionid", 3600) go globalSessions.GC()}
预防 session 劫持
session 劫持是一种广泛存在的比较严重的安全威胁,在 session 技术中,客户端和服务端通过 session 的标识符来维护会话,但这个标识符很容易就能被嗅探到,从而被其他人利用。它是中间人攻击的一种类型。下面将通过一个实例来演示会话劫持,希望通过这个实例,能让大家更好地理解 session 的本质。
session 劫持过程
我们写了如下的代码来展示一个 count 计数器:func count(w http.ResponseWriter, r *http.Request) { sess := globalSessions.SessionStart(w, r) ct := sess.Get("countnum") if ct == nil { sess.Set("countnum", 1) } else { sess.Set("countnum", (ct.(int) + 1)) } t, _ := template.ParseFiles("count.gtpl") w.Header().Set("Content-Type", "text/html") t.Execute(w, sess.Get("countnum"))}count.gtpl 的代码如下所示:
Hi. Now count:{{.}}
然后我们在浏览器里面刷新可以看到如下内容:图:浏览器端显示 count 数
随着刷新,数字将不断增长,当数字显示为 6 的时候,打开浏览器(以 chrome 为例)的 cookie 管理器,可以看到类似如下的信息:
图:获取浏览器端保存的 cookie
下面这个步骤最为关键:打开另一个浏览器(这里我们打开了 firefox 浏览器),复制 chrome 地址栏里的地址到新打开的浏览器的地址栏中。然后打开 firefox 的 cookie 模拟插件,新建一个 cookie,把按上图中 cookie 内容原样在 firefox 中重建一份:
图:模拟 cookie
回车后,大家将看到如下内容:
图:劫持 session 成功
可以看到虽然换了浏览器,但是我们却获得了 sessionID,然后模拟了 cookie 存储的过程。这个例子是在同一台计算机上做的,不过即使换用两台来做,其结果仍然一样。此时如果交替点击两个浏览器里的链接你会发现它们其实操纵的是同一个计数器。
不必惊讶,此处 firefox 盗用了 chrome 和 goserver 之间的维持会话的钥匙,即 gosessionid,这是一种类型的“会话劫持”。在 goserver 看来,它从 http 请求中得到了一个 gosessionid,由于 HTTP 协议的无状态性,它无法得知这个 gosessionid 是从 chrome 那里“劫持”来的,它依然会去查找对应的 session,并执行相关计算。与此同时 chrome 也无法得知自己保持的会话已经被“劫持”。
session 劫持防范
cookieonly 和 token
通过上面 session 劫持的简单演示可以了解到 session 一旦被其他人劫持,就非常危险,劫持者可以假装成被劫持者进行很多非法操作。那么如何有效的防止 session 劫持呢?其中一个解决方案就是 sessionID 的值只允许 cookie 设置,而不是通过 URL 重置方式设置,同时设置 cookie 的 httponly 为 true,这个属性是设置是否可通过客户端脚本访问这个设置的 cookie,第一这个可以防止这个 cookie 被 XSS 读取从而引起 session 劫持,第二 cookie 设置不会像 URL 重置方式那么容易获取 sessionID。
第二步就是在每个请求里面加上 token,实现类似前面章节里面讲的防止 form 重复递交类似的功能,我们在每个请求里面加上一个隐藏的 token,然后每次验证这个 token,从而保证用户的请求都是唯一性。
h := md5.New()salt:="astaxie%^7&8888"io.WriteString(h,salt+time.Now().String())token:=fmt.Sprintf("%x",h.Sum(nil))if r.Form["token"]!=token{ //提示登录}sess.Set("token",token)
间隔生成新的 SID
还有一个解决方案就是,我们给 session 额外设置一个创建时间的值,一旦过了一定的时间,我们销毁这个 sessionID,重新生成新的 session,这样可以一定程度上防止 session 劫持的问题。createtime := sess.Get("createtime")if createtime == nil { sess.Set("createtime", time.Now().Unix())} else if (createtime.(int64) + 60) session 启动后,我们设置了一个值,用于记录生成 sessionID 的时间。通过判断每次请求是否过期(这里设置了 60 秒)定期生成新的 ID,这样使得攻击者获取有效 sessionID 的机会大大降低。
上面两个手段的组合可以在实践中消除 session 劫持的风险,一方面,由于 sessionID 频繁改变,使攻击者难有机会获取有效的 sessionID;另一方面,因为 sessionID 只能在 cookie 中传递,然后设置了 httponly,所以基于 URL 攻击的可能性为零,同时被 XSS 获取 sessionID 也不可能。最后,由于我们还设置了 MaxAge=0,这样就相当于 session cookie 不会留在浏览器的历史记录里面。
文中关于golang的知识介绍,希望对你的学习有所帮助!若是受益匪浅,那就动动鼠标收藏这篇《Go语言session的创建和管理》文章吧,也可关注golang学习网公众号了解相关技术文章。
-
215 收藏
-
399 收藏
-
458 收藏
-
151 收藏
-
169 收藏
-
202 收藏
-
199 收藏
-
145 收藏
-
168 收藏
-
165 收藏
-
473 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 507次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习