using System.Text; // 堆代码 duidaima.com 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中显示的则不同。
会话状态在默认情况下采用分布式缓存的形式来存储,而我们的实例采用的是基于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。
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并不能唯一标识一个会话。