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支付。 项目截图 支付流程 用户…...
WEB3全栈开发——面试专业技能点P2智能合约开发(Solidity)
一、Solidity合约开发 下面是 Solidity 合约开发 的概念、代码示例及讲解,适合用作学习或写简历项目背景说明。 🧠 一、概念简介:Solidity 合约开发 Solidity 是一种专门为 以太坊(Ethereum)平台编写智能合约的高级编…...
有限自动机到正规文法转换器v1.0
1 项目简介 这是一个功能强大的有限自动机(Finite Automaton, FA)到正规文法(Regular Grammar)转换器,它配备了一个直观且完整的图形用户界面,使用户能够轻松地进行操作和观察。该程序基于编译原理中的经典…...
【电力电子】基于STM32F103C8T6单片机双极性SPWM逆变(硬件篇)
本项目是基于 STM32F103C8T6 微控制器的 SPWM(正弦脉宽调制)电源模块,能够生成可调频率和幅值的正弦波交流电源输出。该项目适用于逆变器、UPS电源、变频器等应用场景。 供电电源 输入电压采集 上图为本设计的电源电路,图中 D1 为二极管, 其目的是防止正负极电源反接, …...
Windows安装Miniconda
一、下载 https://www.anaconda.com/download/success 二、安装 三、配置镜像源 Anaconda/Miniconda pip 配置清华镜像源_anaconda配置清华源-CSDN博客 四、常用操作命令 Anaconda/Miniconda 基本操作命令_miniconda创建环境命令-CSDN博客...
MySQL 索引底层结构揭秘:B-Tree 与 B+Tree 的区别与应用
文章目录 一、背景知识:什么是 B-Tree 和 BTree? B-Tree(平衡多路查找树) BTree(B-Tree 的变种) 二、结构对比:一张图看懂 三、为什么 MySQL InnoDB 选择 BTree? 1. 范围查询更快 2…...
Python 训练营打卡 Day 47
注意力热力图可视化 在day 46代码的基础上,对比不同卷积层热力图可视化的结果 import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pypl…...
如何配置一个sql server使得其它用户可以通过excel odbc获取数据
要让其他用户通过 Excel 使用 ODBC 连接到 SQL Server 获取数据,你需要完成以下配置步骤: ✅ 一、在 SQL Server 端配置(服务器设置) 1. 启用 TCP/IP 协议 打开 “SQL Server 配置管理器”。导航到:SQL Server 网络配…...
CTF show 数学不及格
拿到题目先查一下壳,看一下信息 发现是一个ELF文件,64位的 用IDA Pro 64 打开这个文件 然后点击F5进行伪代码转换 可以看到有五个if判断,第一个argc ! 5这个判断并没有起太大作用,主要是下面四个if判断 根据题目…...
EasyRTC音视频实时通话功能在WebRTC与智能硬件整合中的应用与优势
一、WebRTC与智能硬件整合趋势 随着物联网和实时通信需求的爆发式增长,WebRTC作为开源实时通信技术,为浏览器与移动应用提供免插件的音视频通信能力,在智能硬件领域的融合应用已成必然趋势。智能硬件不再局限于单一功能,对实时…...
精益数据分析(98/126):电商转化率优化与网站性能的底层逻辑
精益数据分析(98/126):电商转化率优化与网站性能的底层逻辑 在电子商务领域,转化率与网站性能是决定商业成败的核心指标。今天,我们将深入解析不同类型电商平台的转化率基准,探讨页面加载速度对用户行为的…...
