淺談ASP.NET Core中間件實(shí)現(xiàn)分布式 Session
1.1. 中間件原理
1.1.1. 什么是中間件
中間件是段代碼用于處理請(qǐng)求和響應(yīng),通常多個(gè)中間件鏈接起來(lái)形成管道,由每個(gè)中間件自己來(lái)決定是否要調(diào)用下一個(gè)中間件。

1.1.2. 中間件執(zhí)行過(guò)程
舉一個(gè)示例來(lái)演示中間件的執(zhí)行過(guò)程(分別有三個(gè)中間件:日志記錄、權(quán)限驗(yàn)證和路由):當(dāng)請(qǐng)求進(jìn)入應(yīng)用程序時(shí),執(zhí)行執(zhí)行日志記錄的中間件,它記錄請(qǐng)求屬性并調(diào)用鏈中的下一個(gè)中間件權(quán)限驗(yàn)證,如果權(quán)限驗(yàn)證通過(guò)則將控制權(quán)傳遞給下一個(gè)中間件,不通過(guò)則設(shè)置401 HTTP代碼并返回響應(yīng),響應(yīng)傳遞給日志中間件進(jìn)行返回。

1.1.3. 中間件的配置
中間件配置主要是用Run、Map和Use方法進(jìn)行配置;簡(jiǎn)單的中間件可以直接使用匿名方法就可以搞定,如下代碼:
app.Run(async (context,next) =>
{
await context.Response.WriteAsync("environment " + env);
await next();
});
如果想重用中間件,就需要單獨(dú)封裝到一個(gè)類中進(jìn)行調(diào)用。
1.2. 依賴注入中間件
在實(shí)際項(xiàng)目中,中間件往往需要調(diào)用其它對(duì)象的方法。所以要?jiǎng)?chuàng)建對(duì)象之間的依賴,由于ASP.NET Core 內(nèi)置的依賴注入系統(tǒng),寫程序的時(shí)候可以創(chuàng)建更優(yōu)雅的代碼。
首先需要要在IOC容器中注冊(cè)類,就是Startup類中的ConfigureServices方法中進(jìn)行注冊(cè),ConfigureServices方法會(huì)在Configure方法之前被執(zhí)行。以便在用中間件時(shí)所有依賴都準(zhǔn)備好了。
現(xiàn)在有一個(gè)Greeter類:
public class Greeter : IGreeter
{
public string Greet()
{
return "Hello from Greeter!";
}
}
public interface IGreeter
{
string Greet();
}
第一步在ConfigureServices方法中進(jìn)行注冊(cè)
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IGreeter, Greeter>();
}
筆者這里使用的是AddTransient進(jìn)行注冊(cè),該方法在每次請(qǐng)求時(shí)創(chuàng)建該類的新實(shí)例。可以選擇其它方法:AddSingleton,AddScoped或簡(jiǎn)單的Add(所有在幕后前使用)。整個(gè)DI系統(tǒng)在官方文檔中有所描述。
在注冊(cè)了依賴項(xiàng)后,就可以使用它們了。IApplicationBuilder實(shí)例允許在Configure方法中有一個(gè)RequestServices屬性用于獲取Greeter實(shí)例。由于已經(jīng)注冊(cè)了這個(gè)IGreeter接口,所以不需要將中間件與具體的Greeter實(shí)現(xiàn)相結(jié)合。
app.Use(async (ctx, next) =>
{
IGreeter greeter = ctx.RequestServices.GetService<IGreeter>();
await ctx.Response.WriteAsync(greeter.Greet());
await next();
});
如果Greeter類有一個(gè)參數(shù)化的構(gòu)造函數(shù),它的依賴關(guān)系也必須在其中注冊(cè)ConfigureServices。
中間件可以很容易解決依賴關(guān)系。可以向中間件構(gòu)造函數(shù)添加其他參數(shù):
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);
}
}
或者,可以將此依賴關(guān)系添加到Invoke方法中:
public async Task Invoke(HttpContext context, IGreeter greeter)
{
await context.Response.WriteAsync(greeter.Greet());
await _next(context);
}
如果DI系統(tǒng)知道這些參數(shù)的類型,則在類被實(shí)例化時(shí),它們將被自動(dòng)解析。很簡(jiǎn)單!
1.3. Cookies和session中間件
1.3.1. Session
HTTP是一個(gè)無(wú)狀態(tài)協(xié)議,Web服務(wù)器將每一個(gè)請(qǐng)求都視為獨(dú)立請(qǐng)求。并且不保存之前請(qǐng)求中用戶的值。
Session 狀態(tài)是ASP.NET Core提供的一個(gè)功能,它可以在用戶通應(yīng)用訪問(wèn)網(wǎng)絡(luò)服務(wù)器的時(shí)候保存和存儲(chǔ)用戶數(shù)據(jù)。由服務(wù)器上的字典和散列表組成,Session狀態(tài)通過(guò)瀏覽器的請(qǐng)求中得到,Session的數(shù)據(jù)保存到緩存中。
ASP.NET Core通過(guò)包含Session ID的Cookie來(lái)維護(hù)會(huì)話狀態(tài),每個(gè)請(qǐng)求都會(huì)攜帶此Session ID。
在Microsoft.AspNetCore.Session包中提供的中間件用來(lái)管理Session狀態(tài)。要啟用Session中間件,Startup類里面需要做以下幾個(gè)操作:
- 使用任何一個(gè)實(shí)現(xiàn)了IDistributedCache接口的服務(wù)來(lái)啟用內(nèi)存緩存,
- 設(shè)置AddSession回調(diào),由于AddSession是在Microsoft.AspNetCore.Session包內(nèi)實(shí)現(xiàn)的,所以必須在Nuget中添加Microsoft.AspNetCore.Session包
- UseSession回調(diào)
具體示例代碼如下:
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using System;
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// 添加一個(gè)內(nèi)存緩存
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
// 設(shè)置10秒鐘Session過(guò)期來(lái)測(cè)試
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.Cookie.HttpOnly = true;
});
}
public void Configure(IApplicationBuilder app)
{
app.UseSession();
app.UseMvcWithDefaultRoute();
}
}
上面代碼中IdleTimeout屬性用來(lái)確定用戶多久沒(méi)有操作時(shí)丟棄Session。此屬性和Cookie超時(shí)無(wú)關(guān),通過(guò)Session中間件的每個(gè)請(qǐng)求都會(huì)重置超時(shí)時(shí)間。
1.3.2. Session保存到Redis中
實(shí)現(xiàn)分布式Session方法官方提供有Redis、Sql Server等。但是Sql Server效率對(duì)于這種以key/value獲取值的方式遠(yuǎn)遠(yuǎn)不及Redis效率高,所以這里筆者選用Redis來(lái)作示例實(shí)現(xiàn)分布式Session。
準(zhǔn)備Redis
由于目前Redis還不支持windows,所以大家在安裝Redis的時(shí)候準(zhǔn)備一臺(tái)linux操作系統(tǒng),筆者這里的系統(tǒng)是ubuntu 16.04;下載及安裝方式可以參考官方示例。
安裝成功以后啟動(dòng)Redis 服務(wù),如果看到以下信息,就代表Redis啟動(dòng)成功:

相關(guān)配置
首先需要用Nuget安裝包Microsoft.Extensions.Caching.Redis,安裝成功以后就可以在app.csproj文件中可以看到。

在Configure方法中添加app.UseSession();然后再ConfigureServices添加Redis服務(wù)
public void ConfigureServices(IServiceCollection services){
services.AddDistributedRedisCache(options=>{
options.Configuration="127.0.0.1"; //多個(gè)redis服務(wù)器:{RedisIP}:{Redis端口},{RedisIP}:{Redis端口}
options.InstanceName="sampleInstance";
});
services.AddMvc();
services.AddSession();
}
以上代碼中筆者只用一個(gè)Redis服務(wù)器來(lái)作測(cè)試,實(shí)際項(xiàng)目中需要多個(gè)Redis服務(wù)器;配置方法如:options.Configuration="地址1:端口,地址2:端口";,這里筆者并沒(méi)有給端口而是用的默認(rèn)端口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頁(yè)面只做一件事給Session設(shè)置值:"apptestvalue",ShowRedis頁(yè)面顯示Session值。
ShowRedis.cshtml
Redis Session Value:ViewData["Redis"]
演示結(jié)果
現(xiàn)在開始運(yùn)行頁(yè)面,首先直接進(jìn)入到ShowRedis頁(yè)面,Session值顯示為空

當(dāng)點(diǎn)擊SetSessionValue以后,再次回到ShowRedis頁(yè)面,Session就值顯示出來(lái)了

看到apptestvalue代表Session值已經(jīng)存到Redis里面,怎樣證明apptestvalue值是從Redis里面取到呢?接下來(lái)就證明給大家看。
1.3.3. 實(shí)現(xiàn)分布Session
前面已經(jīng)將Session保存到Redis中,但是大家不清楚這個(gè)值是否是真的保存到Redis里面去了還是在項(xiàng)目?jī)?nèi)存中;所以這里就實(shí)現(xiàn)在兩個(gè)不的應(yīng)用程序(或兩臺(tái)不同的機(jī)器)中共享Session,也就是實(shí)現(xiàn)分布式Session,分布式即代表了不同的機(jī)器不同的應(yīng)用程序,但往往有下面的一種尷尬的情況,就算是每個(gè)HTTP請(qǐng)求時(shí)都攜帶了相同的cookie值。

造成這個(gè)的問(wèn)題的原因是每個(gè)機(jī)器上面的ASP.NET Core的應(yīng)用程序的密鑰是不一樣的,所以沒(méi)有辦法得到前一個(gè)應(yīng)用程序保存的Session數(shù)據(jù);為了解決這個(gè)問(wèn)題,.NET Core團(tuán)隊(duì)為提供了Microsoft.AspNetCore.DataProtection.AzureStorage和Microsoft.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");
下面演示怎樣實(shí)現(xiàn)分布式Session
配置步驟
同時(shí)創(chuàng)建兩個(gè)項(xiàng)目,分別為app1和app2
添加Microsoft.AspNetCore.DataProtection.Redis和StackExchange.Redis.StrongName包

由于在同一臺(tái)機(jī)器上,ASP.NET Core程序默認(rèn)啟動(dòng)的時(shí)候端口為5000,由于app1已經(jīng)占用了,所以將app2的啟端口設(shè)置為5001

完整代碼
app1項(xiàng)目
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項(xiàng)目
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"]
運(yùn)行效果
app1 項(xiàng)目
首次打開進(jìn)入ShowRedis頁(yè)面,Session值為空

點(diǎn)擊SetSessionValue以后,再回到ShowRedis頁(yè)面:

app2項(xiàng)目,直接在瀏覽器訪問(wèn):http://localhost:5001

以上是用Redis實(shí)現(xiàn)分布式Session示例。
1.4. 總結(jié)
本節(jié)講解了中間件的運(yùn)行原理及配置過(guò)程,中間件之間對(duì)象依賴關(guān)系的配置和平時(shí)項(xiàng)目中常用到Session的配置問(wèn)題。并在實(shí)際代碼展示了怎樣使用中間件實(shí)現(xiàn)分布式Session。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- 如何在ASP.NET Core中使用Session的示例代碼
- 如何解決asp.net負(fù)載均衡時(shí)Session共享的問(wèn)題
- Asp.Net Core中基于Session的身份驗(yàn)證的實(shí)現(xiàn)
- 解析Asp.net Core中使用Session的方法
- asp.net(C#)清除全部Session與單個(gè)Session的方法
- 詳解ASP.NET中Session的用法
- ASP.NET ASHX中獲得Session的方法
- ASP.NET將Session保存到數(shù)據(jù)庫(kù)中的方法
- asp.net session的使用與過(guò)期實(shí)例代碼
- Asp.net中判斷一個(gè)session是否合法的方法
- ASP.NET MVC在基控制器中處理Session
相關(guān)文章
C# 無(wú)限級(jí)分類的實(shí)現(xiàn)
采用存儲(chǔ)過(guò)程實(shí)現(xiàn)遞歸邏輯,直接返回子分類列表的方式應(yīng)該有更好的性能,尤其是Web服務(wù)器與數(shù)據(jù)庫(kù)服務(wù)器不位于同一臺(tái)服務(wù)器上時(shí),更會(huì)受網(wǎng)絡(luò)影響。2009-02-02
asp.net(vb)實(shí)現(xiàn)金額轉(zhuǎn)換成大寫的函數(shù)
asp.net(vb)實(shí)現(xiàn)金額轉(zhuǎn)換成大寫的函數(shù)代碼,需要的朋友可以參考下。2011-10-10
.NET?SkiaSharp?生成二維碼驗(yàn)證碼及指定區(qū)域截取方法實(shí)現(xiàn)
這篇文章主要為大家介紹了.NET?SkiaSharp?生成二維碼驗(yàn)證碼及指定區(qū)域截取方法實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10

