105

ASP.NET Core中间件实现分布式 Session - 行动派Xdpie

 6 years ago
source link: http://www.cnblogs.com/vipyoumay/p/7771237.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

1.1. 中间件原理

1.1.1. 什么是中间件

中间件是段代码用于处理请求和响应,通常多个中间件链接起来形成管道,由每个中间件自己来决定是否要调用下一个中间件。

1.1.2. 中间件执行过程

举一个示例来演示中间件的执行过程(分别有三个中间件:日志记录、权限验证和路由):当请求进入应用程序时,执行执行日志记录的中间件,它记录请求属性并调用链中的下一个中间件权限验证,如果权限验证通过则将控制权传递给下一个中间件,不通过则设置401 HTTP代码并返回响应,响应传递给日志中间件进行返回。

1.1.3. 中间件的配置

中间件配置主要是用RunMapUse方法进行配置,三者的不同参见上篇ASP.NET Core 运行原理剖析;简单的中间件可以直接使用匿名方法就可以搞定,如下代码:

app.Run(async (context,next) =>
        {
            await context.Response.WriteAsync("environment " + env);
            await next();
        });

如果想重用中间件,就需要单独封装到一个类中进行调用。

1.2. 依赖注入中间件

在实际项目中,中间件往往需要调用其它对象的方法。所以要创建对象之间的依赖,由于ASP.NET Core 内置的依赖注入系统,写程序的时候可以创建更优雅的代码。

首先需要要在IOC容器中注册类,就是Startup类中的ConfigureServices方法中进行注册,ConfigureServices方法会在Configure方法之前被执行。以便在用中间件时所有依赖都准备好了。

现在有一个Greeter类:

public class Greeter : IGreeter
{
    public string Greet()
    {
        return "Hello from Greeter!";
    }
}

public interface IGreeter
{
    string Greet();
}

第一步在ConfigureServices方法中进行注册


public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IGreeter, Greeter>();
}

笔者这里使用的是AddTransient进行注册,该方法在每次请求时创建该类的新实例。可以选择其它方法:AddSingleton,AddScoped或简单的Add(所有在幕后前使用)。整个DI系统在官方文档中有所描述。

在注册了依赖项后,就可以使用它们了。IApplicationBuilder实例允许在Configure方法中有一个RequestServices属性用于获取Greeter实例。由于已经注册了这个IGreeter接口,所以不需要将中间件与具体的Greeter实现相结合。


app.Use(async (ctx, next) =>
    {
        IGreeter greeter = ctx.RequestServices.GetService<IGreeter>();
        await ctx.Response.WriteAsync(greeter.Greet());
        await next();
    });

如果Greeter类有一个参数化的构造函数,它的依赖关系也必须在其中注册ConfigureServices

中间件可以很容易解决依赖关系。可以向中间件构造函数添加其他参数:


public class MyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IGreeter _greeter;

    public MyMiddleware(RequestDelegate next, IGreeter greeter)
    {
        _next = next;
        greeter = greeter;
    }

    public async Task Invoke(HttpContext context)
    {
        await context.Response.WriteAsync(_greeter.Greet());
        await _next(context);
    }
}

或者,可以将此依赖关系添加到Invoke方法中:


public async Task Invoke(HttpContext context, IGreeter greeter)
{
    await context.Response.WriteAsync(greeter.Greet());
    await _next(context);
}

如果DI系统知道这些参数的类型,则在类被实例化时,它们将被自动解析。很简单!

1.3. Cookies和session中间件

1.3.1. Session

HTTP是一个无状态协议,Web服务器将每一个请求都视为独立请求。并且不保存之前请求中用户的值。

Session 状态是ASP.NET Core提供的一个功能,它可以在用户通应用访问网络服务器的时候保存和存储用户数据。由服务器上的字典和散列表组成,Session状态通过浏览器的请求中得到,Session的数据保存到缓存中。

ASP.NET Core通过包含Session ID的Cookie来维护会话状态,每个请求都会携带此Session ID。

Microsoft.AspNetCore.Session包中提供的中间件用来管理Session状态。要启用Session中间件,Startup类里面需要做以下几个操作:

  • 使用任何一个实现了IDistributedCache接口的服务来启用内存缓存,
  • 设置AddSession回调,由于AddSession是在Microsoft.AspNetCore.Session包内实现的,所以必须在Nuget中添加Microsoft.AspNetCore.Session
  • UseSession回调

具体示例代码如下:


using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using System;

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();

        // 添加一个内存缓存
        services.AddDistributedMemoryCache();

        services.AddSession(options =>
        {
            // 设置10秒钟Session过期来测试
            options.IdleTimeout = TimeSpan.FromSeconds(10);
            options.Cookie.HttpOnly = true;
        });
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseSession();
        app.UseMvcWithDefaultRoute();
    }
}

上面代码中IdleTimeout属性用来确定用户多久没有操作时丢弃Session。此属性和Cookie超时无关,通过Session中间件的每个请求都会重置超时时间。

1.3.2. Session保存到Redis中

实现分布式Session方法官方提供有Redis、Sql Server等。但是Sql Server效率对于这种以key/value获取值的方式远远不及Redis效率高,所以这里笔者选用Redis来作示例实现分布式Session。

准备Redis

由于目前Redis还不支持windows,所以大家在安装Redis的时候准备一台linux操作系统,笔者这里的系统是ubuntu 16.04;下载及安装方式可以参考官方示例。

安装成功以后启动Redis 服务,如果看到以下信息,就代表Redis启动成功:

首先需要用Nuget安装包Microsoft.Extensions.Caching.Redis,安装成功以后就可以在app.csproj文件中可以看到。

Configure方法中添加app.UseSession();然后再ConfigureServices添加Redis服务


public void ConfigureServices(IServiceCollection services){
    services.AddDistributedRedisCache(options=>{
        options.Configuration="127.0.0.1"; //多个redis服务器:{RedisIP}:{Redis端口},{RedisIP}:{Redis端口}
        options.InstanceName="sampleInstance";
    });
    services.AddMvc();
    services.AddSession();
}

以上代码中笔者只用一个Redis服务器来作测试,实际项目中需要多个Redis服务器;配置方法如:options.Configuration="地址1:端口,地址2:端口";,这里笔者并没有给端口而是用的默认端口6379

Startup.cs


using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Caching.Redis;
using Microsoft.Extensions.Caching.Distributed;

namespace app{    
    public class Startup{        
        public Startup(IConfiguration configuration)        
        {            
            Configuration = configuration;        
        }
        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services){                     
            services.AddDistributedRedisCache(options =>{                
                options.Configuration = "127.0.0.1";                
                options.InstanceName = "sampleInstance";            
            });            
            services.AddMvc();            
            services.AddSession();        
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env){            
            if (env.IsDevelopment())
            {                
                app.UseDeveloperExceptionPage();            
            }            
            else            
            {                
                app.UseExceptionHandler("/Home/Error");            
            }
            app.UseSession();
            app.UseStaticFiles();
            app.UseMvc(routes =>{                
                routes.MapRoute(name: "default",template: "{controller=Home}/{action=Index}/{id?}");           
            });        
        }    
    }
}

HomeControler.cs


public class HomeController : Controller   
{        
    public IActionResult Index()       
    {            
        HttpContext.Session.Set("apptest",Encoding.UTF8.GetBytes("apptestvalue"));
        return View();        
    }
    public IActionResult ShowRedis()        
    {            
        byte[] temp;
        if(HttpContext.Session.TryGetValue("apptest",out temp))
        {                
            ViewData["Redis"]=Encoding.UTF8.GetString(temp);            
        }            
        return View();        
    }
}

Index页面只做一件事给Session设置值:"apptestvalue",ShowRedis页面显示Session值。

ShowRedis.cshtml


Redis Session Value:ViewData["Redis"]

现在开始运行页面,首先直接进入到ShowRedis页面,Session值显示为空

当点击SetSessionValue以后,再次回到ShowRedis页面,Session就值显示出来了

看到apptestvalue代表Session值已经存到Redis里面,怎样证明apptestvalue值是从Redis里面取到呢?接下来就证明给大家看。

1.3.3. 实现分布Session

前面已经将Session保存到Redis中,但是大家不清楚这个值是否是真的保存到Redis里面去了还是在项目内存中;所以这里就实现在两个不的应用程序(或两台不同的机器)中共享Session,也就是实现分布式Session,分布式即代表了不同的机器不同的应用程序,但往往有下面的一种尴尬的情况,就算是每个HTTP请求时都携带了相同的cookie值。

造成这个的问题的原因是每个机器上面的ASP.NET Core的应用程序的密钥是不一样的,所以没有办法得到前一个应用程序保存的Session数据;为了解决这个问题,.NET Core团队为提供了Microsoft.AspNetCore.DataProtection.AzureStorageMicrosoft.AspNetCore.DataProtection.Redis包将密钥保存到Azure或Redis中。这里选择将密钥保存到Redis。

共享密钥

利用Microsoft.AspNetCore.DataProtection.Redis包提供的PersistKeysToRedis重载方法将密钥保存到Redis里面去。所以这里需要在ConfigureServices方法中添AddDataProtection()


var redis = ConnectionMultiplexer.Connect("127.0.0.1:6379");
    services.AddDataProtection()
        .SetApplicationName("session_application_name")
        .PersistKeysToRedis(redis, "DataProtection-Keys");

下面演示怎样实现分布式Session

  • 同时创建两个项目,分别为app1和app2
  • 添加Microsoft.AspNetCore.DataProtection.RedisStackExchange.Redis.StrongName
  • 由于在同一台机器上,ASP.NET Core程序默认启动的时候端口为5000,由于app1已经占用了,所以将app2的启端口设置为5001
  • app1项目

Startup.cs


using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Caching.Redis;
using Microsoft.Extensions.Caching.Distributed;

namespace app1{    
    public class Startup{        
        public Startup(IConfiguration configuration)        
        {            
            Configuration = configuration;        
        }
        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services){
            var redis = ConnectionMultiplexer.Connect("127.0.0.1:6379");
            services.AddDataProtection()
                .SetApplicationName("session_application_name")
                .PersistKeysToRedis(redis, "DataProtection-Keys");          
            services.AddDistributedRedisCache(options =>{                
                options.Configuration = "127.0.0.1";                
                options.InstanceName = "sampleInstance";            
            });            
            services.AddMvc();            
            services.AddSession();        
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env){            
            if (env.IsDevelopment())
            {                
                app.UseDeveloperExceptionPage();            
            }            
            else            
            {                
                app.UseExceptionHandler("/Home/Error");            
            }
            app.UseSession();
            app.UseStaticFiles();
            app.UseMvc(routes =>{                
                routes.MapRoute(name: "default",template: "{controller=Home}/{action=Index}/{id?}");           
            });        
        }    
    }
}

HomeControler.cs


public class HomeController : Controller   
{        
    public IActionResult Index()       
    {            
        HttpContext.Session.Set("app1test",Encoding.UTF8.GetBytes("app1testvalue"));
        return View();        
    }
    public IActionResult ShowRedis()        
    {            
        byte[] temp;
        if(HttpContext.Session.TryGetValue("app1test",out temp))
        {                
            ViewData["Redis"]=Encoding.UTF8.GetString(temp);            
        }            
        return View();        
    }
}

ShowRedis.cshtml


Redis Session Value:ViewData["Redis"]

  • app2项目

Startup.cs
配置同app1配置一样。

HomeControler.cs


public class HomeController : Controller   
{        
    public IActionResult Index()       
    {            
        byte[] temp;
        if(HttpContext.Session.TryGetValue("app1test",out temp))
        {                
            ViewData["Redis"]=Encoding.UTF8.GetString(temp);            
        }  
        return View();        
    }
}

Index.cshtml


ViewData["Redis"]

  • app1 项目

首次打开进入ShowRedis页面,Session值为空

点击SetSessionValue以后,再回到ShowRedis页面:

以上是用Redis实现分布式Session示例。

1.4. 总结

本节讲解了中间件的运行原理及配置过程,中间件之间对象依赖关系的配置和平时项目中常用到Session的配置问题。并在实际代码展示了怎样使用中间件实现分布式Session。

作者:xdpie 出处: http://www.cnblogs.com/vipyoumay/p/7771237.html


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK