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

今天咱们来聊一下如何自己动手,实现会话(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的存储方式的更多相关文章

  1. ASP.net 中关于Session的存储信息及其它方式存储信息的讨论与总结

    通过学习和实践笔者总结一下Session 的存储方式.虽然里面的理论众所周知,但是我还是想记录并整理一下.作为备忘录吧.除了ASP.net通过Web.config配置的方式,还有通过其它方式来存储的方 ...

  2. asp.net core 自定义认证方式--请求头认证

    asp.net core 自定义认证方式--请求头认证 Intro 最近开始真正的实践了一些网关的东西,最近写几篇文章分享一下我的实践以及遇到的问题. 本文主要介绍网关后面的服务如何进行认证. 解决思 ...

  3. session的存储方式和配置

    Session又称为会话状态,是Web系统中最常用的状态,用于维护和当前浏览器实例相关的一些信息.我们控制用户去权限中经常用到Session来存储用户状态,这篇文章会讲下Session的存储方式.在w ...

  4. 可灵活扩展的自定义Session状态存储驱动

    Session是互联网应用中非常重要的玩意儿,对于超过单台部署的站点集群,都会存在会话共享的需求.在web.config中,微软提供了sessionstate节点来定义不同的Session状态存储方式 ...

  5. Asp.net Mvc 自定义Session (二)

    在 Asp.net Mvc 自定义Session (一)中我们把数据缓存工具类写好了,今天在我们在这篇把 剩下的自定义Session写完 首先还请大家跟着我的思路一步步的来实现,既然我们要自定义Ses ...

  6. Spring session(redis存储方式)监听导致创建大量redisMessageListenerContailner-X线程

    待解决的问题 Spring session(redis存储方式)监听导致创建大量redisMessageListenerContailner-X线程 解决办法 为spring session添加spr ...

  7. 如何在ASP.NET Core自定义中间件中读取Request.Body和Response.Body的内容?

    原文:如何在ASP.NET Core自定义中间件中读取Request.Body和Response.Body的内容? 文章名称: 如何在ASP.NET Core自定义中间件读取Request.Body和 ...

  8. asp.net core 自定义 Policy 替换 AllowAnonymous 的行为

    asp.net core 自定义 Policy 替换 AllowAnonymous 的行为 Intro 最近对我们的服务进行了改造,原本内部服务在内部可以匿名调用,现在增加了限制,通过 identit ...

  9. asp.net core 自定义基于 HttpContext 的 Serilog Enricher

    asp.net core 自定义基于 HttpContext 的 Serilog Enricher Intro 通过 HttpContext 我们可以拿到很多有用的信息,比如 Path/QuerySt ...

随机推荐

  1. 从Mpx资源构建优化看splitChunks代码分割

    背景 MPX是滴滴出品的一款增强型小程序跨端框架,其核心是对原生小程序功能的增强.具体的使用不是本文讨论的范畴,想了解更多可以去官网了解更多. 回到正题,使用MPX开发小程序有一段时间了,该框架对不同 ...

  2. Unity3D学习笔记6——GPU实例化(1)

    目录 1. 概述 2. 详论 3. 参考 1. 概述 在之前的文章中说到,一种材质对应一次绘制调用的指令.即使是这种情况,两个三维物体使用同一种材质,但它们使用的材质参数不一样,那么最终仍然会造成两次 ...

  3. 函数式(Functional)接口

    public class LambdaTest2 { @Test public void test1(){ happyTime(500, new Consumer<Double>() { ...

  4. 【docker专栏2】CentOS操作系统安装DockerCE

    目录 一.前置要求 二.更新软件源信息 三.安装 Docker-CE 四.卸载Docker CE 为大家介绍在CentOS操作系统中安装docker的过程,linux其他发行版本安装docker方法可 ...

  5. 攻防世界MISC进阶区 61-63

    61.肥宅快乐题 得到swf文件,但是用PotPlayer打不开,用浏览器应该可以打开,打开后可以在npc的对话中看到一段base64 解密后就可以得到flag 62.warmup 得到一张png和一 ...

  6. 159_模型_Power BI 地理分析之形状地图

    159_模型_Power BI 地理分析之形状地图 声明以下地图元素仅供学习交流所用,如需地图公开使用请提前做好报审工作. 一.背景 当企业的体量达到一定体量的时候,保持稳定的增长是非常重要的事情.本 ...

  7. 你真的了解JAVA中对象和类、this、super和static关键字吗

    作者:小牛呼噜噜 | https://xiaoniuhululu.com 计算机内功.JAVA底层.面试相关资料等更多精彩文章在公众号「小牛呼噜噜 」 目录 Java对象究竟是什么? 创建对象的过程 ...

  8. ApiDay001 __02 Java_StringBuilder

    Java 核心API StringBuilder String 类型的连接性能不好,Java提供了StringBuilder解决字符串连接性能问题. 简单理解 StringBuilder性能好!(重点 ...

  9. C++指针和结构体基础知识

    学习C++首先要回忆起C语言当中的指针和结构体知识,本文作者将通过一段代码来总结指针和结构体基础知识:指针是一个变量,其值为另一个变量的地址,即,内存位置的直接地址.就像其他变量或常量一样,您必须在使 ...

  10. FPS游戏逆向-方框透视(三角函数)

    本套课程主要学习FPS类游戏安全 由于FPS类游戏本身的特性问题,可能产生一些通用的游戏安全问题 在通过逆向与正向对FPS类游戏分析之后,找到其可能出现的不安全点 才能更好的保护游戏不被外部力量侵犯 ...