【ASP.NET Core】自定义Session的存储方式

news/2024/5/14 17:14:05/文章来源:https://www.cnblogs.com/tcjiaan/p/16677969.html

在开始今天的表演之前,老周先跟大伙伴们说一句:“中秋节快乐”。

今天咱们来聊一下如何自己动手,实现会话(Session)的存储方式。默认是存放在分布式内存中。由于HTTP消息是无状态的,所以,为了让服务器能记住用户的一些信息,就用到了会话。但会话数据毕竟是临时性的,不宜长久存放,所以它会有过期时间。过期了数据就无法使用。比较重要的数据一般会用数据库来长久保存,会话一般放些状态信息。比如你登录了没?你刚才刷了几个贴子?

每一次会话的建立都要分配一个唯一的标识,可以叫 Session ID,或叫 Session Key。为了让服务器与客户端的会话保持一致的上下文,服务器在分配了新会话后,会在响应消息中设置一个 Cookie,里面包含会话标识(一般是加密的)。客户端在发出请求时会携带这个 Cookie,到了服务器上就可以验证是否在同一个会话中进行的通信。Cookie的过期时间也有可能与服务器上缓存的会话的过期时间不一致。此时应以服务器上的数据为准,哪怕客户端携带的 Cookie 还没过期。只要服务器缓存的会话过期,保存标识的 Cookie 也相应地变为无效。

 由于会话仅仅是些临时数据,所以在存储方式上,你拥有可观的 DIY 空间。只要脑洞足够大,你就能做出各种存储方案——存内存中,存文件中,存某些流中,存数据库中……多款套餐,任君选择。

ASP.NET Core 或者说面向整个 .NET ,服务容器和依赖注入为程序扩展提供了许多便捷性。不管怎么扩展,都是通过自行实现一些接口来达到目的。就拿今天要做的存储 Session 数据来说,也是有两个关键接口要实现。

接口一:ISessionStore。这个接口的实现类型会被添加到服务容器中用于依赖注入。它只要求你实现一个方法:

ISession Create(string sessionKey, TimeSpan idleTimeout, TimeSpan ioTimeout, Func<bool> tryEstablishSession, bool isNewSessionKey);

sessionKey:会话标识。

idleTimeout:会话过期时间。

ioTimeout:读写会话的过期时间。如果你觉得你实现的读写操作不花时间,也可以忽略不处理它。

tryEstablishSession:这是个委托,返回 bool。主要检查能不能设置会话,在 ISession.Set 方法实现时可以调用它,要是返回 false,就抛异常。

isNewSessionKey:表示当前会话是不是新建立的,还是已有的。

这个Create方法的实现会引出第二个接口。

接口二:ISession。此接口实现 Session 读写的核心逻辑,前面的 ISessionStore 只是负责返回 ISession 罢了。ISession 的实现类型不需要添加到服务容器中。原因就是刚说的,因为 ISessionStore 已经在容器中了,用它就能获得 ISession 了,所以 ISession 就没必要再放进容器中了。

ISession 接口要实现的成员比较多。

1、IsAvailable 属性。只读,布尔类型。它用来表示这个 Session 能不能加载到数据,可不可用。如果返回 false,表示这个 Session 加载不到数据,用不了。

2、Id 属性。字符串类型,只读。这个返回当前 Session 的标识。

3、Keys 属性。返回当前 Session 中数据的键集合。这个和字典数据一样的道理,Session 也是用字典形式的访问方式。Key 是字符串,Value 是字节数组。

4、Clear 方法。清空当前 Session 的数据项。只是清空数据,不是干掉会话本身。

5、CommitAsync 方法。调用它保存 Session 数据,这个就是靠我们自己实现了,存文件或存内存,或存数据库。

6、LoadAsync 方法。加载 Session。这也是我们自己实现,从数据库中加载?内存中加载?文件中加载?

7、Remove 方法。根据 Key 删除某项会话数据,不是删除会话本身。

8、Set 方法。设置会话的数据项,就像字典中的 dict[key] = value。

9、TryGetValue 方法。获取与给定 Key 对应的数据。类似字典对象的 dict[key]。

 

为了简单,老周这里就只是实现一个用静态字典变量保存 Session 的例子。嗯,也就是保存在内存中。

1、实现 ISession 接口。

    public class CustSession : ISession{#region 私有字段private readonly string _sessionId;private readonly CustSessionDataManager _dataManager;private readonly TimeSpan _idleTimeout, _ioTimeout;private readonly Func<bool> _tryEstablishSession;private readonly bool _isNewId;// 这个字段表示是否成功加载数据private bool _isLoadSuccessed = false;// 当前正在使用的会话数据private SessionData _currentData;#endregion// 构造函数public CustSession(string sessionId,       // 会话标识TimeSpan idleTimeout,   // 过期时间TimeSpan ioTimeout,     // 读写过期时间bool isNewId,           // 是否为新会话// 这个委托表示能否设置会话Func<bool> tryEstablishSession,// 用于管理会话数据的自定义类
                CustSessionDataManager dataManager){_sessionId = sessionId;_idleTimeout = idleTimeout;_ioTimeout = ioTimeout;_isNewId = isNewId;_tryEstablishSession = tryEstablishSession;_dataManager = dataManager;_currentData = new();}public bool IsAvailable{get{// 尝试加载一次
                LoadCore();return _isLoadSuccessed;}}public string Id => _sessionId;public IEnumerable<string> Keys => _currentData?.Data?.Keys ?? Enumerable.Empty<string>();public void Clear(){_currentData.Data?.Clear();}public Task CommitAsync(CancellationToken cancellationToken = default){_currentData.CreateTime = DateTime.Now;_currentData.Expires = _currentData.CreateTime + _idleTimeout;SessionData newData = new();newData.CreateTime = _currentData.CreateTime;newData.Expires = _currentData.Expires;// 复制数据foreach(string k in _currentData.Data.Keys){newData.Data[k] = _currentData.Data[k];}// 添加新记录_dataManager.SessionDataList[_sessionId] = newData;return Task.CompletedTask;}public Task LoadAsync(CancellationToken cancellationToken = default){LoadCore();return Task.CompletedTask;}// 内部方法private void LoadCore(){// 条件1:还没加载过数据// 条件2:会话不是新的,新建会话不用加载if (_isNewId){return;}if (_isLoadSuccessed)return;if (_currentData.Data == null){_currentData.Data = new Dictionary<string, byte[]>();}// 临时变量SessionData? tdata = _dataManager.SessionDataList.FirstOrDefault(k => k.Key == _sessionId).Value;if (tdata != null){_currentData.CreateTime = tdata.CreateTime;_currentData.Expires = tdata.Expires;// 复制数据foreach(string k in tdata.Data.Keys){_currentData.Data[k] = tdata.Data[k];}_isLoadSuccessed = true;}}public void Remove(string key){LoadCore();_currentData.Data.Remove(key);}public void Set(string key, byte[] value){if (_tryEstablishSession() == false){throw new InvalidOperationException();}if (_currentData.Data == null){_currentData.Data = new Dictionary<string, byte[]>();}_currentData.Data.Add(key, value);}public bool TryGetValue(string key, [NotNullWhen(true)] out byte[]? value){value = null;LoadCore();return _currentData.Data.TryGetValue(key, out value);}}

构造函数的参数基本是接收从 ISessionStore.Create方法处获得的参数。

这里涉及两个自定义的类:

第一个是 SessionData,负责存会话,关键信息有创建时间和过期时间,以及会话数据(用字典表示)。存储过期时间是方便后面实现清理——过期的删除。

    internal class SessionData{/// <summary>/// 会话创建时间/// </summary>public DateTime CreateTime { get; set; }/// <summary>/// 会话过期时间/// </summary>public DateTime Expires { get; set; }/// <summary>/// 会话数据/// </summary>public IDictionary<string, byte[]> Data { get; set; } = new Dictionary<string, byte[]>();}

我们的服务器肯定不会只有一个人访问,肯定会有很多 Session,所以自定义一个 CustSessionDataManager 类,用来管理一堆 SessionData。

    public class CustSessionDataManager{private readonly static Dictionary<string, SessionData> sessionDatas = new();internal IDictionary<string, SessionData> SessionDataList{get{CheckAndRemoveExpiredItem();return sessionDatas;}}/// <summary>/// 扫描并清除过期的会话/// </summary>private void CheckAndRemoveExpiredItem(){var now = DateTime.Now;foreach(string key in sessionDatas.Keys){SessionData data = sessionDatas[key];if(data.Expires < now)sessionDatas.Remove(key);}}}

CustSessionDataManager 待会儿会把它放进服务容器中,用于注入其他对象中使用。SessionDataList 属性获取已缓存的 Session 列表,字典结构,Key 是 Session ID,Value是SessionData实例。

老周这里的删除方案是每当访问 SessionDataList 属性时就调用一次 CheckAndRemoveExpiredItem 方法。这个方法会扫描所有已缓存的会话数据,找到过期的就删除。这个是为了省事,如果你认为这样不太好,也可以写个后台服务,用 Timer 来控制每隔一段时间清理一次数据,也可以。只要你开动脑子,啥方案都行。

 

好了,下面轮到实现 ISessionStore 了。

    public class CustSessionStore : ISessionStore{// 用于接收依赖注入private readonly CustSessionDataManager _dataManager;public CustSessionStore(CustSessionDataManager manager){_dataManager = manager;}public ISession Create(string sessionKey, TimeSpan idleTimeout, TimeSpan ioTimeout, Func<bool> tryEstablishSession, bool isNewSessionKey){return new CustSession(sessionKey, idleTimeout, ioTimeout, isNewSessionKey, tryEstablishSession, _dataManager);}}

核心代码就是 Create 方法里的那一句。

刚才我为啥说要把 CustSessionDataManager 也放进服务容器呢,你看,这就用上了,在 CustSessionStore 的构造函数中就可以直接获取了。

 

最后一步,咱封装一套扩展方法,就像 ASP.NET Core 里面 AddSession、AddRazorPages 那样,只要简单调用就行。

    public static class CustSessionExtensions{public static IServiceCollection AddCustSession(this IServiceCollection services, Action<SessionOptions> options){services.AddOptions();services.Configure(options);services.AddDataProtection();
            services.AddSingleton<CustSessionDataManager>();services.AddTransient<ISessionStore, CustSessionStore>();
            return services;}public static IServiceCollection AddCustSession(this IServiceCollection services){return services.AddCustSession(opt => { });}}

因为服务器在响应时要对 Cookie 加密,所以要依赖数据保护功能,因此记得调用 AddDataProtection 扩展方法。另外的两行,就是向服务容器添加我们刚写的类型。

 

好了,回到 Program.cs,在应用程序初始化过程中,我们就可以用上面的扩展方注册自定义 Session 功能。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCustSession(opt =>
{// 设置过期时间opt.IdleTimeout = TimeSpan.FromSeconds(4);
});
var app = builder.Build();

为了能快速看到过期效果,我设定过期时间为 4 秒。


测试一下。

app.UseSession();
app.MapGet("/", (HttpContext context) =>
{ISession session = context.Session;string? val = session.GetString("mykey");if (val == null){// 设置会话session.SetString("mykey", "官仓老鼠大如斗");return "你是首次访问,已设置会话";}return $"欢迎回来\n会话:{val}";
});app.Run();

请大伙伴们记住:在任何要使用 Session 的中间件/终结点之前,一定要调用 UseSession 方法。这样才能把 ISessionFeature 添加到 HttpContext 对象中,然后 HttpContext.Session 属性才能访问。

运行一下看看。现在没有设置会话,所以显示是第一次访问本站的消息。

 

 一旦会话设置了,再次访问,就是欢迎回来了。

 

 

好了,就这样了。本示例仅作演示,由于 bug 过多,无法投入生产环境使用。

 

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

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

相关文章

MySQL-3-多表查询和事务(结合案例学习)

我们之前在讲解SQL语句的时候&#xff0c;讲解了DQL语句&#xff0c;也就是数据查询语句&#xff0c;但是之前讲解的查询都是单表查询&#xff0c;而本章节我们要学习的则是多表查询操作&#xff0c;主要从以下几个方面进行讲解。 多表查询多表查询多表关系分类连接查询内连接隐…

【数据结构】绪论

文章目录 1. 绪论 1.1 概述 1.2 数据与数据结构 1.2.1 术语 1.2.2 逻辑结构 1.2.3 存储结构&#xff1a; 1.2.4 数据操作&#xff1a; 1.3 算法 1.3.1 算法特性 1.3.2 算法目标 1.3.3 算法分析&#xff1a;概述 1.3.4 算法分析&#xff1a;时间复杂度&#xff08;大…

Markdown笔记软件之 Obsidian

我使用过什么markdown笔记软件了解自己的需求 Markdown 语法简洁vscode内置 markdown 插件,预览等 snippet(摘要功能)自定义代码片段typero实时渲染,所见即所得 美观缺点不适合我个人 收费 不支持打标签 tag 放弃:解决不了我的痛点(全键盘),收费 不支持移动端joplin支持 v…

模拟用户登录功能的实现以及演示SQL注入现象

模拟用户登录功能的实现以及演示SQL注入现象 /* 实现功能&#xff1a;1、需求&#xff1a;模拟用户登录功能的实现。2、业务描述&#xff1a;程序运行的时候&#xff0c;提供一个输入的入口&#xff0c;可以让用户输入用户名和密码用户输入用户名和密码之后&#xff0c;提交信息…

Day07__面向对象

面向对象 什么是面向对象回顾方法的定义 package objectOriented;import java.io.IOException;//回顾方法的定义 public class Demo01 {public static void main(String[] args) {}public static String sayHello(){return "Hello,World!";}public int max(int a,int…

Deno 会取代NodeJS吗?

目标:了解Deno的学习价值和前景。 从下面几个维度进行分析 成熟度 Node已经在大量商业应用中,Deno只是还在商业试验阶段 生态 Node已经有丰富的生态,包含各种框架和库,并且都已经广泛应用Deno的框架和库基本上都是刚刚起步 学习成本 如果你已经了解Node,Deno也还是需要不…

基于蜜蜂算法求解电力系统经济调度(Matlab代码实现)

目录 1 蜜蜂优化算法 1.1 蜂群觅食机制 1.2 蜜蜂算法 1.3 流程 2 经济调度 3 运行结果 4 Matlab代码及文章 5 参考文献 6 写在最后 1 蜜蜂优化算法 蜜蜂算法( Bees Algorithm&#xff0c;BA) 由英国学者 AfshinGhanbarzadeh 和他的研究小组于 2005 年提出。该算法是一…

element table 列头和行高调整

1、行高调整<el-table :row-style="{height:0}"></el-table>2、列头高度调整<el-table :header-cell-style="{padding:0}" :row-style="{height:0}"></el-table>

都这麽大了还不了解防火墙?

目录 一、思考 二、实验 三、过程 1、实验拓扑 2、cloud-IO配置 3、防火墙配置 3.1 登录防火墙 4、区域划分 方法一 方法二 4.1 内网划分 4.2 各区域网关 4.3 区域配置 5、防火墙策略 5.1 允许-回程路由&#xff08;内网~外网&#xff09; 5.2 禁止-新建策略…

AI作画飞入平民百姓家——stable diffusion初体验

1. 前言 stable_diffusion来了&#xff0c;这个号称是最强的民用文本生成图片的模型它来了&#xff0c;相比较DAEE等大模型&#xff0c;它能够让我们消费级的显卡也能够实现文本到图像的生成。下面&#xff0c;我们也来试一下。 2. 准备过程 该服务器上必须要有的基础工具an…

[基于瑞芯微RV1126调试RTL8818FU WIFI模组支持STA和AP模式]

基于瑞芯微RV1126调试RTL8818FU WIFI模组支持STA和AP模式内核menuconfig配置内核dts配置文件系统配置和更改驱动编译wifi工具编译libnl库编译openssl编译wpa_supplicant编译hostapd编译开机运行脚本测试WIFI—STA模式运行脚本测试WIFI-AP模式内核menuconfig配置 CONFIG_NETFIL…

高光谱图像分类简述+《Deep Learning for Hyperspectral Image Classification: An Overview》综述论文笔记

论文题目《Deep Learning for Hyperspectral Image Classification: An Overview》 论文作者:Shutao Li, Weiwei Song, Leyuan Fang,Yushi Chen, Pedram Ghamisi,Jn Atli Benediktsson 论文发表年份:2019 一、高光谱简述高光谱成像是一项重要的遥感技术,它采集了从…

SQL server 2008 安装教程

SQL server 2008 安装教程 1. 安装 SQL server 2008 的主要步骤如下 1.1 点击 setup.exe1.2 选中 “安装”&#xff0c;并点击右边的 “全新 SQL sever 独立或向现有安装添加功能1.3 重启电脑&#xff0c;再找到安装程序 “setup.exe” 重复上面的步骤1.4 输入产品秘钥 “JD8Y…

The Art of Prompting: Event Detection based on Type Specific Prompts

Motivation之前的研究表明prompt可以提高模型在事件检测方面的性能,包括使用特定structure 使用每种事件类型特定的query 原型 trigger这些尝试启发对不同prompt效果的探究Settings 作者在3种setting下做了实验:Supervised event detection Few-shot Event detection两个数据…

对课上相关问题的研究和解答

问题一:从测试中看不足 1、JAVA的基本运行单位是类 2、类中由类变量和类方法共同组成 3、变量的类型相互之间存在可以转换的关系,具体来说,可以分为以下几种情况: 1、(byte、short、char)-int-long-float-double,从低级到高级的排序,数据类型可以直接由低级向高级转换 举…

SpringCloud微服务架构

什么是微服务 微服务架构的基础是将的那个应用程序开发为一组小型独立服务&#xff0c;这些独立服务在自己的进程中运行&#xff0c;独立开发和部署。 SpringCloud Alibaba微服务&#xff1a; Spring Cloud Alibaba 是Spring Cloud的一个子项目&#xff0c;致力于提供微服务…

9--RNN

有隐藏状态的循环神经网络 假设在时间步t有小批量输入&#xff0c;即对于n个序列样本的小批量&#xff0c;的每一行对应于来自该序列的时间步t处的一个样本&#xff0c;用表示时间步t的隐藏变量。与MLP不同的是&#xff0c; 我们在这里保存了前一个时间步的隐藏变量&#xff0c…

《Mycat分布式数据库架构》之数据切分实战

文章目录1、引言2、前期准备2.1 系统环境2.2 数据库集群3 注意事项3.1 分片原则3.2 如何选择分片键4 数据切分实战4.1 配置访问用户及权限4.2 配置逻辑库及逻辑表4.3 配置分片规则4.3.1 简单取模分片4.3.2 哈希取模分片4.3.3 枚举分片4.3.4 字符串范围取模分片前文回顾&#xf…

Selenium操作已经打开的Chrome(只怪自己尝试的太迟)

&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d; &#x1f970; 博客首页&#xff1a;…

抖音视频

刻度尺读取方法0n:/ 复制打开抖音,看看【天子骄龙的作品】初中物理-刻度尺读数 ηηQ2VtW0nGyv8▽▽ 秒表读取方法 8.76 aNW:/ 复制打开抖音,看看【天子骄龙的作品】初中物理-秒表读数# 专业的事交给专业的人 初中物理... https://v.douyin.com/6RTySK2/