【ASP.NET Core】自定义Session的存储方式
在开始今天的表演之前,老周先跟大伙伴们说一句:“中秋节快乐”。
今天咱们来聊一下如何自己动手,实现会话(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 过多,无法投入生产环境使用。
【ASP.NET Core】自定义Session的存储方式的更多相关文章
- ASP.net 中关于Session的存储信息及其它方式存储信息的讨论与总结
通过学习和实践笔者总结一下Session 的存储方式.虽然里面的理论众所周知,但是我还是想记录并整理一下.作为备忘录吧.除了ASP.net通过Web.config配置的方式,还有通过其它方式来存储的方 ...
- asp.net core 自定义认证方式--请求头认证
asp.net core 自定义认证方式--请求头认证 Intro 最近开始真正的实践了一些网关的东西,最近写几篇文章分享一下我的实践以及遇到的问题. 本文主要介绍网关后面的服务如何进行认证. 解决思 ...
- session的存储方式和配置
Session又称为会话状态,是Web系统中最常用的状态,用于维护和当前浏览器实例相关的一些信息.我们控制用户去权限中经常用到Session来存储用户状态,这篇文章会讲下Session的存储方式.在w ...
- 可灵活扩展的自定义Session状态存储驱动
Session是互联网应用中非常重要的玩意儿,对于超过单台部署的站点集群,都会存在会话共享的需求.在web.config中,微软提供了sessionstate节点来定义不同的Session状态存储方式 ...
- Asp.net Mvc 自定义Session (二)
在 Asp.net Mvc 自定义Session (一)中我们把数据缓存工具类写好了,今天在我们在这篇把 剩下的自定义Session写完 首先还请大家跟着我的思路一步步的来实现,既然我们要自定义Ses ...
- Spring session(redis存储方式)监听导致创建大量redisMessageListenerContailner-X线程
待解决的问题 Spring session(redis存储方式)监听导致创建大量redisMessageListenerContailner-X线程 解决办法 为spring session添加spr ...
- 如何在ASP.NET Core自定义中间件中读取Request.Body和Response.Body的内容?
原文:如何在ASP.NET Core自定义中间件中读取Request.Body和Response.Body的内容? 文章名称: 如何在ASP.NET Core自定义中间件读取Request.Body和 ...
- asp.net core 自定义 Policy 替换 AllowAnonymous 的行为
asp.net core 自定义 Policy 替换 AllowAnonymous 的行为 Intro 最近对我们的服务进行了改造,原本内部服务在内部可以匿名调用,现在增加了限制,通过 identit ...
- asp.net core 自定义基于 HttpContext 的 Serilog Enricher
asp.net core 自定义基于 HttpContext 的 Serilog Enricher Intro 通过 HttpContext 我们可以拿到很多有用的信息,比如 Path/QuerySt ...
随机推荐
- NFS网络文件系统搭建
1. 简介 NFS, 就是network file system的简称. 可以通过NFS, 来共享不同主机的文件.目录. 2010年,NFS已经发展到v4.1版本. 2. 应用场景 在中小型企业中,N ...
- 【JS】两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标. 你可以假设每种输入只会对应一个答案.但是,数组中同一 ...
- bat-命令行配置静态IP地址
查看连接名称ipconfig 打开命令提示符,输入netsh后回车 输入interface后回车 输入ip,回车 输入set address "连接名称" static 新IP地址 ...
- 一题多解,ASP.NET Core应用启动初始化的N种方案[上篇]
ASP.NET Core应用本质上就是一个由中间件构成的管道,承载系统将应用承载于一个托管进程中运行起来,其核心任务就是将这个管道构建起来.在ASP.NET Core的发展历史上先后出现了三种应用承载 ...
- CentOS查看操作系统安装时间信息:
CentOS查看系统安装时间信息: 方法1:[root@logserver ~]# ll /boot/|egrep -i "(grub|lost\+found)" 方法2:[ro ...
- Mybatis SqlNode源码解析
1.ForEachSqlNode mybatis的foreach标签可以将列表.数组中的元素拼接起来,中间可以指定分隔符separator <select id="getByUserI ...
- 运行Flutter时连接超时
这个墙不知道浪费了开发者多少的时间!!!!!!!!!!!!!!!!!!! 1.修改仓库地址为阿里仓库: 编辑android/build.gradle,把文件中的两处: google() jcenter ...
- 基于 Hexo 从零开始搭建个人博客(五)
阅读本篇前,请先阅读前几篇文章: 基于 Hexo 从零开始搭建个人博客(一) 基于 Hexo 从零开始搭建个人博客(二) 基于 Hexo 从零开始搭建个人博客(三) 基于 Hexo 从零开始搭建个人博 ...
- input函数的使用
input()函数的介绍 作用:接受来自用户的输入 返回值类型:输入值的类型为str 值得存储:使用=对输入得值进行存储 input()函数的基本使用 name = input('What's you ...
- 第二十一天python3 python的正则表达式re模块学习
python的正则表达式 python使用re模块提供了正则表达式处理的能力: 常量 re.M re.MULTILINE 多行模式 re.S re.DOTALL 单行模式 re.I re.IGNORE ...