1. 程式人生 > >ASP.NET Core中介軟體實現分散式 Session

ASP.NET Core中介軟體實現分散式 Session

1.1. 中介軟體原理

1.1.1. 什麼是中介軟體

中介軟體是段程式碼用於處理請求和響應,通常多箇中間件連結起來形成管道,由每個中介軟體自己來決定是否要呼叫下一個中介軟體。

2017-10-10-21-47-00

1.1.2. 中介軟體執行過程

舉一個示例來演示中介軟體的執行過程(分別有三個中介軟體:日誌記錄、許可權驗證和路由):當請求進入應用程式時,執行執行日誌記錄的中介軟體,它記錄請求屬性並呼叫鏈中的下一個中介軟體許可權驗證,如果許可權驗證通過則將控制權傳遞給下一個中介軟體,不通過則設定401 HTTP程式碼並返回響應,響應傳遞給日誌中介軟體進行返回。

2017-10-10-22-47-32

1.1.3. 中介軟體的配置

中介軟體配置主要是用Run

MapUse方法進行配置,三者的不同參見上篇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啟動成功:

2017-10-30-20-33-47

相關配置

首先需要用Nuget安裝包Microsoft.Extensions.Caching.Redis,安裝成功以後就可以在app.csproj檔案中可以看到。

2017-10-30-20-12-20

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值顯示為空

2017-10-31-06-47-24

當點選SetSessionValue以後,再次回到ShowRedis頁面,Session就值顯示出來了

2017-10-31-23-56-58

看到apptestvalue代表Session值已經存到Redis裡面,怎樣證明apptestvalue值是從Redis裡面取到呢?接下來就證明給大家看。

1.3.3. 實現分佈Session

前面已經將Session儲存到Redis中,但是大家不清楚這個值是否是真的儲存到Redis裡面去了還是在專案記憶體中;所以這裡就實現在兩個不的應用程式(或兩臺不同的機器)中共享Session,也就是實現分散式Session,分散式即代表了不同的機器不同的應用程式,但往往有下面的一種尷尬的情況,就算是每個HTTP請求時都攜帶了相同的cookie值。

2017-10-30-20-35-19

造成這個的問題的原因是每個機器上面的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

2017-10-31-23-41-37

  • 由於在同一臺機器上,ASP.NET Core程式預設啟動的時候埠為5000,由於app1已經佔用了,所以將app2的啟埠設定為5001

2017-10-31-23-54-19

完整程式碼

  • 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值為空

2017-10-31-06-47-24

點選SetSessionValue以後,再回到ShowRedis頁面:

2017-11-01-00-04-13

2017-11-01-00-00-30

以上是用Redis實現分散式Session示例。

1.4. 總結

本節講解了中介軟體的執行原理及配置過程,中介軟體之間物件依賴關係的配置和平時專案中常用到Session的配置問題。並在實際程式碼展示了怎樣使用中介軟體實現分散式Session。