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  string
	lock        sync.Mutex
	provider    Provider
	maxLifeTime 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 实现接口 Provider

func (pder *Provider) SessionInit(sid string) (session.Session, error) {
	// 根据 sid 创建一个 SessionStore
	pder.lock.Lock()
	defer pder.lock.Unlock()
	v := make(map[interface{}]interface{})
	// 同时更新两个字段
	newsess := &SessionStore{sid: sid, timeAccessed: time.Now(), value: v}
	// list 用于GC
	element := pder.list.PushBack(newsess)
	// 存放 kv
	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
	}
}

// 服务端 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
}

// 回收过期的 cookie

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) < time.Now().Unix() {
			// 更新两者的值

			// 垃圾回收
			pder.list.Remove(element)
			// 删除 map 中的kv
			delete(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 实现的方法:

// 创建 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 == "" {
		// 查看是否为当前客户端注册过名为 gosessionid 的 cookie,如果没有注册过,就为客户端创建一个该 cookie

		// 创建 sessionID
		sid := manager.sessionID()
		// 创建一个 session 接口,这其实是一个 创建完成的 SessionStore ,SessionStore 实现了该接口
		session, _ = manager.provider.SessionInit(sid)
		// 创建 cookie
		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
}

// 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)

	// 在一个单独的 goroutine 中定期调用 GC
	go func() {
		time.Sleep(time.Duration(manager.maxLifeTime))
		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] = value
	err := 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 main

import (
	_ "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] = value
	err := 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 过期的时候,服务器会执行:

// 回收过期的 cookie

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) < time.Now().Unix() {
			// 更新两者的值

			// 垃圾回收
			pder.list.Remove(element)
			// 删除 map 中的kv
			delete(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 服务:
在这里插入图片描述
可以看到已经实现了“冒名顶替”。

全文完,感谢阅读。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

3年功能测试经验,面试想拿到15k很难吗?

一直觉得经验多&#xff0c;无论在哪都能找到满意的工作&#xff0c;但是现实却是给我打了一个大巴掌&#xff01;事后也不会给糖的那种... 个人情况 大概介绍一下个人情况&#xff0c;男&#xff0c;本科&#xff0c;三年多测试工作经验&#xff0c;一毕业因为不成熟的经验以…

【iOS】属性关键字

文章目录 前言一、深拷贝与浅拷贝1、OC的拷贝方式有哪些2. OC对象实现的copy和mutableCopy分别为浅拷贝还是深拷贝&#xff1f;3. 自定义对象实现的copy和mutableCopy分别为浅拷贝还是深拷贝&#xff1f;4. 判断当前的深拷贝的类型&#xff1f;(区别是单层深拷贝还是完全深拷贝…

【原创】H3C交换机链路聚合配置

图示 中间两个交换机&#xff0c;使用两根网线直连&#xff0c;这样本来是10G级联&#xff0c;变成了20G级联。 在默认情况下&#xff0c;这两根线在STP协议下&#xff0c;只有一路是通的&#xff0c;另一路处于备用状态。如果要将这两路都设置为级联&#xff0c;那么还需要…

knife4j 整合 springboot

官方文档&#xff1a;https://doc.xiaominfo.com/knife4j 版本兼容说明&#xff1a;https://doc.xiaominfo.com/docs/quick-start/start-knife4j-version 升级说明&#xff1a;https://doc.xiaominfo.com/docs/upgrading/upgrading-to-v4版本兼容惯关系&#xff1a; springboot…

强大的处理器和接口支持BL304ARM控制器

在智慧医疗领域&#xff0c;BL304可以用于实现医疗设备的智能化、远程监控和数据交换。在智慧电力领域&#xff0c;BL304可以帮助实现电网的智能化管理&#xff0c;提升电力供应的效率。在智慧安防领域&#xff0c;BL304可以实现智能监控、智能门锁等应用&#xff0c;保障安全。…

C# WPF监听USB插入拨出

可以全部监听。好用 private void FormF100WriteCortexLicense_Load(object sender, EventArgs e){this.Text this.Text " " FT_Tools.Program.version;USB USBWatcher new USB();USBWatcher.AddUSBEventWatcher(USBEventHandler, USBEventHandler, new TimeSpa…

高级IO(select、poll、epoll)

在介绍本文之前&#xff0c;先提出一个问题 什么是IO&#xff1f; 等数据拷贝 1.等 - IO事件就绪&#xff08;检测功能成分&#xff09; 2.数据拷贝 高效的IO就是&#xff1a;单位时间&#xff0c;等的比重越小&#xff0c;IO的效率越高 五种IO模型 IO模型&#xff1a; 阻塞式…

【项目设计】高并发内存池(Concurrent Memory Pool)

目录 1️⃣项目介绍 &#x1f359;项目概述 &#x1f359;知识储备 2️⃣内存池介绍 &#x1f359;池化技术 &#x1f359;内存池 &#x1f359;内存池主要解决的问题 &#x1f365;内碎片 &#x1f365;外碎片 &#x1f359;malloc 3️⃣ 定长内存池设计 4️⃣ 项…

Windows docker desktop 基于HyperV的镜像文件迁移到D盘

Docker desktop的HyperV镜像文件&#xff0c;默认是在C盘下 C:\ProgramData\DockerDesktop\vm-data\DockerDesktop.vhdx如果部署的软件较多&#xff0c;文件较大&#xff0c;或者产生日志&#xff0c;甚至数据等&#xff0c;这将会使此文件越来越大&#xff0c;容易导致C盘空间…

Python学习笔记:正则表达式、逻辑运算符、lamda、二叉树遍历规则、类的判断

1.正则表达式如何写&#xff1f; 序号实例说明1.匹配任何字符(除换行符以外)2\d等效于[0-9]&#xff0c;匹配数字3\D等效于[^0-9]&#xff0c;匹配非数字4\s等效于[\t\r\n\f]&#xff0c;匹配空格字符5\S等效于[^\t\r\n\f]&#xff0c;匹配非空格字符6\w等效于[A-Za-z0-9]&…

知识储备--基础算法篇-数组

1.学习 2.数组 2.1第53题-最大子数组和 给你一个整数数组 nums &#xff0c;请你找出一个具有最大和的连续子数组&#xff08;子数组最少包含一个元素&#xff09;&#xff0c;返回其最大和。 子数组 是数组中的一个连续部分。 心得&#xff1a;一直在纠结这个连续的事情&…

【Vue3】组件递归

【Vue3】组件递归 实现效果 通过传入一个数字&#xff0c;实现数字次循环 父组件 <script setup> import { ref } from "vue"; import RecursionMe from "./components/RecursionMe/index.vue";const level ref(0);const add () > level.val…

【计算机知识】Base64 编码说明

一、理论 Base64 是一种基于 64 个可打印字符来表示二进制数据的表示方法&#xff0c;由于 2^664&#xff0c;所以每 6 个比特为一个单元&#xff0c;对应某个可打印字符。 Base64 常用于在通常处理文本数据的场合&#xff0c;表示、传输、存储一些二进制数据&#xff0c;包括…

前端实习第七周周记

前言 第六周没写&#xff0c;是因为第六周的前两天在处理第五周的样本库部分。问题解决一个是嵌套问题&#xff08;因为我用到了递归&#xff09;&#xff0c;还有一个问题在于本机没有问题&#xff0c;打包上线接口404。这个问题我会在这周的总结中说。 第六周第三天才谈好新…

Java“魂牵”京东店铺所有商品数据接口,京东店铺所有商品API接口,京东API接口申请指南

要通过京东的API获取店铺所有商品数据&#xff0c;您可以使用京东开放平台提供的接口来实现。以下是一种使用Java编程语言实现的示例&#xff0c;展示如何通过京东开放平台API获取整店商品数据&#xff1a; 首先&#xff0c;确保您已注册成为京东开放平台的开发者&#xff0c;…

Linux 权限

什么是权限 在Linux下有两种用户&#xff0c;分别是超级用户&#xff08;root&#xff09;和普通用户。超级用户可以在Linux下做任何事情&#xff0c;几乎不受限制&#xff0c;而普通用户一般只能在自己的工作目录下&#xff08;/home/xxx&#xff09;工作&#xff0c;以及在系…

【原创】H3C路由器OSPF测试

网络拓扑图 路由器配置&#xff1a; 路由器1上接了4跟线&#xff0c;分别为这四个接口配置IP地址。 # interface GigabitEthernet0/0/0port link-mode routecombo enable copperip address 2.1.1.2 255.255.255.0 # interface GigabitEthernet0/0/1port link-mode routecombo…

《奥本海默》热映,Sam Altman 会是下个他吗?

撰文&#xff1a;Nathan Gardels 来源&#xff1a;Noema 治理可能摧毁社会的技术。 图片来源&#xff1a;由无界AI生成 电影导演克里斯托弗 - 诺兰&#xff08;Christopher Nolan&#xff09;说&#xff0c;他曾与正在经历“奥本海默时刻”的人工智能科学家交谈过&#xff0c;他…

20. python从入门到精通——Flask框架

目录 安装虚拟环境和Flask 第一个Flask程序 Flask的调试模式 路由 变量规则&#xff1a;当在页面中输出变量的时候就需要遵循变量的规则 构造URL 在route函数中设置http方法 获取静态文件路径 蓝图 模板 Web表单 CSRF 安装虚拟环境和Flask Flask框架主要依赖两个库…

利用 AI 赋能云安全,亚马逊云科技的安全技术创新服务不断赋能开发者

文章分享自亚马逊云科技 Community Builder&#xff1a;李少奕 2023年6月14日&#xff0c;一年一度的亚马逊云科技 re:Inforce 全球大会在美国安纳海姆落下了帷幕。re:Inforce 是亚马逊云科技全球最大的盛会之一&#xff0c;汇集了来自全球各地的安全专家&#xff0c;共同学习、…