客户端和服务器基于HTTP的消息交换就好比两个完全没有记忆能力的人在交流,每次单一的HTTP事务体现为一次“一问一答”的对话。单一的对话毫无意义,在在同一语境下针对某个主题进行的多次对话才会有结果。会话的目的就是在同一个客户端和服务器之间建立两者交谈的语境或者上下文,ASP.NET Core利用一个名为SessionMiddleware的中间件实现了会话。本篇提供了几个简单的实例来演示如何在一个ASP.NET Core应用中利用会话来存储用户的状态。(本文提供的示例演示已经同步到《ASP.NET Core 6框架揭秘-实例演示版》)。

[S2301]设置和提取会话状态(源代码

[S2302]查看存储的会话状态(源代码

[S2303] 查看Cookie(源代码

[S2301]设置和提取会话状态

每个会话都有一个被称为Session Key的标识(但不是唯一标识),会话状态以一个数据字典的形式将Session Key保存在服务端。当SessionMiddleware中间件在处理会话的第一个请求时,它会创建一个Session Key,并据此创建一个独立的数据字典来存储会话状态。这个Session Key最终以Cookie的形式写入响应并返回客户端,客户端在每次发送请求时会自动附加这个Cookie,那么应用程序能够准确识别会话并成功定位存储会话状态的数据字典。下面我们利用一个简单的实例来演示会话状态的读写。ASP.NET应用在默认情况下会利用分布式缓存来存储会话状态。我们采用基于Redis数据库的分布式缓存,所以需要添加针对NuGet包“Microsoft.Extensions.Caching.Redis”的依赖。下面的演示程序调用了AddDistributedRedisCache扩展方法添加了基于DistributedRedisCache的服务注册,SessionMiddleware中间件则通过调用UseSession扩展方法进行注册。

using System.Text;

var builder = WebApplication.CreateBuilder();
builder.Services
.AddDistributedRedisCache(options => options.Configuration = "localhost")
.AddSession();
var app = builder.Build();
app.UseSession();
app.MapGet("/{foobar?}", ProcessAsync);
app.Run(); static async ValueTask<IResult> ProcessAsync(HttpContext context)
{
var session = context.Session;
await session.LoadAsync();
string sessionStartTime;
if (session.TryGetValue("__SessionStartTime", out var value))
{
sessionStartTime = Encoding.UTF8.GetString(value);
}
else
{
sessionStartTime = DateTime.Now.ToString();
session.SetString("__SessionStartTime", sessionStartTime);
} var html = $@"
<html>
<head><title>Session Demo</title></head>
<body>
<ul>
<li>Session ID:{session.Id}</li>
<li>Session Start Time:{sessionStartTime}</li>
<li>Current Time:{DateTime.Now}</li>
<ul>
</body>
</html>";
return Results.Content(html, "text/html");
}

我们针对路由模板“/{foobar?}”注册了一个终结点,后者的处理器指向ProcessAsync方法。该方法当前HttpContext上下文中获取表示会话的Session对象,并调用其TryGetValue方法获取会话开始时间,这里使用的Key为“__SessionStartTime”。由于TryGetValue方法总是以字节数组的形式返回会话状态值,所以我们采用UTF-8编码转换成字符串形式。如果会话开始时间尚未设置,我们会调用SetString方法采用相同的Key进行设置。我们最终生成一段用于呈现Session ID和当前实时时间HTML,并封装成返回的ContentResult对象。程序启动之后,我们利用Chrome和IE访问请求注册的终结点,从图1可以看出针对Chrome的两次请求的Session ID和会话状态值都是一致的,但是IE中显示的则不同。

图1 以会话状态保存的“会话开始时间”

[S2302]查看存储的会话状态

会话状态在默认情况下采用分布式缓存的形式来存储,而我们的实例采用的是基于Redis数据库的分布式缓存,那么会话状态会以什么样的形式存储在Redis数据库中的呢?由于缓存数据在Redis数据库中是以散列的形式存储的,所以我们只有知道具体的Key才能知道存储的值。缓存状态是基于作为会话标识的Session Key进行存储的,它与Session ID具有不同的值,到目前为止我们不能使用公布出来的API来获取它,但可以利用反射的方式来获取Session Key。在默认情况下,表示Session的是一个DistributedSession对象,它通过如下所示的字段_sessionKey表示这个用来存储会话状态的Session Key。

public class DistributedSession : ISession
{
private readonly string _sessionKey;
...
}

接下来我们对上面演示的程序做简单的修改,从而使Session Key能够呈现出来。如下面的代码片段所示,我们可以采用反射的方式得到代表当前会话的DistributedSession对象的_sessionKey字段的值,并将它写入响应HTML文档的主体内容中。

static async ValueTask<IResult> ProcessAsync(HttpContext context)
{
var session = context.Session;
await session.LoadAsync();
string sessionStartTime;
if (session.TryGetValue("__SessionStartTime", out var value))
{
sessionStartTime = Encoding.UTF8.GetString(value);
}
else
{
sessionStartTime = DateTime.Now.ToString();
session.SetString("__SessionStartTime", sessionStartTime);
} var field = typeof(DistributedSession).GetTypeInfo().GetField("_sessionKey", BindingFlags.Instance | BindingFlags.NonPublic)!;
var sessionKey = field.GetValue(session); var html = $@"
<html>
<head><title>Session Demo</title></head>
<body>
<ul>
<li>Session ID:{session.Id}</li>
<li>Session Start Time:{sessionStartTime}</li>
<li>Session Key:{sessionKey}</li>
<li>Current Time:{DateTime.Now}</li>
<ul>
</body>
</html>";
return Results.Content(html, "text/html");
}

按照同样的方式启动应用后,我们使用浏览器访问目标站点得到的输出结果如图2所示,可以看到,Session Key的值被正常呈现出来,它是一个不同于Session ID的GUID。

图2 呈现当前会话的Session Key

如果有这个保存当前会话状态的Session Key,我们就可以按照图3所示的方式采用命令行的形式将存储在Redis数据库中的会话状态数据提取出来。当会话状态在采用默认的分布式缓存进行存储时,整个数据字典(包括Key和Value)会采用预定义的格式序列化成字节数组,这基本上可以从图3体现出来。我们还可以看出基于会话状态的缓存默认采用的是基于滑动时间的过期策略,默认采用的滑动过期时间为20分(12 000 000 000纳秒)。

图3 存储在Redis数据库中的会话状态

[S2303]查看Cookie

虽然整个会话状态数据存储在服务端,但是用来提取对应会话状态数据的Session Key需要以Cookie的形式由客户端来提供。如果请求没有以Cookie的形式携带Session Key,SessionMiddleware中间件就会将当前请求视为会话的第一次请求。在此情况下,它会生成一个GUID作为Session Key,并最终以Cookie的形式返回客户端。

HTTP/1.1 200 OK
...
Set-Cookie:.AspNetCore.Session=CfDJ8CYspSbYdOtFvhKqo9CYj2vdlf66AUAO2h2BDQ9%2FKoC2XILfJE2bk
IayyjXnXpNxMzMtWTceawO3eTWLV8KKQ5xZfsYNVlIf%2Fa175vwnCWFDeA5hKRyloWEpPPerphndTb8UJNv5R68bGM8jP%2BjKVU7za2wgnEStgyV0ceN%2FryfW; path=/; httponly

如上所示的代码片段是响应报头中携带Session Key的Set-Cookie报头在默认情况下的表现形式。可以看出Session Key的值不仅是被加密的,更具有一个httponly标签以防止Cookie值被跨站读取。在默认情况下,Cookie采用的路径为“/”。当我们使用同一个浏览器访问目标站点时,发送的请求将以如下形式附加上这个Cookie。

GET http://localhost:5000/ HTTP/1.1
...
Cookie: .AspNetCore.Session=CfDJ8CYspSbYdOtFvhKqo9CYj2vdlf66AUAO2h2BDQ9%2FKoC2XILfJE2bkIayyjXnXpNxMzMtWTceawO3eTWLV8KKQ5xZfsYNVlIf%2Fa175vwnCWFDeA5hKRyloWEpPPerphndTb8UJNv5R68bGM8jP%2BjKVU7za2wgnEStgyV0ceN%2FryfW

除了Session Key,前面还提到了Session ID,读者可能不太了解两者具有怎样的区别。Session Key和Session ID是两个不同的概念,上面演示的实例也证实了它们的值其实是不同的。Session ID可以作为会话的唯一标识,但是Session Key不可以。两个不同的Session肯定具有不同的Session ID,但是它们可能共享相同的Session Key。当SessionMiddleware中间件接收到会话的第一个请求时,它会创建两个不同的GUID来分别表示Session Key和Session ID。其中Session ID将作为会话状态的一部分被存储起来,而Session Key以Cookie的形式返回客户端。

会话是具有有效期的,会话的有效期基本决定了存储的会话状态数据的有效期,默认过期时间为20分钟。在默认情况下,20分钟之内的任意一次请求都会将会话的寿命延长至20分钟后。如果两次请求的时间间隔超过20分钟,会话就会过期,存储的会话状态数据(包括Session ID)会被清除,但是请求携带可能还是原来的Session Key。在这种情况下,SessionMiddleware中间件会创建一个新的会话,该会话具有不同的Session ID,但是整个会话状态依然沿用这个Session Key,所以Session Key并不能唯一标识一个会话。

ASP.NET Core 6框架揭秘实例演示[35]:利用Session保留语境的更多相关文章

  1. ASP.NET Core 6框架揭秘实例演示[07]:文件系统

    ASP.NET Core应用具有很多读取文件的场景,如读取配置文件.静态Web资源文件(如CSS.JavaScript和图片文件等).MVC应用的视图文件,以及直接编译到程序集中的内嵌资源文件.这些文 ...

  2. ASP.NET Core 6框架揭秘实例演示[08]:配置的基本编程模式

    .NET的配置支持多样化的数据源,我们可以采用内存的变量.环境变量.命令行参数.以及各种格式的配置文件作为配置的数据来源.在对配置系统进行系统介绍之前,我们通过几个简单的实例演示一下如何将具有不同来源 ...

  3. ASP.NET Core 6框架揭秘实例演示[09]:配置绑定

    我们倾向于将IConfiguration对象转换成一个具体的对象,以面向对象的方式来使用配置,我们将这个转换过程称为配置绑定.除了将配置树叶子节点配置节的绑定为某种标量对象外,我们还可以直接将一个配置 ...

  4. ASP.NET Core 6框架揭秘实例演示[10]:Options基本编程模式

    依赖注入使我们可以将依赖的功能定义成服务,最终以一种松耦合的形式注入消费该功能的组件或者服务中.除了可以采用依赖注入的形式消费承载某种功能的服务,还可以采用相同的方式消费承载配置数据的Options对 ...

  5. ASP.NET Core 6框架揭秘实例演示[11]:诊断跟踪的几种基本编程方式

    在整个软件开发维护生命周期内,最难的不是如何将软件系统开发出来,而是在系统上线之后及时解决遇到的问题.一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根 ...

  6. ASP.NET Core 6框架揭秘实例演示[12]:诊断跟踪的进阶用法

    一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根据当前的运行状态预知未来可能发生的问题,并将问题扼杀在摇篮中.诊断跟踪能够帮助我们有效地纠错和排错&l ...

  7. ASP.NET Core 6框架揭秘实例演示[13]:日志的基本编程模式[上篇]

    <诊断跟踪的几种基本编程方式>介绍了四种常用的诊断日志框架.其实除了微软提供的这些日志框架,还有很多第三方日志框架可供我们选择,比如Log4Net.NLog和Serilog 等.虽然这些框 ...

  8. ASP.NET Core 6框架揭秘实例演示[14]:日志的进阶用法

    为了对各种日志框架进行整合,微软创建了一个用来提供统一的日志编程模式的日志框架.<日志的基本编程模式>以实例演示的方式介绍了日志的基本编程模式,现在我们来补充几种"进阶" ...

  9. ASP.NET Core 6框架揭秘实例演示[15]:针对控制台的日志输出

    针对控制台的ILogger实现类型为ConsoleLogger,对应的ILoggerProvider实现类型为ConsoleLoggerProvider,这两个类型都定义在 NuGet包"M ...

随机推荐

  1. Eclipse历史版本下载和选择对应的java版本

    下载Eclipse 官网: https://www.eclipse.org/ 直达 直接进入连接:https://www.eclipse.org/downloads/packages/installe ...

  2. ASP.NET MVC的核心-Controller(控制器)

    "每一个请求都必须通过Controller处理,然而其中有些请求是不需要模型和视图的" MVC框架规定带Controller后缀的类称为所谓的"控制器",在xx ...

  3. HashMap1.8常见面试问题

    1.hashmap转红黑树的时机: for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNod ...

  4. java单链表基本操作

    /** * */ package cn.com.wwh; /** * @Description:TODO * @author:wwh * @time:2021-1-18 19:24:47 */ pub ...

  5. # 8 快速入门 dubbo

    8 快速入门 dubbo 所需资料 注册中心 Zookeeper 安装 zookeeper 官方推荐使用 zookeeper 注册中心: 注册中心负责服务地址的注册与查找,相当于目录服务: 服务提供者 ...

  6. 彻底理解DDS(信号发生器)的fpga实现(verilog设计代码)

    DDS(Direct Digital Synthesis)是一种把一系列数字信号通过D/A转换器转换成模拟信号的数字合成技术. 它有查表法和计算法两种基本合成方法.在这里主要记录DDS查表法的fpga ...

  7. ROS机械臂 Movelt 学习笔记3 | kinect360相机(v1)相关配置

    目标是做一个机械臂视觉抓取的demo,在基地里翻箱倒柜,没有找到学长所说的 d435,倒是找到了一个老古董 kinect 360. 前几天就已经在旧电脑上配置好了,现在记录在新电脑上的配置过程. 1. ...

  8. linux学习系列--初识Linux系统

    ### 认识Linux- Linux是一种类UNIX的系统,Unix是1965年在贝尔实验室开发的一个项目,用来开发操作系统- Linux之父-Linus Torvalds在1991年10月5日,他在 ...

  9. MySQL建表DDL规范(欢迎补充)

    MySQL建表DDL规范(欢迎补充) 基本规范: 表名和字段名全大写,一般表名以T开头 脚本需支持可重复执行,带IF NOT EXISTS ,但不可带DROP语句 字符集使用utf8mb4 (CHAR ...

  10. Postgres常用时间查询

    如select extract(day from now());