ASP.NET Core-IHttpContextAccessorに基づくシステムレベル識別

16678 ワード

問題の導入:


[ASP.NET Core[ソース分析編]-認証]という記事では、認証モジュールを通過するように要求されると、現在のHttpContextに現在のユーザーIDが付与され、認証が必要なコントローラに[Authorize]認証ラベルを付けると、ControllerBaseのUserプロパティで宣言ベースの権限ID(ClaimsPrincipal)を取得できることを知っています.
残念なことに、これはControllerレベルにすぎません.多くのシーンでは、サービス層やデータ層でユーザー情報を直接使用する必要があります.この場合、Userは使用できません.  

ソリューション:


Asp.net 4.x時代、私たちの一般的なやり方はHttpContext.を通じてCurrentは、現在のリクエストのコンテキストを取得し、現在のUserプロパティを取得するので、問題の切り込み点は、現在のHttpContextコンテキストをどのように取得するかです.
我々のAspnet Coreアプリケーションでは,HttpContextのアクセサオブジェクトIHttpContextAccessorを注入することによって現在のHttpContextを取得する.

インプリメンテーション


まず、StartupのConfigureServicesメソッドにIHttpContextAccessorのインスタンスを登録する必要があります.
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

    ....
}

この登録により、IHttpContextAccessorのHttpContextから対応するClaimsPrincipalを取得する方法をカプセル化し、以下のようにします(認証に合格した後、Userは現在のユーザーのアイデンティティフラグを持つClaimsPrincipalです):
public class PrincipalAccessor : IPrincipalAccessor
{
  // ,User
public ClaimsPrincipal Principal => _httpContextAccessor.HttpContext?.User; private readonly IHttpContextAccessor _httpContextAccessor; public PrincipalAccessor(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } } public interface IPrincipalAccessor { ClaimsPrincipal Principal { get; } }

StartupのConfigureServicesメソッドでは、同様にこのクラスも登録に追加します.
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton();
    services.AddSingleton<IPrincipalAccessor, PrincipalAccessor>();
    ....
}

以上のようなインフラストラクチャが提供するものがあって、残りはどのようにClaimsPrincipalから対応するClaimsを取得する必要があるかです.ここでは、PrincipalAccessorからユーザーのアイデンティティフラグ情報を抽出するClaimsAccessorクラスを定義します.例えば、ユーザーのロール、Idなどのビジネスデータは、Tokenを取得する際にシステムが提供した情報です.
  public class ClaimsAccessor : IClaimsAccessor
    {
        protected IPrincipalAccessor PrincipalAccessor { get; }

        public ClaimsAccessor(IPrincipalAccessor principalAccessor)
        {
            PrincipalAccessor = principalAccessor;
        }

        /// 
        ///     ID
        /// 
        public int? ApiUserId
        {
            get
            {
                var userId = PrincipalAccessor.Principal?.Claims.FirstOrDefault(c => c.Type == SystemClaimTypes.UserId)?.Value;
                if (userId != null)
                {
                    int id = 0;
                    int.TryParse(userId, out id);
                    return id;
                }

                return null;
            }
        }

       /// 
        ///     Id
        /// 
        public string RoleIds
        {
            get
            {
                var roleIds = PrincipalAccessor.Principal?.Claims.FirstOrDefault(c => c.Type == SystemClaimTypes.RoleIds)?.Value;
                if (string.IsNullOrWhiteSpace(roleIds))
                {                    
                    return string.Empty;
                }

                return roleIds;
            }
        }
  }

    public interface IClaimsAccessor
    {
        /// 
        ///     ID
        /// 
        int? ApiUserId { get; }
       
        /// 
        ///     Id
        /// 
        string RoleIds{ get; }
    }

   同样我们在Startup的ConfigureServices方法中把IClaimsAccessor注册进来

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton();
    services.AddSingleton();
    services.AddSingleton<IClaimsAccessor, ClaimsAccessor>();
    ....
}

これにより、アイデンティティフラグを取得する必要があるベースクラスが定義され、これらのベースクラスをどのように使用するかを見てみましょう.

使用


以上のクラスでは、ビジネスで注入方式に依存して必要なオブジェクトを解析する必要があります.具体的に使用する方法を見てみましょう.
    public class TestService : ITestService
    {
        private readonly IClaimsAccessor _claims;
        private readonly IRepository _productRepository;

        public TestService(IClaimsAccessor claims, IRepository productRepository)
        {
            _claims = claims;
            _productRepository = productRepository;
        }


        public Result AddProduct(ProductDto dto)
        {
            _productRepository.Insert(new Product
            {
                Name = dto.Name,
                ...
                CreatorUserId = _claims.ApiUserId
            });
        }
    }

IClaimsAccessorが解析すると、すべてのClaims情報を取得し、これらの情報に基づいてアクセスユーザーのアイデンティティフラグを抽出することができます.これにより、Controllerレベルに限らずユーザーのアイデンティティフラグを取得することができます.これで、システムレベルの識別子は完了しました.プロジェクトの開始項目でASPを利用することを覚えています.NET Coreのコンテナはサービスを登録し、必要な場所で解析して使用できます.

改善


この時、私たちの業務を満たすことができますか?できる!しかし、私たちが少し高い点を要求しているプログラマーにとって、各サービスが上記のように書かれていると、明らかに実際のアプリケーションでは重複したコードを書く必要があり、毎回手動で構造注入を行う必要があるのは煩雑であることがわかります.では、優雅で多重化可能な簡略化と改善をどのように行うかを見てみましょう.
まず、ServiceProviderInstanceクラスを定義します.
  public class ServiceProviderInstance
    {
        public static IServiceProvider Instance { get; set; }
    } 

このクラスの役割はIServiceProviderのインスタンスを保存することですが、なぜそうする必要があるのでしょうか.ここでの1つの設計思想は、私たちが必要とするIClaimsAccessorオブジェクトをどのように順調に解析し、私たちが必要とする情報を得ることができるかということです.ASP.NET Coreのコンテナでは、IServiceCollectionを提供してサービスを登録し、IServiceProviderを提供しています.これにより、ASP.NET Core-依存注入文書で説明した依存注入を解析することができます.この場合、現在のアプリケーションのIServiceProviderインスタンスを取得することを目標としています.したがって,このServiceProviderInstanceクラスの役割は,IServiceProviderのために設計された静的クラスを取得するためである.

アプリケーションのIServiceProviderインスタンスを取得するにはどうすればいいですか?


初期化を適用すると、WebHostBuilderはServiceCollectionを使用して新しいServiceProviderを作成してシステムで使用するため、StartupクラスのConfigureメソッドでは、アプリケーションBuilderのApplicationServicesプロパティを使用してシステムのServiceProviderインスタンスを取得できます.ここでは、ServiceProviderInstanceのInstanceプロパティを使用して現在のIServiceProviderを保存してシステムの後に使用します.
  public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
     {
            ...
            ServiceProviderInstance.Instance = app.ApplicationServices;
    }

システムのIServiceProviderインスタンスを取得した後、残りはこのインスタンスを利用して私たちが前に登録した基礎サービスIClaimsAccessorを解析し、オブジェクト向けの特徴を利用してベースクラスを作成して継承することができます.
    public abstract class ServiceBase 
    {
        /// 
        ///     
        /// 
        protected IClaimsAccessor Claims { get; set; }

        /// 
        /// cotr
        /// 
        protected ServiceBase ()
        {
            Claims = ServiceProviderInstance.Instance.GetRequiredService();
        }
    }

   好的让我们看看改进后在一个实际环境中该如何使用

   public class TestService : ServiceBase,ITestService
    {
        private readonly IRepository _productRepository;

        public TestService(IRepository productRepository)
        {
            _productRepository = productRepository;
        }


        public Result AddProduct(ProductDto dto)
        {
            _productRepository.Insert(new Product
            {
                Name = dto.Name,
                ...
                CreatorUserId = Claims.ApiUserId
            });
        }
    }

すべてのサブクラスにServiceBaseを継承させることで、重複コードを書くことなく、すべてのビジネス・レイヤでユーザーのアイデンティティ情報を直接取得できます.
 

まとめ


  1. ASPを利用する.NET Coreが提供するIHttpContextAccessorは、HttpContextのUser属性を取得する
  2. 一連の基礎クラスをカプセル化し,依存注入を用いてすべてのClaimsを解析した.
  3. 過剰な侵入コードを回避するために、すべてのビジネスクラスに優雅で多重化可能なServiceBaseの作成
 
もしあなたがもっと良い考えやアドバイスがあれば、私に知らせてください.