ASP.NET Core MVCでの承認



パブロイグレシアスがデザインしたロゴ。







この記事では、ASP.NET Core MVCの承認パターンと手法について説明します。 承認(ユーザー権利の検証)のみを考慮し、認証は考慮しないことを強調します。したがって、この記事ではASP.NET ID、認証プロトコルなどを使用しません。 サーバーコードの多くの例、コアMVCソースの深さへの小さな余談、およびテストプロジェクト(記事の最後のリンク)があります。 猫に興味がある人を招待します。







内容:











クレーム



ASP.NET Core MVCの承認と認証の原則は、フレームワークの以前のバージョンと比較して変更されておらず、詳細のみが異なります。 比較的新しい概念の1つは、クレームベースの承認であり、これから旅を始めます。 クレームとは何ですか? これはいくつかのキーと値の文字列で、キーには「FirstName」、「EmailAddress」などを指定できます。 したがって、クレームは、ユーザーのプロパティ、データを含む文字列、または「 ユーザーが何かを持っている」などのステートメントとして解釈することができます。 多くの開発者に馴染みのある1次元の役割ベースのモデルは、多次元のクレームベースのモデルに有機的に含まれています 。 あなたの主張を作成することは禁止されていません。







次の重要な概念はアイデンティティです。 これは、一連のクレームを含む単一のステートメントです。 したがって、IDは不可欠なドキュメント(パスポート、運転免許証など)として解釈できます。この場合、クレームはパスポートの行(生年月日、姓...)です。 コアMVCはSystem.Security.Claims.ClaimsIdentityクラスを使用します







もう1つ上のレベルは、ユーザー自身を示すプリンシパルの概念です。 実生活と同じように、人は複数のドキュメントを同時に持つことができますが、Core MVCでは、プリンシパルにはユーザーに関連付けられた複数のIDを含めることができます。 Core MVCの既知のプロパティHttpContext.Userは、 System.Security.Claims.ClaimsPrincipal型です。 当然、プリンシパルを通じて、各アイデンティティのすべてのクレームを取得できます。 複数のIDのセットを使用して、サイト/サービスのさまざまなセクションへのアクセスを制限できます。









この図は、System.Security.Claims名前空間のクラスのプロパティとメソッドの一部のみを示しています。







なぜこれがすべて必要なのですか? クレームベースの承認の場合、リソースにアクセスするには、ユーザーが目的のクレーム(ユーザープロパティ)を持っている必要があることを明示的に示します。 最も単純なケースでは、特定のクレームの存在自体が確認されますが、はるかに複雑な組み合わせが可能です(ポリシー、要件、権限を使用して設定-これらの概念を以下で詳細に検討します)。 実際の例:乗用車を運転するには、オープンカテゴリB(クレーム)のIDライセンスが必要です。







準備作業



以下、記事全体を通して、Webサイトのさまざまなページへのアクセスを構成します。 提示されたコードを実行するには、Visual Studio 2015タイプ「ASP.NET Core Web Application」で新しいアプリケーションを作成し、Webアプリケーションテンプレートと認証タイプ「認証なし」を設定します。







「個人ユーザーアカウント」認証を使用すると、ASP.NET Identity、EF Core、およびlocaldbを使用してユーザーをデータベースに格納およびロードするコードが生成されます。 この記事では、軽量のEntityFrameworkCore.InMemoryテストソリューションがあります 、これは完全に冗長です。 さらに、原則として、ASP.NET ID 認証ライブラリは必要ありません。 承認のためのプリンシパルの取得は、メモリ内で個別にエミュレートできます。また、Cookie内のプリンシパルのシリアル化は、標準のコアMVCツールを使用して可能です。 テストに必要なのはこれだけです。







インメモリユーザーストレージでASP.NET Identityを使用する場合

ユーザーリポジトリをエミュレートするには、Startup.csを開いて、組み込みのDIコンテナーにスタブサービスを登録するだけです。







public void ConfigureServices(IServiceCollection services) { // Identity services.AddIdentity<IdentityUser, IdentityRole>(); //  services.AddTransient<IUserStore<IdentityUser>, FakeUserStore>(); services.AddTransient<IRoleStore<IdentityRole>, FakeRoleStore>(); }
      
      





ところで、 AddEntityFrameworkStores <TContext>呼び出しが行うのと同じジョブを実行しました







 services.AddIdentity<IdentityUser, IdentityRole>() .AddEntityFrameworkStores<IdentityDbContext>();
      
      





サイトでのユーザー認証から始めましょうGET /Home/Login



でスタブフォームを描画し、空のフォームをサーバーに送信するボタンを追加します。 POST /Home/Login



、プリンシパル、ID、およびクレームを手動で作成します(実際のアプリケーションでは、このデータはデータベースから取得されます)。 HttpContext.Authentication.SignInAsync



呼び出すと、プリンシパルがシリアル化され、暗号化されたCookieに格納されます。CookieはWebサーバーの応答に添付され、クライアント側に保存されます。







ユーザーがサイトにログインするときにプリンシパルスタブを作成する
 [HttpGet] [AllowAnonymous] public IActionResult Login(string returnUrl = null) { ViewData["ReturnUrl"] = returnUrl; return View(); } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<IActionResult> Login(LoginViewModel vm, string returnUrl = null) { //TODO:  ,    ,  ..  .. var claims = new List<Claim> { new Claim(ClaimTypes.Name, "Fake User"), new Claim("age", "25", ClaimValueTypes.Integer) }; var identity = new ClaimsIdentity("MyCookieMiddlewareInstance"); identity.AddClaims(claims); var principal = new ClaimsPrincipal(identity); await HttpContext.Authentication.SignInAsync("MyCookieMiddlewareInstance", principal, new AuthenticationProperties { ExpiresUtc = DateTime.UtcNow.AddMinutes(20) }); _logger.LogInformation(4, "User logged in."); return RedirectToLocal(returnUrl); }
      
      





Startup.Configure(アプリ)メソッドでCookie認証を有効にします。







 app.UseCookieAuthentication(new CookieAuthenticationOptions() { AuthenticationScheme = "MyCookieMiddlewareInstance", CookieName = "MyCookieMiddlewareInstance", LoginPath = new PathString("/Home/Login/"), AccessDeniedPath = new PathString("/Home/AccessDenied/"), AutomaticAuthenticate = true, AutomaticChallenge = true });
      
      





わずかな変更を加えたこのコードは、以降のすべての例の基礎となります。







属性とアクセスポリシーを承認する



[Authorize]



属性はMVCから消えていません。 前と同様に、この属性でコントローラー/アクションをマークすると、承認されたユーザーのみが内部にアクセスできます。 ポリシー(ポリシー)の名前を追加で指定すると、物事はより興味深いものになります-ユーザーの主張の要件:







 [Authorize(Policy = "age-policy")] public IActionResult About() { return View(); }
      
      





ポリシーは、 Startup.ConfigureServices



メソッドで作成されます。







 services.AddAuthorization(options => { options.AddPolicy("age-policy", x => { x.RequireClaim("age"); }); });
      
      





このポリシーは、「年齢」のクレームを持つ許可ユーザーのみが「About」ページにアクセスでき、クレームの値は考慮されないことを確立します。 次のセクションでは、より複雑な例(最後に!)に進みます。そして、今度はそれが内部でどのように機能するかを理解します。







[Authorize]



-マーカー属性。それ自体にはロジックが含まれていません。 どのコントローラー/アクションがAuthorizeFilter-組み込みのコアMVCフィルターの1つに接続する必要があるかをMVCに示すためにのみ必要です。 フィルターの概念は 、以前のバージョンのフレームワークと同じです。フィルターは順次実行され、コントローラー/アクションにアクセスする前後にコードを実行できます。 ミドルウェアとの重要な違い:フィルターはMVC固有のコンテキストにアクセスできます(そして、すべてのミドルウェアの後に自然に実行されます)。 ただし、[MiddlewareFilter]属性を使用してミドルウェア呼び出しをフィルターチェーンに埋め込むことができるため 、フィルターとミドルウェアの境界は非常にあいまいです。







承認とAuthorizeFilterに戻ります。 最も興味深いのは、 OnAuthorizationAsyncメソッドで起こります。







  1. ポリシーのリストから、[Authorize]属性で指定された値に基づいて必要なものを選択します(またはAuthorizationPolicy-名前を話す1つの要件のみを含むデフォルトポリシー-DenyAnonymousAuthorizationRequirementを取得します)。
  2. 一連のユーザーIDおよびクレーム(たとえば、Cookie要求から以前に受信したもの)がポリシー要件に準拠しているかどうかを確認します。


提供されたソースコードへのリンクが、Core MVCのフィルターの内部構造のアイデアを提供してくれることを願っています。







アクセスポリシー設定



上記の流れるようなインターフェイスを使用してアクセスポリシーを作成しても、実際のアプリケーションに必要な柔軟性は提供されません。 もちろん、 RequireClaim("x", params values)



呼び出しを介して有効なクレーム値を明示的に指定できますRequireClaim("x").RequireClaim("y")



呼び出すことで論理条件と複数条件を組み合わせることができます。 最後に、異なるポリシーをコントローラーとアクションにアタッチできますが、論理Iを介して条件の同じ組み合わせになります。明らかに、ポリシーを作成するためのより柔軟なメカニズムが必要であり、それは要件とハンドラーです。







 services.AddAuthorization(options => { options.AddPolicy("age-policy", policy => policy.Requirements.Add(new AgeRequirement(42), new FooRequirement())); });
      
      





要件は、対応するハンドラーにパラメーターを渡すためのDTOにすぎず、ハンドラーはHttpContext.Userにアクセスし、プリンシパルとそれに含まれるID /クレームのチェックを自由に行うことができます。 さらに、ハンドラーは組み込みのCore MVC DIコンテナーを介して外部の依存関係を受け取ることができます。







要件とハンドラーの例
 public class MinAgeRequirement : IAuthorizationRequirement { public MinAgeRequirement(int age) { Age = age; } public int Age { get; private set; } } public class MinAgeHandler : AuthorizationHandler<MinAgeRequirement> { public MinAgeHandler(IFooService fooService) { // fooService    DI } protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinAgeRequirement requirement) { bool hasClaim = context.User.HasClaim(c => c.Type == "age"); bool hasIdentity = context.User.Identities.Any(i => i.AuthenticationType == "MultiPass"); string claimValue = context.User.FindFirst(c => c.Type == "age").Value; if (int.Parse(claimValue) >= requirement.Age) { context.Succeed(requirement); } else { context.Fail(); } return Task.CompletedTask; } }
      
      





Startup.ConfigureServices()にハンドラー自体を登録すると、使用する準備が整います:







 services.AddSingleton<IAuthorizationHandler, MinAgeHandler>();
      
      





ハンドラーは、ANDとORの両方で組み合わせることができます。 そのため、 AuthorizationHandler<FooRequirement>



いくつかの後継を登録すると、それらすべてが呼び出されます。 この場合、 context.Succeed()



呼び出しは不要であり、 context.Fail()



呼び出しは、他のハンドラーの結果に関係なく、一般的な許可拒否につながります。 合計すると、考慮されるアクセスメカニズムを次のように互いに組み合わせることができます。









リソースベースの承認



前述のように、ポリシーベースの承認は、フィルターパイプラインでCore MVCによって実行されます。 保護されたアクションを呼び出す前。 この場合の承認の成功は、ユーザーのみに依存します-ユーザーが必要な主張を持っているかどうか。 しかし、保護されたリソースとそのプロパティも考慮する必要がある場合、外部ソースからどのようなデータを取得する必要がありますか? 実例: GET /Orders/{id}



の形式のアクションを保護します。これは、データベースから注文のある行をidで読み取ります。 ユーザーに特定の注文に対する権限を与えてください。データベースからこの注文を受け取った後にのみ決定できます。 これにより、ユーザーコードが不適切に制御される前に実行されるMVCフィルターに基づいて、前述のアスペクト指向スクリプトが自動的にレンダリングされます。 幸いなことに、Core MVCを手動で認証する方法があります。







これを行うには、コントローラー IAuthorizationService



実装が必要IAuthorizationService



。 通常どおり、コンストラクターで依存関係を実装することで取得します。







 public class ResourceController : Controller { IAuthorizationService _authorizationService; public ResourceController(IAuthorizationService authorizationService) { _authorizationService = authorizationService; } }
      
      





次に、新しいポリシーとハンドラーを作成します。







 options.AddPolicy("resource-allow-policy", x => { x.AddRequirements(new ResourceBasedRequirement()); }); public class ResourceHandler : AuthorizationHandler<ResourceBasedRequirement, Order> { protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, ResourceBasedRequirement requirement, Order order) { // TODO: ,         if (true) context.Succeed(requirement); return Task.CompletedTask; } }
      
      





最後に、アクション内のユーザーとリソースが目的のポリシーに準拠しているかどうかを確認します( [Authorize]



属性は不要になりました)。







 public async Task<IActionResult> Allow(int id) { Order order = new Order(); //    if (await _authorizationService.AuthorizeAsync(User, order, "my-resource-policy")) { return View(); } else { // 401  403      return new ChallengeResult(); } }
      
      





IAuthorizationService.AuthorizeAsync



メソッドには、ポリシー名の代わりに要件からリストをIAuthorizationService.AuthorizeAsync



するオーバーロードがあります。







 Task<bool> AuthorizeAsync( ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements);
      
      





これにより、アクセス権をさらに柔軟に構成できます。 デモンストレーションのために、事前定義されたOperationAuthorizationRequirement



を使用します(はい、この例はdocs.microsoft.comから直接記事に移行しました)。







 public static class Operations { public static OperationAuthorizationRequirement Create = new OperationAuthorizationRequirement { Name = "Create" }; public static OperationAuthorizationRequirement Read = new OperationAuthorizationRequirement { Name = "Read" }; public static OperationAuthorizationRequirement Update = new OperationAuthorizationRequirement { Name = "Update" }; public static OperationAuthorizationRequirement Delete = new OperationAuthorizationRequirement { Name = "Delete" }; }
      
      





これにより、次のことができます。







 _authorizationService.AuthorizeAsync( User, resource, Operations.Create, Operations.Read, Operations.Update);
      
      





対応するハンドラーのHandleRequirementAsync(context, requirement, resource)



メソッドでは、 requirement.Name



で指定された操作に従って権限を確認するだけでよく、ユーザーが認証に失敗した場合はcontext.Fail()



を呼び出すことを忘れないでください:







 protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, Order order) { string operationName = requirement.Name; // ,         if(true) context.Succeed(requirement); return Task.CompletedTask; }
      
      





ハンドラーは、 AuthorizeAsync



渡した要件と同じ回数だけ呼び出され、各要件を個別にチェックします。 ハンドラーへの1回の呼び出しで操作に対するすべての権限を1回だけ確認するには、次のように、要件内の操作のリストを転送します。







  new OperationListRequirement(new[] { Ops.Read, Ops.Update })
      
      





これで、リソースベースの認証機能の概要が完了しました。次は、ハンドラーをテストでカバーします。







 [Test] public async Task MinAgeHandler_WhenCalledWithValidUser_Succeed() { var requirement = new MinAgeRequirement(24); var user = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim> { new Claim("age", "25") })); var context = new AuthorizationHandlerContext(new [] { requirement }, user, resource: null); var handler = new MinAgeHandler(); await handler.HandleAsync(context); Assert.True(context.HasSucceeded); }
      
      





Razorマークアップ認証



マークアップで直接実行されるユーザー権利チェックは、ユーザーがアクセスできないUI要素を非表示にするのに役立ちます。 もちろん、ビューでは、必要なすべてのフラグをViewModelを介して渡すことができます(他のすべてが等しい場合、このオプションを使用します)。または、HttpContext.Userを介してプリンシパルに直接アクセスできます。







 <h4>: @User.GetClaimValue("age")</h4>
      
      





関心がある場合、ビューはRazorPageクラスから継承され、 @ @Context



プロパティを使用してマークアップからHttpContextに直接アクセスできます。







一方、前のセクションのアプローチを使用することもできますIAuthorizationService



を介してIAuthorizationService



の実装をIAuthorizationService



し(はい、表示されます)、目的のポリシーの要件に準拠しているかどうかを確認します。







 @inject IAuthorizationService AuthorizationService @if (await AuthorizationService.AuthorizeAsync(User, "my-policy"))
      
      





テストプロジェクトでSignInManager.IsSignedIn(User)



呼び出しを使用しないでください(Webアプリケーションテンプレートで認証タイプ「個別ユーザーアカウント」を使用)。 まず、このクラスが属するMicrosoft.AspNetCore.Identity



認証ライブラリを使用しないためです。 内部のメソッドは、ユーザーがライブラリコードで配線された名前のIDを持っているかどうかを確認する以外は何もしません。







許可ベースの承認。 独自の認証フィルター



次のような、ユーザー認証中に要求されたすべての操作(主にCRUDの中から)の宣言的なリスト。







 var requirement = OperationListRequirement(new[] { Ops.FooAction, Ops.BarAction }); _authorizationService.AuthorizeAsync(User, resource, requirement);
      
      





...プロジェクトに個人の許可(許可)のシステムがある場合、それは理にかなっています:ビジネスロジックの多数の高レベルの操作の特定のセットがあり、特定のリソースを使用して特定の操作に対する権限を手動で付与されたユーザー(またはユーザーのグループ)があります。 たとえば、Vasyaには「デッキをスクラブする」、「コックピットで寝る」権利があり、Petyaは「舵を切る」ことができます。 このパターンが良いか悪いかは、別の記事のトピックです(個人的には、私はそれについて熱心ではありません)。 このアプローチの明らかな問題:操作のリストは、最大のシステムであっても数百に簡単に増加します。







承認のために保護されたリソースの特定のインスタンスを考慮する必要がない場合、状況は単純化され、システムは、保護されたコード内の何百ものAuthorizeAsync



呼び出しの代わりに、チェックされた操作のリストを持つ属性をメソッド全体に単純に添付するのに十分な粒度です。 ただし、ポリシーベースの承認[Authorize(Policy = "foo-policy")]



を使用すると、アプリケーション内のポリシーの数が組み合わせて爆発的に増加します。 古き良きロールベースの認証を使用してみませんか? 以下のコードサンプルでは、​​ユーザーはFooControllerにアクセスするために、指定されたすべてのロールのメンバーである必要があります。







 [Authorize(Roles = "PowerUser")] [Authorize(Roles = "ControlPanelUser")] public class FooController : Controller { }
      
      





同様のソリューションでは、多数の許可とそれらの可能な組み合わせを備えたシステムに十分な詳細と柔軟性が提供されない場合があります。 追加の問題は、役割ベースの許可と許可ベースの許可の両方が必要なときに始まります。 そして意味的には、ロールと操作は2つの異なるものです;私はそれらの承認を別々に処理したいと思います。 解決済み: [Authorize]



属性のバージョンを記述してください! 最終結果を示します。







 [AuthorizePermission(Permission.Foo, Permission.Bar)] public IActionResult Edit() { return View(); }
      
      





ユーザーを検証するための操作、要件、およびハンドラーの列挙を作成することから始めましょう。







非表示のテキスト
 public enum Permission { Foo, Bar } public class PermissionRequirement : IAuthorizationRequirement { public Permission[] Permissions { get; set; } public PermissionRequirement(Permission[] permissions) { Permissions = permissions; } } public class PermissionHandler : AuthorizationHandler<PermissionRequirement> { protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, PermissionRequirement requirement) { //TODO:   ,         if (requirement.Permissions.Any()) { context.Succeed(requirement); } return Task.CompletedTask; } }
      
      





先ほど、 [Authorize]



属性は純粋にマーカーであり、 AuthorizeFilter



を適用するために必要であると述べました。 既存のアーキテクチャと戦うことはありません。そのため、類推により、独自の認証フィルターを作成します。 各アクションの許可のリストは異なるため、次のようにします。







  1. 呼び出しごとにフィルターインスタンスを作成する必要があります。
  2. 組み込みのDIコンテナを介して直接インスタンス化することはできません。


幸い、Core MVCでは、これらの問題は[TypeFilter]属性を使用して簡単に解決できます。







 [TypeFilter(typeof(PermissionFilterV1), new object[] { new[] { Permission.Foo, Permission.Bar } })] public IActionResult Index() { return View(); }
      
      





PermissionFilterV1
 public class PermissionFilterV1 : Attribute, IAsyncAuthorizationFilter { private readonly IAuthorizationService _authService; private readonly Permission[] _permissions; public PermissionFilterV1(IAuthorizationService authService, Permission[] permissions) { _authService = authService; _permissions = permissions; } public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { bool ok = await _authService.AuthorizeAsync( context.HttpContext.User, null, new PermissionRequirement(_permissions)); if (!ok) context.Result = new ChallengeResult(); } }
      
      





完全に機能するが、見苦しいソリューションが得られました。 呼び出しコードからフィルター実装の詳細を隠すために、 [AuthorizePermission]



属性は役に立ちます:







 public class AuthorizePermissionAttribute : TypeFilterAttribute { public AuthorizePermissionAttribute(params Permission[] permissions) : base(typeof(PermissionFilterV2)) { Arguments = new[] { new PermissionRequirement(permissions) }; Order = Int32.MaxValue; } }
      
      





結果:







 [AuthorizePermission(Permission.Foo, Permission.Bar)] [Authorize(Policy = "foo-policy")] public IActionResult Index() { return View(); }
      
      





注:承認フィルターは独立して機能するため、相互に組み合わせることができます。 汎用キュー内のフィルターの実行順序は、 AuthorizePermissionAttribute.Order



プロパティを使用して調整できます。







トピックに関する追加の資料(リストに含めるためのリンクも歓迎します):









これで、ASP.NET Core MVCの承認の概要が完了しました。 ほとんどの資料はWebAPIに適用されます。 この記事の例を再現したい場合は、 デモプロジェクトを使用することをお勧めします。 次の記事(希望)では、専用の認証サーバーでWebサイトとパブリックAPIを保護します。








All Articles