go Session的实现(一)
〇、前言
众所周知,http协议是无状态的,这对于服务器确认是哪一个客户端在发请求是不可能的,因此为了能确认到,通常方法是让客户端发送请求时带上身份信息。容易想到的方法就是客户端在提交信息时,带上自己的账户和密码。但是这样存在着严重的安全问题,可以改进的方法就是,服务器给一个确定的客户端返回一个唯一 id,客户端将这个 id 保存在本地,每次发送请求时只需要携带着这个 id,就可以做到较好的验证(当然也存在着安全问题,这个后面再说)。
这个方法就是 现今很成熟的 session、cookie 技术。session和cookie的目的相同,都是为了克服http协议无状态的缺陷,但完成的方法不同。session通过cookie,在客户端保存session id,而将用户的其他会话消息保存在服务端的session对象中。与此相对的,cookie需要将所有信息都保存在客户端。因此cookie存在着一定的安全隐患,例如本地cookie中保存的用户名密码被破译,或cookie被其他网站收集。
本文将尝试着实现一个成熟的 go session,从而实现会话保持。
思维导图如下:
一、架构设计
0、思维导图
1、管理器
type Manager struct {cookieName stringlock sync.Mutexprovider ProvidermaxLifeTime int64
}
其中 Provider 是一个接口:
// Provider 接口
type Provider interface {SessionInit(sid string) (Session, error) // SessionInit函数实现Session的初始化,操作成功则返回此新的Session变量SessionRead(sid string) (Session, error) // SessionRead函数返回sid所代表的Session变量.如果不存在,那么将以sid为参数调用SessionInit函数创建并返回一个新的Session变量SessionDestroy(sid string) error // SessionDestroy函数用来销毁sid对应的Session变量SessionGC(maxLifeTime int64) // SessionGC根据maxLifeTime来删除过期的数据
}
这里又定义了一个Provider 结构体,它实现了 Provider 接口:
// Provider 实现接口 Providerfunc (pder *Provider) SessionInit(sid string) (session.Session, error) {// 根据 sid 创建一个 SessionStorepder.lock.Lock()defer pder.lock.Unlock()v := make(map[interface{}]interface{})// 同时更新两个字段newsess := &SessionStore{sid: sid, timeAccessed: time.Now(), value: v}// list 用于GCelement := pder.list.PushBack(newsess)// 存放 kvpder.sessions[sid] = elementreturn 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}
}// 服务端 session 销毁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
}// 回收过期的 cookiefunc (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) < time.Now().Unix() {// 更新两者的值// 垃圾回收pder.list.Remove(element)// 删除 map 中的kvdelete(pder.sessions, element.Value.(*SessionStore).sid)} else {break}}
}func (pder *Provider) SessionUpdate(sid string) error {pder.lock.Lock()defer pder.lock.Unlock()if element, ok := pder.sessions[sid]; ok {// 这里更新也就更新了个时间,这意味着 session 的生命得到了延长element.Value.(*SessionStore).timeAccessed = time.Now()pder.list.MoveToFront(element)return nil}return nil
}
管理器 Manager 实现的方法:
// 创建 Sessionfunc (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 == "" {// 查看是否为当前客户端注册过名为 gosessionid 的 cookie,如果没有注册过,就为客户端创建一个该 cookie// 创建 sessionIDsid := manager.sessionID()// 创建一个 session 接口,这其实是一个 创建完成的 SessionStore ,SessionStore 实现了该接口session, _ = manager.provider.SessionInit(sid)// 创建 cookiecookie := 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
}// Session 重置func (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()err := manager.provider.SessionDestroy(cookie.Value)if err != nil {return}expiration := time.Now()cookie := http.Cookie{Name: manager.cookieName, Path: "/", HttpOnly: true, Expires: expiration, MaxAge: -1}http.SetCookie(w, &cookie)}
}// Session 回收func (manager *Manager) GC() {manager.lock.Lock()defer manager.lock.Unlock()manager.provider.SessionGC(manager.maxLifeTime)// 每 20秒触发一次time.AfterFunc(time.Second*20, func() { manager.GC() })
}
2、sessions存放
在 Provider 结构体中:
sessions map[string]*list.Element // 存放 sessionStores
list *list.List // 用来做gc
sessions 中存放不同客户端的 session,而 list 中也会同时刷新,它用来回收过期的 session。
每一个session用 SessionStore 结构体来存储。
Session 接口:
// Session 接口
type Session interface {Set(key, value interface{}) error // 设置 session 的值Get(key interface{}) interface{} // 获取 session 的值Delete(key interface{}) error // 删除 session 的值SessionID() string // 返回当前 session 的 ID
}
这个接口,由 SessionStore 实现:
// SessionStore 结构体type SessionStore struct {sid string // session id唯一标识timeAccessed time.Time // 最后访问时间value map[interface{}]interface{} // 值
}
// SessionStore 实现 Session 接口func (st *SessionStore) Set(key, value interface{}) error {st.value[key] = valueerr := pder.SessionUpdate(st.sid)if err != nil {return err}return nil
}func (st *SessionStore) Get(key interface{}) interface{} {err := pder.SessionUpdate(st.sid)if err != nil {return nil}if v, ok := st.value[key]; ok {return v} else {return nil}
}func (st *SessionStore) Delete(key interface{}) error {delete(st.value, key)err := pder.SessionUpdate(st.sid)if err != nil {return err}return nil
}func (st *SessionStore) SessionID() string {return st.sid
}
二、实现细节
1、provider 注册表
// provider 注册表
var provides = make(map[string]Provider)
任何一个 Maneger 在创建之前,都需要在 provider 注册表中注册。因此在创建一个全局注册表pder,并注册,这应该是 init 的:
// 创建全局 pder
var pder = &Provider{list: list.New()}
func init() {pder.sessions = make(map[string]*list.Element)session.Register("memory", pder)
}
注册器:
func Register(name string, provider Provider) {if provider == nil {panic("session: Register provide is nil")}if _, dup := provides[name]; dup {panic("session: Register called twice for provide " + name)}provides[name] = provider
}
2、全局管理器
var globalSessions *session.Manager
func init() {globalSessions, _ = session.NewManager("memory", "gosessionid", 3600)go globalSessions.GC()
}
这个管理器就是一个 cookie 管理器,它只对cookie名字为gosessionid
的 cookie 负责。
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
}
3、案例演示
现在已经初始化好了,就等着客户端访问了。
现在我们写一个很简单的计数器,前端访问的时候,自动+1:
func count(c *gin.Context) {sess := globalSessions.SessionStart(c.Writer, c.Request)ct := sess.Get("countnum")if ct == nil {err := sess.Set("countnum", 1)if err != nil {return}} else {// 更新err := sess.Set("countnum", ct.(int)+1)if err != nil {return}}t, err := template.ParseFiles("template/count.html")if err != nil {fmt.Println(err)}c.Writer.Header().Set("Content-Type", "text/html")err = t.Execute(c.Writer, sess.Get("countnum"))if err != nil {return}
}
当中的count.html
这样写:
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Count</title>
</head><body><h1>Hi. Now count:{{.}}</h1>
</body></html>
main.go
这样写:
package mainimport (_ "Go_Web/memory""Go_Web/session""fmt""github.com/gin-gonic/gin""html/template""net/http"
)// 全局 sessions 管理器
var globalSessions *session.Manager// init 初始化func init() {globalSessions, _ = session.NewManager("memory", "gosessionid",20)go globalSessions.GC()
}func count(c *gin.Context) {sess := globalSessions.SessionStart(c.Writer, c.Request)ct := sess.Get("countnum")if ct == nil {err := sess.Set("countnum", 1)if err != nil {return}} else {// 更新err := sess.Set("countnum", ct.(int)+1)if err != nil {return}}t, err := template.ParseFiles("template/count.html")if err != nil {fmt.Println(err)}c.Writer.Header().Set("Content-Type", "text/html")err = t.Execute(c.Writer, sess.Get("countnum"))if err != nil {return}
}func main() {r := gin.Default()r.GET("/count", count)err := r.Run(":9000")if err != nil {return}}
我们把 session 的过期时间设为 20 秒,这样可以 更快的看到过期效果。
现在把服务器启动,来看看整个过程。
编译运行之后,在浏览器访问 count:
看下 cookie:
可以继续点击,这个只要在 20 秒之内点击,cookie 就不回过期,因为每次发送请求都会更新 sessionStore:
err := sess.Set("countnum", ct.(int)+1)
// SessionStore 实现 Session 接口func (st *SessionStore) Set(key, value interface{}) error {st.value[key] = valueerr := pder.SessionUpdate(st.sid)if err != nil {return err}return nil
}
func (pder *Provider) SessionUpdate(sid string) error {pder.lock.Lock()defer pder.lock.Unlock()if element, ok := pder.sessions[sid]; ok {// 这里更新也就更新了个时间,这意味着 session 的生命得到了延长element.Value.(*SessionStore).timeAccessed = time.Now()pder.list.MoveToFront(element)return nil}return nil
}
不要点击等 20 秒等它过期,再点一下:
可以看到已经过期了,再查看下 cookie:
可以看到 sessionId 并没有变,这是因为就算本地 cookie过期,当发送请求时,服务器依然会拿到这个 cookie。
session 过期的时候,服务器会执行:
// 回收过期的 cookiefunc (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) < time.Now().Unix() {// 更新两者的值// 垃圾回收pder.list.Remove(element)// 删除 map 中的kvdelete(pder.sessions, element.Value.(*SessionStore).sid)} else {break}}
}
这意味着,pder 中的list 和 sessions 中都不存在 键为countnum
的sessionStore
。但是依然会执行:
sid, _ := url.QueryUnescape(cookie.Value)session, _ = manager.provider.SessionRead(sid)
SessionRead():
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}
}
执行SessionRead()的时候,由于 session 已经被删除,只能执行pder.SessionInit(sid)
了,因此,服务器会创建一个和原来一样的 sessionId。之后count()自然就会执行err := sess.Set("countnum", 1)
。
ct := sess.Get("countnum")if ct == nil {err := sess.Set("countnum", 1)if err != nil {return}} else {// 更新err := sess.Set("countnum", ct.(int)+1)if err != nil {return}}
至此,整个过程就完了。
二、session 劫持
session劫持是一种广泛存在的比较严重的安全威胁,在session技术中,客户端和服务端通过session的标识符来维护会话, 但这个标识符很容易就能被嗅探到,从而被其他人利用。它是中间人攻击的一种类型。
这个服务是靠着 sessionid维持的,所以一旦这个 sessionid 泄露,被另一个客户端获取,就可以冒名顶替干一些操作(把过期时间设置长一点)。
首先在 Chrome 中访问服务器的服务,点击到随便一个数字:
然后打开 cookie,复制:
再打开FireFox,随便找一个 cookie 管理器,创建一个 cookie:
保存,直接访问服务器count 服务:
可以看到已经实现了“冒名顶替”。
全文完,感谢阅读。
相关文章:

go Session的实现(一)
〇、前言 众所周知,http协议是无状态的,这对于服务器确认是哪一个客户端在发请求是不可能的,因此为了能确认到,通常方法是让客户端发送请求时带上身份信息。容易想到的方法就是客户端在提交信息时,带上自己的账户和密…...

QTableView合并单元格
QtableView的功能 QTableView是Qt框架提供的用于显示表格数据的类。它是基于MVC(模型-视图-控制器)设计模式的一部分,用于将数据模型和界面视图分离。 以下是一些QTableView的主要特点和功能: 1. 显示表格数据: QTa…...

如何使用SpringCloud Eureka 创建单机Eureka Server-注册中心
😀前言 本篇博文是关于使用SpringCloud Eureka 创建单机Eureka Server-注册中心,希望你能够喜欢 🏠个人主页:晨犀主页 🧑个人简介:大家好,我是晨犀,希望我的文章可以帮助到大家&…...

QT连接OpenCV库实现人脸识别
一、关于图像处理的相关类和函数 图像容器:Mat类 读取图像: Mat imread( const String& filename, int flags IMREAD_COLOR ); 功能:读取出图像 参数:图像路径 返回值:读取的图像 命名展示图像的窗口ÿ…...

基于SSM+Vue的网上花店系统
末尾获取源码 开发语言:Java Java开发工具:JDK1.8 后端框架:SSM 前端:采用Vue技术开发 数据库:MySQL5.7和Navicat管理工具结合 服务器:Tomcat8.5 开发软件:IDEA / Eclipse 是否Maven项目&#x…...

两种解法解决 LeetCode 27. 移除元素【C++】
移除元素 27. 移除元素题目:[移除元素](https://leetcode.cn/problems/remove-element/description/)示例和提示:解法:1. 暴力解法 2. 快慢指针 27. 移除元素 题目:移除元素 示例和提示: 解法: 1. 暴力解…...

Vue + Element UI 前端篇(七):功能组件封装
组件封装 为了避免组件代码的臃肿,这里对主要的功能部件进行封装,保证代码的模块化和简洁度。 组件结构 组件封装重构后,试图组件结构如下图所示 代码一览 Home组件被简化,包含导航、头部和主内容三个组件。 Home.vue <te…...

QT QToolBox控件使用详解
本文详细的介绍了QToolBox控件的各种操作,例如:新建界面、添加页签、索引设置当前项、获取当前项的索引、获取当前项窗口、获取索引值是int的窗口、移除索引值项、获取项的数量、获取指定索引值、设置索引项是否激活、获取索引值项是否激活、设置项的图标…...

数学建模--主成分分析法(PCA)的Python实现(
目录 1.算法核心思想: 2.算法核心代码: 3.算法分类效果: 1.算法核心思想: 1.设置降维后主成分的数目为2 2.进行数据降维 3.设置main_factors1个划分类型 4.根据组分中的值进行分类 5.绘制出对应的图像 2.算法核心代码:…...

【数据结构篇】线性表2 —— 栈和队列
前言:上一篇我们介绍了顺序表和链表 (https://blog.csdn.net/iiiiiihuang/article/details/132615465?spm1001.2014.3001.5501), 这一篇我们将介绍栈和队列,栈和队列都是基于顺序表和链表来实现的 目录 栈ÿ…...

万物互联:软件与硬件的协同之道
在当今数字化时代,我们身边的一切似乎都与计算机和互联网有关。从智能手机到智能家居设备,从自动驾驶汽车到工业生产线,无论我们走到哪里,都能看到软件和硬件的协同作用。本文将探讨这种协同作用,解释软件和硬件如何相…...

ping: www.baidu.com: Name or service not known 写了DNS还是不行
环境描述:ESXI平台上,一台Centos7虚拟主机。 问题描述:平台上的其他的虚拟机可以正常ping通,就这台ping IP地址可以通,ping域名解析失败。 排查过程: 1、检查网卡配置文件和/etc/resolv.conf配置文件是否…...

C++中的decltype、std::declval 和 std::decay_t傻傻分不清楚
文章目录 前言它们是什么通俗解释总结 前言 在C中提到推导第一个映入脑海的可能是“模板”,当然有人也可能想到 auto,这些都是和推导相关的语言语法,再比如“完美转发”等等,总是就是他们的类型不用明明白白的写出来,…...

什么是Ubuntu LTS?与常规版本的区别
Ubuntu LTS(Long-Term Support)是Ubuntu操作系统的一个特殊版本,旨在提供更长时间的支持和稳定性。与常规的Ubuntu版本相比,LTS版本在以下几个方面有所不同: 支持周期更长: 使用Ubuntu LTS版本,…...

如何写一个可以找到工作的简历不至于太烂
简历是自己的一个很重要的标签,是获得面试的敲门砖,简历是要时常更新的,否则会错过一些机会。简历也是给自己的正反馈。 方法 ● 模仿,例如Boss,拉钩下面都给你一个案例模板供你参考,但是我觉得其实参考性…...

el-select 使用
案例: /* * label : 界面上展示的是哪个字段,我这里需要展示名称 * value : 绑定的字段,一般是id */<el-selectv-model"Form.BillNumber"placeholder"请选择"change"changeValue($event)"><el-optionv-for"…...

思维导图怎么变成ppt?4个思维导图一键生成ppt的方法
做好的思维导图如何变成一份ppt?本文罗列了4个可行方法,一起来看看吧。 一 直接复制粘贴 这是最简单的方法,虽然这样可能会花费一些时间,但可以确保内容排版和布局与你想要的一致。当然,我们大可使用更高效的方法。…...

3D点云处理:点云投影为2D图像 调平点云(附源码)
文章目录 0. 测试效果1. 基本内容1.1 计算点云位姿1.2 调平点云1.3 点云投影2. 代码实现文章目录:3D视觉个人学习目录微信:dhlddxB站: Non-Stop_0. 测试效果...

mysql 查询优化 、索引失效
查询优化 物理查询优化 通过索引和表连接方式等技术来进行优化,这里重点需要掌握索引的使用 逻辑查询优化 通过SQL 等价变换 提升查询效率,直白一点就是说,换一种查询写法执行效率可能更高 索引失效 计算、函数、类型转换(自动或…...

支付宝pc支付(springboot版),简单配置即可实现支付
概述 支付宝pc支付,只需要修改配置就可以实现支付,0基础小白都可以用。使用springboot编写,简单易用。 详细 DEMO简介 springboot整合支付宝pc支付,仅仅需要少量的配置,就可以实现pc支付。 项目截图 支付流程 用户…...

【Redis专题】Redis持久化、主从与哨兵架构详解
目录 前言课程目录一、Redis持久化1.1 RDB快照(Snapshot):二进制文件基本介绍开启/关闭方式触发方式bgsave的写时复制(COW,Copy On Write)机制优缺点 1.2 AOF(append-only file)&…...

【vue2第十三章】自定义指令 自定义v-loading指令
自定义指令 像 v-html,v-if,v-for都是vue内置指令,而我们也可以封装自定义指令,提升编码效率。 什么是自定义指令? 自己定义的一些指令,可以进行一些dom操作,扩展格外的功能。比如让图片懒加载…...

数据结构--6.3查找算法(静态、动态)(插值查找)
静态查找:数据集合稳定,不需要添加,删除元素的查找操作。 动态查找:数据集合在查找的过程中需要同时添加或删除元素的查找操作。 对于静态查找来说,我们不妨可以用线性表结构组织数据,这样可以使用顺序查找…...

Spring Boot日志基础使用 设置日志级别
然后 我们来说日志 日志在实际开发中还是非常重要的 即可记录项目状态和一些特殊情况发生 因为 我们这里不是将项目 所以 讲的也不会特别深 基本还是将Spring Boot的日志设置或控制这一类的东西 相对业务的领域我们就不涉及了 日志 log 初期最明显的作用在于 开发中 你可以用…...

Playwright for Python:断言
一、支持的断言 Playwright支持以下几种断言: 断言描述expect(locator).to_be_checked()复选框被选中expect(locator).to_be_disabled()元素是禁用状态expect(locator).to_be_editable()元素是可编辑状态expect(locator).to_be_empty()容器是空的expect(locator).…...

websocket--技术文档--spring后台+vue基本使用
阿丹: 给大家分享一个可以用来进行测试websocket的网页,个人觉得还是挺好用的. WebSocket在线测试工具 还有一个小家伙ApiPost也可以进行使用websocket的测试。 本文章只是基本使用--给大家提供思路简单实现!! 使用spring-boot建立一个服…...

day01-ES6新特性以及ReactJS入门
课程介绍 ES6新特性ReactJS入门学习 1、ES6 新特性 1.2、let 和 const 命令 var 之前,我们写js定义变量的时候,只有一个关键字: var var 有一个问题,变量作用域的问题,作用域不可控,就是定义的变量有时会…...

MySQL5.7慢查询实践
总结 获取慢查询SQL 已经执行完的SQL,检查慢查询日志,日志中有执行慢的SQL正在执行中的SQL,show proccesslist;,结果中有执行慢的SQL 慢查询日志关键参数 名称解释Query_time查询消耗时间Time慢查询发生时间 分析慢查询SQL e…...

MySQL数据库的增删改查(进阶)
目录 数据库约束 约束类型 NULL约束 UNIQUE:唯一约束 DEFAULT:默认值约束 PRIMARY KEY:主键约束 FOREIGN KEY:外键约束 表的设计 一对一关系 一对多关系 多对多关系 查询 聚合查询 聚合函数 GROUP BY子句 HAVING …...

韶音骨传导耳机好不好用,韶音的骨传导耳机怎么样
提到韶音骨传导耳机,相信很多人在第一时间会想到韶音OpenRun Pro这一款骨传导耳机,这是在去年韶音新发布的一款骨传导耳机,在佩戴舒适性面做了很多优化,采用了夹紧力道适度的柔韧钛合金材质后挂;发声单元包裹柔软硅胶材…...