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 中都不存在 键为countnumsessionStore。但是依然会执行:

	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 服务:
在这里插入图片描述
可以看到已经实现了“冒名顶替”。

全文完,感谢阅读。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/117125.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

17.看楼房

目录 Description Input Output Notes 思路 注意事项 C完整代码&#xff08;含详细注释&#xff09; Description 小张在暑假时间进行了暑期社会调查。调查的内容是楼房的颜色如何影响人们的心情。于是他找到了一个楼房从左到右排成一排的小区&#xff0c;这个小区一共有…

51单片机项目(7)——基于51单片机的温湿度测量仿真

本次做的设计&#xff0c;是利用DHT11传感器&#xff0c;测量环境的温度以及湿度&#xff0c;同时具备温度报警的功能&#xff1a;利用两个按键&#xff0c;设置温度阈值的加和减&#xff0c;当所测温度大于温度阈值的时候&#xff0c;蜂鸣器就会响起&#xff0c;进行报警提示。…

安卓 tcp 客户端

安卓 tcp 客户端 Server:8888 是Qt 写的Tcp 服务器 ip 是 192.168.2.103 port是8888 安卓手机运行 kotlin 语法的Tcp Client &#xff0c;连接&#xff0c;收发数据 效果如下图 Tcpclient package com.example.myapplicationimport android.os.Handler import android.os.Loo…

【leetcode 力扣刷题】数学题之计算次幂//次方:快速幂

利用乘法求解次幂问题—快速幂 50. Pow(x, n)372. 超级次方 50. Pow(x, n) 题目链接&#xff1a;50. Pow(x, n) 题目内容&#xff1a; 题目就是要求我们去实现计算x的n次方的功能函数&#xff0c;类似c的power()函数。但是我们不能使用power()函数直接得到答案&#xff0c;那…

ARM Cortex-M 的 SP

文章目录 1、栈2、栈操作3、Cortex-M中的栈4、MDK中的SP操作流程5、Micro-Lib的SP差别1. 使用 Micro-Lib2. 未使用 Micro-Lib 在嵌入式开发中&#xff0c;堆栈是一个很基础&#xff0c;同时也是非常重要的名词&#xff0c;堆栈可分为堆 (Heap) 和栈 (Stack) 。 栈(Stack): 一种…

EG1164大功率同步整流升压模块开源,最高效率97%

EG1164大功率同步整流Boost升压电源模块&#xff0c;最高效率97%&#xff0c;输入电压8~50V&#xff0c;输出电压8~60V可调&#xff0c;最大功率300瓦以上&#xff0c;开关频率219kHz。 白嫖了张嘉立创的彩色丝印券就随便画了个板试试&#xff0c;第一次打彩色丝印。 因为我测…

flutter plugins插件【一】【FlutterJsonBeanFactory】

1、FlutterJsonBeanFactory 在Setting->Tools->FlutterJsonBeanFactory里边自定义实体类的后缀&#xff0c;默认是entity 复制json到粘贴板&#xff0c;右键自己要存放实体的目录&#xff0c;可以看到JsonToDartBeanAction Class Name是实体名字&#xff0c;会默认加上…

基于硬件隔离增强risc-v调试安全1_问题描述

安全之安全(security)博客目录导读 2023 RISC-V中国峰会 安全相关议题汇总 说明&#xff1a;本文参考RISC-V 2023中国峰会如下议题&#xff0c;版权归原作者所有。

每日一题 2511. 最多可以摧毁的敌人城堡数目

难度&#xff1a;简单 翻译&#xff1a;寻找距离最远的 1 和 -1 的组合&#xff0c;要求它们之间只有0 class Solution:def captureForts(self, forts: List[int]) -> int:res, t 0, -1for i, fort in enumerate(forts):if fort -1 or fort 1:if t > 0 and fort ! f…

前端Vue自定义得分构成水平柱形图组件 可用于系统专业门类得分评估分析

引入Vue自定义得分构成水平柱形图组件&#xff1a;cc-horBarChart 随着技术的发展&#xff0c;传统的开发方式使得系统的复杂度越来越高&#xff0c;一个小小的改动或小功能的增加可能会导致整体逻辑的修改&#xff0c;造成牵一发而动全身的情况。为了解决这个问题&#xff0c…

扫盲:常用NoSQL数据库

前言 关系型数据库产品很多&#xff0c;如 MySQL、Oracle、Microsoft SQL Sever 等&#xff0c;但它们的基本模型都是关系型数据模型。 非关系型数据库又称为&#xff1a;NoSQL &#xff0c;没有统一的模型&#xff0c;而且是非关系型的。 常见的 NoSQL 数据库包括键值数据库、…

【前端】Vue2 脚手架模块化开发 -快速入门

&#x1f384;欢迎来到边境矢梦的csdn博文&#x1f384; &#x1f384;本文主要梳理Vue2 脚手架模块化开发 &#x1f384; &#x1f308;我是边境矢梦&#xff0c;一个正在为秋招和算法竞赛做准备的学生&#x1f308; &#x1f386;喜欢的朋友可以关注一下&#x1faf0;&#x…

【MATLAB第70期】基于MATLAB的LightGbm(LGBM)梯度增强决策树多输入单输出回归预测及多分类预测模型(全网首发)

【MATLAB第70期】基于MATLAB的LightGbm(LGBM)梯度增强决策树多输入单输出回归预测及多分类预测模型&#xff08;全网首发&#xff09; 一、学习资料 (LGBM)是一种基于梯度增强决策树(GBDT)算法。 本次研究三个内容&#xff0c;分别是回归预测&#xff0c;二分类预测和多分类预…

系列五、Java操作RocketMQ简单消息之同步消息

一、概述 同步消息的特征是消息发出后会有一个返回值&#xff0c;即RocketMQ服务器收到消息后的一个确认&#xff0c;这种方式非常安全&#xff0c;但是性能上却没有那么高&#xff0c;而且在集群模式下&#xff0c;也是要等到所有的从机都复制了消息以后才会返回&#xff0c;适…

Linux系统Ubuntu以非root用户身份操作Docker的方法

本文介绍在Linux操作系统Ubuntu版本中&#xff0c;通过配置&#xff0c;实现以非root用户身份&#xff0c;进行Docker各项操作的具体方法。 在文章Linux系统Ubuntu配置Docker详细流程&#xff08;https://blog.csdn.net/zhebushibiaoshifu/article/details/132612560&#xff0…

如何使用Puppeteer进行新闻网站数据抓取和聚合

导语 Puppeteer是一个基于Node.js的库&#xff0c;它提供了一个高级的API来控制Chrome或Chromium浏览器。通过Puppeteer&#xff0c;我们可以实现各种自动化任务&#xff0c;如网页截图、PDF生成、表单填写、网络监控等。本文将介绍如何使用Puppeteer进行新闻网站数据抓取和聚…

mac idea启动没反应 无法启动

遇到的问题如下&#xff1a; 启动idea&#xff0c;没反应 无法启动&#xff0c;不论破解还是别的原因&#xff0c;总之无法启动了 应用程序–找到idea–右击显示包内容–Contents–MacOS–打开idea 弹出框提示如下&#xff1a; 双击这个idea可执行文件 1&#xff09;先查看日志…

JS中的new操作符

文章目录 JS中的new操作符一、什么是new&#xff1f;二、new经历了什么过程&#xff1f;三、new的过程分析四、总结 JS中的new操作符 参考&#xff1a;https://www.cnblogs.com/buildnewhomeland/p/12797537.html 一、什么是new&#xff1f; 在JS中&#xff0c;new的作用是通过…

【OpenCV入门】第七部分——图像的几何变换

文章结构 缩放dsize参数实现缩放fx参数和fy参数实现缩放 翻转仿射变换平移旋转倾斜 透视cmath模块 缩放 通过resize()方法可以随意更改图像的大小比例&#xff1a; dst cv2.resize(src, dsize, fx, fy, interpolation)src&#xff1a; 原始图像dsize&#xff1a; 输出图像的…

链表OJ练习(2)

一、分割链表 题目介绍&#xff1a; 思路&#xff1a;创建两个链表&#xff0c;ghead尾插大于x的节点&#xff0c;lhead尾插小于x的节点。先遍历链表。最后将ghead尾插到lhead后面&#xff0c;将大小链表链接。 我们需要在创建两个链表指针&#xff0c;指向两个链表的头节点&…