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

今天咱们来聊一下如何自己动手,实现会话(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. RPA应用场景-自动轮询汇总报表

    场景概述 自动轮询汇总报表 所涉系统名称 券商披露网站 人工操作(时间/次) 36小时 所涉人工数量 1 操作频率 每月 场景流程 1.每月初机器人自动登录网站轮询36家券商披露的财务报告,并下载 2 ...

  2. 手写网站服务器~用Python手动实现一个简单的服务器,不借助任何框架在浏览器中输出任意内容

    写在前面的一些P话: 在公司网站开发中,我们往往借助于Flask.Django等网站开发框架去提高网站开发效率.那么在面试后端开发工程师的时候,面试官可能就会问到网站开发的底层原理是什么? 我们不止仅 ...

  3. NC15975 小C的记事本

    NC15975 小C的记事本 题目 题目描述 小C最近学会了java小程序的开发,他很开心,于是想做一个简单的记事本程序练练手. 他希望他的记事本包含以下功能: 1.append(str),向记事本插 ...

  4. AlterNats是如何做到高性能的发布订阅的?

    前言 在过去的一些文章里面,我们聊了一些.NET平台上高性能编程的技巧,今天带大家了解一下AlterNats这个库是如何做到远超同类SDK性能的. NATS:NATS是一个开源.轻量级.高性能的分布式 ...

  5. Solution -「构造」专练

    记录全思路过程和正解分析.全思路过程很 navie,不过很下饭不是嘛.会持续更新的(应该). 「CF1521E」Nastia and a Beautiful Matrix Thought. 要把所有数 ...

  6. EFCore高级Saas系统下一个DbContext如何支持多数据库迁移

    EFCore高级玩法单DbContext支持多数据库迁移 前言 随着系统的不断开发和迭代默认的efcore功能十分强大,但是随着Saas系统的引进efcore基于表字段的多租户模式已经非常完美了,但是 ...

  7. 牛牛与后缀表达式_via牛客网

    题目 链接:https://ac.nowcoder.com/acm/contest/28537/B 来源:牛客网 时间限制:C/C++ 3秒,其他语言6秒 空间限制:C/C++ 262144K,其他语 ...

  8. 可变参数和Collections集合工具类

    可变参数 /** * 可变参数:jdk1.5后出现的新特性 * 使用前期: * 当方法的参数列表数据类型已经确定的时候但是参数的个数不确定的时候就可以使用可变参数 * 使用格式:定义方法的时候使用 * ...

  9. 一文搞定Vue2组件通信

    vue 组件通信方式 父组件将自己的状态分享给子组件使用: 方法:父组件通过子标签传递数据,子组件通过 props 接收 子组件改变父组件的状态; 方法:父组件在子标签上通过@abc 提供一个改变自身 ...

  10. GIt后悔药:还原提交的操作(谨慎操作)

    一.背景: 偶尔会遇到git的版本分支的文件被误改的情况,需要还原,此篇文章可能会帮助到你. PS: 来理解下 Git 工作区.暂存区和版本库概念,可以更好的理解以下的还原操作. * 工作区:就是你在 ...