ASP.NET Core – 實現Http自定義請求頭策略
- 2020 年 8 月 10 日
- 筆記
- AspNet Core
前言
在正常的情況下,當我們系統用到JWT認證方式時,需要在Http請求頭添加Authorization: XXX,這樣在後台服務的控制器中打上[Authorize]授權標籤,就限定所有的請求必須通過鑒權方可訪問。
在【ASP.NET Core – 基於IHttpContextAccessor實現系統級別身份標識】這篇文章中我們能夠注意到,通過IHttpContextAccessor獲取基於請求生成的HttpContext後,我們是能夠拿到基於該次請求的所有http資訊的。這就給予了我們可以基於Http的請求做自定義請求頭的策略來滿足我們業務的擴展。
場景
在不少的場景中,特別是多租戶的場景中,我們是需要知道當前是哪個租戶下的用戶在操作,而我們又不需要把這租戶作為業務參數傳遞,畢竟如果按照業務參數這樣的方式去實現的話,就好比從源頭傳入了一個污染源,需要一路污染下去,這是非常不推薦的方式,這時就可以利用自定義http請求頭來傳遞這個參數,在最終需要用到這個參數的時候獲取出來。
在我們的系統層面,例如我們的應用部署在K8S集群中的網路,在網關層解析了JWT通過認證後放行之後,內網的應用服務可以依賴內網的保護把JWT的認證刪減掉(這樣在一定程上是可以提高性能的),我們就可以通過以下自定義頭的實現方式,把租戶資訊攜帶在http頭部,一路傳遞下去。
實現
類似於我們在【ASP.NET Core – 基於IHttpContextAccessor實現系統級別身份標識】中的IHttpContextAccessor實現方式一樣,我們先定義我們的IHttpHeaderAccessor用來獲取HttpHeader
/// <summary> /// httpcontext的header /// </summary> public interface IHttpHeaderAccessor { /// <summary> /// the httpheader /// </summary> IHeaderDictionary HttpHeader { get; } }
HttpHeaderAccessor 的實現
/// <summary> /// httpheader訪問器 /// </summary> public class HttpHeaderAccessor : IHttpHeaderAccessor { /// <summary> /// the HttpContextAccessor /// </summary> private readonly IHttpContextAccessor _httpContextAccessor; public IHeaderDictionary HttpHeader => _httpContextAccessor.HttpContext.Request.Headers; /// <summary> /// ctor /// </summary> /// <param name="httpContextAccessor">the HttpContextAccessor</param> public HttpHeaderAccessor(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } }
定義http請求頭的自定義參數
public interface ICustomHeaderAccessor { /// <summary> /// 租戶Id /// </summary> string TenantId { get; } }
http請求頭的自定義參數的獲取,這裡是直接從頭部獲取到租戶資訊
/// <summary> /// 獲取自定義http頭 /// </summary> public class CustomHeaderAccessor : ICustomHeaderAccessor { protected IHttpHeaderAccessor _httpHeaderAccessor { get; } /// <summary> /// ctor /// </summary> /// <param name="httpHeaderAccessor"></param> public CustomHeaderAccessor(IHttpHeaderAccessor httpHeaderAccessor) { _httpHeaderAccessor = httpHeaderAccessor; } /// <summary> /// 租戶Id /// </summary> public string TenantId { get { return _httpHeaderAccessor.HttpHeader[HeaderConst.TenantId]; } } }
擴展
在【ASP.NET Core – 基於IHttpContextAccessor實現系統級別身份標識】我們知道IHttpContextAccessor,IHttpHeaderAccessor,ICustomHeaderAccessor都需要註冊,有了上面的這些方法定義,我們定義一個ServiceCollection的擴展方法進行註冊
/// <summary> /// 基於IServiceCollection的擴展類 /// </summary> public static class ServiceCollectionExtension { /// <summary> /// 註冊IHttpContextAccessor,IHttpHeaderAccessor和ICustomHeaderAccessor /// </summary> /// <param name="services">the IServiceCollection</param> /// <returns></returns> public static IServiceCollection AddCustomHttpHeader(this IServiceCollection services) { services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.AddSingleton<IHttpHeaderAccessor, HttpHeaderAccessor>(); services.AddSingleton<ICustomHeaderAccessor, CustomHeaderAccessor>(); return services; } }
有了服務的註冊,我們還需要一個當前服務的實例提供者,一個ServiceProvider,如下
public class ServiceProviderInstance { public static IServiceProvider Instance { get; set; } }
我們在獲取到當前進程的ServiceProvider後,保存到本地靜態變數
/// <summary> /// 基於IApplicationBuilder的擴展 /// </summary> public static class ServiceProviderExtension { /// <summary> /// 給ServiceProviderInstance賦值ServiceProvider實例 /// </summary> /// <param name="applicationBuilder">the IApplicationBuilder</param> /// <returns></returns> public static IApplicationBuilder UseServiceProviderBulider(this IApplicationBuilder applicationBuilder) { ServiceProviderInstance.Instance = applicationBuilder.ApplicationServices; return applicationBuilder; } }
這裡為什麼能夠在進程內保存一個靜態的ServiceProvider?
是因為在應用啟動過程中,已經把Ioc容器構建好,把該註冊的對象實例關係都已經保存在Ioc容器中了,這時ServiceProvider在進程內是不會有變化的了(動態註冊暫時沒在本篇文章的使用範圍)。
在這裡已經給我們提示了另一種對象實例獲取的方式,就是通過這個本地的ServiceProviderInstance來解析自己的所註冊的對象實例,而不僅僅是通過構造函數(或者屬性)注入的方式。
使用
看看我們如何完成整個流程的註冊以及使用。
首選需要在程式啟動的時候註冊。這裡通過註冊以及swagger的聲明,用來做為本地的調試請求頭添加
public void ConfigureServices(IServiceCollection services) { // 註冊Swagger services.AddAppSwagger(document => { document.Description = "API"; document.OperationProcessors.Add(new OperationSecurityScopeProcessor("TenantId")); document.DocumentProcessors.Add(new SecurityDefinitionAppender("TenantId", new NSwag.OpenApiSecurityScheme { Type = NSwag.OpenApiSecuritySchemeType.ApiKey, Name = "TenantId", In = NSwag.OpenApiSecurityApiKeyLocation.Header, Description = "租戶Id" })); services.AddHttpHeader(); }
獲取系統的ServiceProvider並保存到本地。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseServiceProviderBuilder(); // Swagger中間件 app.UseAppSwagger(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
定義應用服務的抽象類
public abstract class ServiceBase { /// <summary> /// 自定義頭資訊 /// </summary> protected ICustomHeaderAccessor CustomHeader{ get; set; } /// <summary> /// ctor /// </summary> protected ServiceBase () { CustomHeader = ServiceProviderInstance.Instance.GetRequiredService<ICustomHeaderAccessor>(); } }
使用層面上的賦值
public class TestService : ServiceBase,ITestService { private readonly IRepository<Product> _productRepository; public TestService(IRepository<Product> productRepository) { _productRepository = productRepository; } public Result AddProduct(ProductDto dto) { _productRepository.Insert(new Product { Name = dto.Name, ... TenantId = CustomHeader.TenantId }); } }
Swagger的本地調用
為了方便調試,我們在上面的註冊程式碼中寫入了通過swagger中的請求頭攜帶的http頭部請求資訊,在每次的本地調用中,可通過以下方式把我們需要傳遞的自定義資訊填充進去。