Skip to content

32. 会话和状态管理

32.1 关于会话和状态管理

HTTP 是无状态的协议。 默认情况下,HTTP 请求是不保留用户值的独立消息。但是我们可以通过以下几种方式保留请求用户数据:

  • Cookie:通常存储在客户端的数据,请求时带回服务端
  • Session:存储在服务端的数据(可以在存储在内存、进程等介质中)
  • Query Strings:通过 Http 请求地址参数共享
  • HttpContext.Items:存储在服务端,只在请求声明周期内使用,请求结束自动销毁
  • Cache:服务端缓存,包括内存缓存、分布式内存缓存、IO 缓存、序列化缓存以及数据库缓存
  • AsyncLocal<T>:通过异步控制流实现本地数据共享,跨线程

32.2 如何使用

使用 Cookie 非常简单,如:

// 读取 Cookies  
var value = httpContext.Request.Cookies["key"];  

// 设置 Cookies  
var option = new CookieOptions();  
option.Expires = DateTime.Now.AddMilliseconds(10);  
httpContext.Response.Cookies.Append(key, value, option);  

// 删除 Cookies  
httpContext.Response.Cookies.Delete(key);  

特别说明httpContext 可以通过 IHttpContextAccessor 获取,也可以通过 App.HttpContext 获取。

我们还可以通过 Cookie 实现授权功能及单点登录(SSO):网站共享 Cookie

32.2.2 Session 使用

在使用 Session 之前,必须注册 Session 服务:(如果

public class Startup  
{  
    public void ConfigureServices(IServiceCollection services)  
    {  
        // services.AddDistributedMemoryCache(); 框架内部已经默认注册  

        services.AddSession(options =>  
        {  
            options.IdleTimeout = TimeSpan.FromSeconds(10);  
            options.Cookie.HttpOnly = true;  
            options.Cookie.IsEssential = true;  
        }); // 注意在控制器之前注册!!!!  

        services.AddControllersWithViews();  
    }  

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
    {  
        if (env.IsDevelopment())  
        {  
            app.UseDeveloperExceptionPage();  
        }  
        else  
        {  
            app.UseExceptionHandler("/Home/Error");  
            app.UseHsts();  
        }  

        app.UseHttpsRedirection();  
        app.UseStaticFiles();  

        app.UseRouting();  

        app.UseAuthentication();  
        app.UseAuthorization();  

        app.UseSession();  

        app.UseEndpoints(endpoints =>  
        {  
            endpoints.MapDefaultControllerRoute();  
            endpoints.MapRazorPages();  
        });  
    }  
}  

中间件注册顺序app.UseSession() 必须在 app.UseRouting()app.UseEndpoints() 之间注册!

  • 常见例子:
// 读取 Session  
var byteArr = httpContext.Session.Get("key"); // 返回 byte[]  
var str = httpContext.Session.GetString("key");   // 返回 string[]  
var num = httpContext.Session.GetInt32("key");    // 返回 int  

// 设置 Session  
httpContext.Session.SetString("key", "value");  // 设置字符串  
httpContext.Session.SetInt32("key", 1); // 设置 int 类型  

  • 自定义设置任意类型拓展:
public static class SessionExtensions  
{  
    public static void Set<T>(this ISession session, string key, T value)  
    {  
        session.SetString(key, JsonSerializer.Serialize(value));  
    }  

    public static T Get<T>(this ISession session, string key)  
    {  
        var value = session.GetString(key);  
        return value == null ? default : JsonSerializer.Deserialize<T>(value);  
    }  
}  

  • 防止 Session ID 改变或 Session 失效

Startup.csConfigureServices 配置即可:

services.Configure<CookiePolicyOptions>(options =>  
{  
   options.CheckConsentNeeded = context => false; // 默认为true,改为false  
   options.MinimumSameSitePolicy = SameSiteMode.None;  
});  

32.2.3 Query Strings 使用

该方式使用非常简单,只需 httpContext.Request.Query["key"] 即可。

32.2.4 HttpContext.Items 使用

HttpContext 对象提供了 Items 集合属性,可以让我们在单次请求间共享数据,请求结束立即销毁,可以存储任何数据。使用也非常简单,如:

// 读取  
var value = httpContext.Items["key"];  

// 添加  
httpContext.Items["key"] = "任何值包括对象";  

// 删除  
httpContext.Items.Remove("key");  

32.2.5 Cache 方式

参见 分布式缓存 文档

32.2.6 AsyncLocal<T> 方式

AsyncLocal<T> 可以说是进程内共享数据的大利器,可以通过该类实现跨线程、异步控制流中共享数据,如:

using System;  
using System.Threading;  
using System.Threading.Tasks;  

class Example  
{  
    static AsyncLocal<string> _asyncLocalString = new AsyncLocal<string>();  

    static ThreadLocal<string> _threadLocalString = new ThreadLocal<string>();  

    static async Task AsyncMethodA()  
    {  
        // Start multiple async method calls, with different AsyncLocal values.  
        // We also set ThreadLocal values, to demonstrate how the two mechanisms differ.  
        _asyncLocalString.Value = "Value 1";  
        _threadLocalString.Value = "Value 1";  
        var t1 = AsyncMethodB("Value 1");  

        _asyncLocalString.Value = "Value 2";  
        _threadLocalString.Value = "Value 2";  
        var t2 = AsyncMethodB("Value 2");  

        // Await both calls  
        await t1;  
        await t2;  
     }  

    static async Task AsyncMethodB(string expectedValue)  
    {  
        Console.WriteLine("Entering AsyncMethodB.");  
        Console.WriteLine("   Expected '{0}', AsyncLocal value is '{1}', ThreadLocal value is '{2}'",  
                          expectedValue, _asyncLocalString.Value, _threadLocalString.Value);  
        await Task.Delay(100);  
        Console.WriteLine("Exiting AsyncMethodB.");  
        Console.WriteLine("   Expected '{0}', got '{1}', ThreadLocal value is '{2}'",  
                          expectedValue, _asyncLocalString.Value, _threadLocalString.Value);  
    }  

    static async Task Main(string[] args)  
    {  
        await AsyncMethodA();  
    }  
}  
// The example displays the following output:  
//   Entering AsyncMethodB.  
//      Expected 'Value 1', AsyncLocal value is 'Value 1', ThreadLocal value is 'Value 1'  
//   Entering AsyncMethodB.  
//      Expected 'Value 2', AsyncLocal value is 'Value 2', ThreadLocal value is 'Value 2'  
//   Exiting AsyncMethodB.  
//      Expected 'Value 2', got 'Value 2', ThreadLocal value is ''  
//   Exiting AsyncMethodB.  
//      Expected 'Value 1', got 'Value 1', ThreadLocal value is ''  

为了简化操作,Furion v2.18+ 版本实现了轻量级的 CallContext 静态类,内部使用 AsyncLocal<T> 实现,使用如下:

CallContext.SetLocalValue("name", "Furion");  
CallContext.GetLocalValue("name");  

CallContext<int>.SetLocalValue("count", 1);  
CallContext<int>.GetLocalValue("count");  

了解更多 AsyncLocal<T> 知识:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.asynclocal-1?redirectedfrom\=MSDN\&view\=net-5.0

32.3 反馈与建议

与我们交流给 Furion 提 Issue