Loading... ## 前言 在`ASP.NET Core`中的`Session`该如何存储?本文介绍了`Session`信息在单个应用实例和多个应用实例中的不同存储方式,以及`ASP.NET Core`在`Session`数据安全方面做的一些努力。 ## TL;DR 在`ASP.NET Core`中使用分布式缓存存储会话时,仅仅将`Session`进行分布式存储还不够,由于`ASP.NET Core`的数据安全保护机制,还需要将`Data Protection`所使用的加密密钥也进行分布式存储,这样才能正确解密`Cookies`: ```csharp public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { //将数据保护的秘钥存储为分布式,否则多个实例会有多个秘钥加密cookie值,导致即使读取到客户端的cookie也无法解密 services.AddDataProtection().PersistKeysToStackExchangeRedis(ConnectionMultiplexer.Connect($"localhost:6379,defaultDatabase=0"), "DataProtection-Keys"); //添加reidis作为分布式缓存 services.AddStackExchangeRedisCache(options => { options.Configuration = $"localhost:6379,defaultDatabase=0"; }); //注册Session services.AddSession(options => { options.IdleTimeout = TimeSpan.FromSeconds(10); }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseRouting(); //使用Session中间件 app.UseSession(); app.UseEndpoints(endpoints => { endpoints.MapDefaultControllerRoute(); endpoints.MapRazorPages(); }); } } ``` ## `Session`管理 单应用实例的会话,直接保存在本机内存中,客户端请求时带上表明用户身份的`Cookie`,便能直接从实例内存中获取到`Session`数据,大概构造如下: ![](/usr/uploads/2020/05/2487585149.png) 这种情况下我们只需要保证单应用实例的内存足够即可。但是单应用实例很难满足我们面对大量并发请求的高性能、高可用性要求,拓展多个应用实例几乎是必须的,那么考虑多实例的`Session`存储,由于我们`Session`是保存在每个应用自己的内存中,所以如果同一个会话的请求被另一个应用实例处理,则无法获取到`Session`信息。解决这个问题有两个方案: * 继续保持`Session`在应用实例内存存储,使用负载均衡器的`Session`跟踪功能,将同一个`Session`的请求总是转发到同一实例。 * 将`Session`存储剥离出来,使用分布式缓存进行存储以保证多个应用实例能够共享`Session`会话。 对于第一种方案,毫无疑问是无需做任何应用层面的处理,只需要设置负载均衡器即可,能够快速的解决当前的问题。但是它的缺点也很明显,一方面增加负载均衡器的压力,能一方面,它会导致负载均衡器无法完全发挥出它`均衡`的功能:毕竟要保证同一个`Session`的请求总是要转发到同一个实例。这看起来不先进且应用的正常工作需要对负载均衡器有依赖和粘性,如果换个不支持会话跟踪的负载均衡器应用便无法多实例部署显然是难以接受的。 另一种方案则高明许多:使用分布式缓存对`Session`进行存储以便多应用实例能够共享: ![](/usr/uploads/2020/05/3446328274.png) 这确实是个好的解决方案,但是它需要对应用进行一点小的改动,幸运的是我们是`ASP.NET Core`用户,它的`Session`存储技术已经支持分布式缓存方案: * 引入`nuget`包`Microsoft.Extensions.Caching.StackExchangeRedis` ```csharp public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { //添加reidis作为分布式缓存 services.AddStackExchangeRedisCache(options => { options.Configuration = $"localhost:6379,defaultDatabase=0"; }); //注册Session services.AddSession(options => { options.IdleTimeout = TimeSpan.FromSeconds(10); }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseRouting(); //使用Session中间件 app.UseSession(); app.UseEndpoints(endpoints => { endpoints.MapDefaultControllerRoute(); endpoints.MapRazorPages(); }); } } ``` 如你所见,我们只需要添加分布式缓存方案即可,`ASP.NET Core`框架在读写`Session`时会使用实现了`IDistributedCache`接口的分布式缓存实例,如果未配置则默认使用`MemoryCache`作为`IDistributedCache`实例,它虽然实现了`IDistributedCache`但它实实在在是个单实例内存缓存。 不出意外的话你按照上述代码配置后,会发现当多个应用实例时,`Session`还是无法正常工作,你会去你的缓存中查看`Session`数据确实被存入,加上日志后你还会发现`Session`依然是跟着某个具体应用实例相关联的:只有创建`Session`的实例才能够获取到`Session`数据,其它实例则无法获取到非自己的`Session`数据。那么问题在这:[ASP.NET Core Data Protection](https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/introduction?view=aspnetcore-3.1)。这是个啥玩意呢?简单来说`ASP.NET Core`为了数据安全做了很多努力,其中之一就是对`Session`的安全处理措施,`ASP.NET Core`使用了秘钥对`Session`信息进行了加密处理。问题关键在于这个密钥,它的存储不是分布式的,它可能会存储在`%LOCALAPPDATA%\ASP.NET\DataProtection-Keys`文件夹或内存中(具体的存储地址:[Data Protection key management and lifetime in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/default-settings?view=aspnetcore-3.1#key-management)),总之它默认不在多个应用实例中共享,这就导致一个应用实例使用密钥加密的`Session`数据无法被另一个应用实例解密出数据。 ![](/usr/uploads/2020/05/1681626093.png) 当然提出这个问题肯定是有解决方案的,继续使用分布式存储方案对密钥进行存储也不失为一种优雅(使用Redis作为示例): * 引入`nuget`包`Microsoft.AspNetCore.DataProtection.StackExchangeRedis` ```csharp public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { //将数据保护的秘钥存储为分布式,否则多个实例会有多个秘钥加密cookie值,导致即使读取到客户端的cookie也无法解密 services.AddDataProtection().PersistKeysToStackExchangeRedis(ConnectionMultiplexer.Connect($"localhost:6379,defaultDatabase=0"), "DataProtection-Keys"); //添加reidis作为分布式缓存 services.AddStackExchangeRedisCache(options => { options.Configuration = $"localhost:6379,defaultDatabase=0"; }); //注册Session services.AddSession(options => { options.IdleTimeout = TimeSpan.FromSeconds(10); }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseRouting(); //使用Session中间件 app.UseSession(); app.UseEndpoints(endpoints => { endpoints.MapDefaultControllerRoute(); endpoints.MapRazorPages(); }); } } ``` ![](/usr/uploads/2020/05/4022775648.png) # Reference * [Configure IDistributedCache and IDataProtection for session in ASP.NET Core](https://medium.com/@tanaka_733/configure-idistributedcache-and-idataprotection-for-session-in-asp-net-core-d80e42bcff92) * [Data Protection key management and lifetime in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/default-settings?view=aspnetcore-3.1) * [Configure ASP.NET Core Data Protection](https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?view=aspnetcore-3.1) * [Session and state management in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/app-state?view=aspnetcore-3.1) * [DataProtection permanently fails to recover when backing storage is purged](https://github.com/dotnet/AspNetCore/issues/13476) * [Issue: Session Middleware with Redis Distributed Caching not working in web farm](https://github.com/aspnet/Session/issues/159) 最后修改:2020 年 09 月 23 日 © 允许规范转载 赞 0 如果觉得我的文章对你有用,请随意赞赏