ASP.NET Core 中Session的分布式存储

分享
ASP.NET Core 中Session的分布式存储

前言

ASP.NET Core中的Session该如何存储?本文介绍了Session信息在单个应用实例和多个应用实例中的不同存储方式,以及ASP.NET CoreSession数据安全方面做的一些努力。

TL;DR

ASP.NET Core中使用分布式缓存存储会话时,仅仅将Session进行分布式存储还不够,由于ASP.NET Core的数据安全保护机制,还需要将Data Protection所使用的加密密钥也进行分布式存储,这样才能正确解密Cookies

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数据,大概构造如下:

这种情况下我们只需要保证单应用实例的内存足够即可。但是单应用实例很难满足我们面对大量并发请求的高性能、高可用性要求,拓展多个应用实例几乎是必须的,那么考虑多实例的Session存储,由于我们Session是保存在每个应用自己的内存中,所以如果同一个会话的请求被另一个应用实例处理,则无法获取到Session信息。解决这个问题有两个方案:

  • 继续保持Session在应用实例内存存储,使用负载均衡器的Session跟踪功能,将同一个Session的请求总是转发到同一实例。
  • Session存储剥离出来,使用分布式缓存进行存储以保证多个应用实例能够共享Session会话。

对于第一种方案,毫无疑问是无需做任何应用层面的处理,只需要设置负载均衡器即可,能够快速的解决当前的问题。但是它的缺点也很明显,一方面增加负载均衡器的压力,能一方面,它会导致负载均衡器无法完全发挥出它均衡的功能:毕竟要保证同一个Session的请求总是要转发到同一个实例。这看起来不先进且应用的正常工作需要对负载均衡器有依赖和粘性,如果换个不支持会话跟踪的负载均衡器应用便无法多实例部署显然是难以接受的。

另一种方案则高明许多:使用分布式缓存对Session进行存储以便多应用实例能够共享:

这确实是个好的解决方案,但是它需要对应用进行一点小的改动,幸运的是我们是ASP.NET Core用户,它的Session存储技术已经支持分布式缓存方案:

引入nugetMicrosoft.Extensions.Caching.StackExchangeRedis

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。这是个啥玩意呢?简单来说ASP.NET Core为了数据安全做了很多努力,其中之一就是对Session的安全处理措施,ASP.NET Core使用了秘钥对Session信息进行了加密处理。问题关键在于这个密钥,它的存储不是分布式的,它可能会存储在%LOCALAPPDATA%\ASP.NET\DataProtection-Keys文件夹或内存中(具体的存储地址:Data Protection key management and lifetime in ASP.NET Core),总之它默认不在多个应用实例中共享,这就导致一个应用实例使用密钥加密的Session数据无法被另一个应用实例解密出数据。

当然提出这个问题肯定是有解决方案的,继续使用分布式存储方案对密钥进行存储也不失为一种优雅(使用Redis作为示例):

引入nugetMicrosoft.AspNetCore.DataProtection.StackExchangeRedis

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();
        });
    }
}

Reference

阅读更多

以太坊黑暗森林-抢跑(front running)

以太坊黑暗森林-抢跑(front running)

前言 鸽了很久之后的今天突然心血来潮,准备写一个系列:以太坊黑暗森林,它介绍以太坊生态上的各种奇思妙想和逆天的攻击方式,会从简单的、常见的攻击方式开始介绍。取这个名字是因为我接触以太坊不久后看的一篇文章 Ethereum is a Dark Forest ,让我想起了《三体》小说中刘慈欣描述的黑暗森林,以太坊是一个弱肉强食的、没有规则的世界,猎人们总是躲在背后监听所有的交易,一旦发现猎物,它们会把它的血给吸干。 开盘抢币 相信进入以太坊生态的韭菜们,一定有过在 uniswap 上买刚开盘新币的经历,新开盘的币,一般会上涨几倍甚至十几倍,越早买入则越能低价买入。你守着时间,等着项目方添加流动性后第一时间买入代币,但是你发现,无论你的手速多块,总是看到一开盘,价格已经飚了几倍,你骂骂咧咧,开始不断拉高 gas 费用,尝试继续买入,但是你眼睁睁的看着代币涨到十倍,自己的交易却一直失败,你开始怀疑项目方自己抢跑,怀疑项目方捣鬼:肯定是项目方吃相难看,用老鼠仓提前买了。另一些聪明人,研究了以太坊的基本技术,他们在 ethscan

By FatTiger
C#:IDisposable 和 析构函数

C#:IDisposable 和 析构函数

C# 中有两种释放资源的方式:实现 IDisposable 或使用析构函数。通常,必须在特定时间释放资源的场景中,我们实现 IDisposable,像这样: public class ExampleDispose : IDisposable { // 非托管资源 private IntPtr _handle; // 使用的其它托管资源 private readonly Stream _stream; private bool disposed = false; public ExampleDispose(Stream stream, IntPtr handle) { this._stream = stream; this._handle = handle; } public void Dispose() { if (disposed) { return; } disposed = t

By FatTiger
ThreadLocal引发的灾难

ThreadLocal引发的灾难

在 Java 里有个称之为线程本地变量的类型叫做 ThreadLocal,它与 ThreadLocal 之于 C# 中是一样的作用,可以在线程范围内设置变量,这个变量只会在当前线程可被访问,但是它们有一点不同的是,在 Java 中,当你设置好变量后,在线程使用完毕回到线程池之前,需要手动调用 ThreadLocal.remove() 方法去清除线程本地变量,否则变量随着线程回到线程池,并且在下次使用此线程时此变量继续存在,而在 C# 中,线程回到线程池时会自动清除本地变量,因此无需手动去清除。 我们的业务有这样一个场景:某个业务 UserService 类中,具有多个方法会频繁(甚至循环)调用一个获取用户标签的接口,具体原因是因为某些方法会进行递归,数据结构有个树状结构,因此,为了优化接口响应时间以及看起来不那么蠢,我使用 ThreadLocal 将用户标签接口的返回数据存储到当前线程,因为在单个请求中,多次调用此接口获取数据是不必要的,它看起来像这样: /** * 此静态变量ThreadLocal会为每个线程创建本地副本, 因此USER_TAGS_THREAD_

By FatTiger
我在币安智能链的日子-区块链基础

我在币安智能链的日子-区块链基础

区块和链 无论是比特币还是以太坊,都是具有一个个区块(称之为Block)的链式结构,学过<数据结构>的肯定明白链表,区块链就像一个链表,每个区块都存储上一个区块哈希。 链(称之为Chain),有非常多的链,他们的协议不同,技术也不尽相同,比特币网络是一个链,以太坊网络是另一个链,每个链都有自己的目标(甚至目标只是为了圈钱),每个链也都有自己的代币,比特币网络的代币是比特币,每次交易都需要比特币作为手续费,以太坊网络代币是以太币,每次在以太坊网络的交易都需要以太币作为手续费。所以,链实际上作为基础设施,非常多的团队喜欢创建新的链,但是一个链光有网络光有代币不行,没有生态,很难成功。 币安智能链(Binance Smart Chain:BSC) 我的主要操作都是在BSC上,没有其它原因,只因为一个穷字。在BTC网络交易,需要BTC用作手续费,这个我可用不起,在以太坊(Ethereum)网络交易,需要以太币(ETH)作为用作手续费,按照以太币目前(

By FatTiger