ASP.NET Core - 實現Http自定義請求頭策略

wu發表於2020-08-10

  前言  

  在正常的情況下,當我們系統用到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頭部請求資訊,在每次的本地呼叫中,可通過以下方式把我們需要傳遞的自定義資訊填充進去。

 

相關文章