web-dev-qa-db-ja.com

ASP.NETコアID2:User.IsInRoleは常にfalseを返します

質問:RoleManager.CreateAsync()RoleManager.AddClaimAsync()を呼び出して、ロールと関連するロールクレームを作成します。次に、UserManager.AddToRoleAsync()を呼び出して、これらのロールにユーザーを追加します。ただし、ユーザーがログインすると、ロールも関連するクレームもClaimsPrincipal(つまり、コントローラーのUserオブジェクト)に表示されません。その結果、User.IsInRole()は常にfalseを返し、User.Claimsによって返されるクレームのコレクションにはロールクレームが含まれず、[Authorize(policy: xxx)]アノテーションは機能しません。

また、1つの解決策は、新しいservices.AddDefaultIdentity()(テンプレート化されたコードによって提供される)の使用からservices.AddIdentity().AddSomething().AddSomethingElse()の呼び出しに戻すことです。さまざまなユースケースに合わせてAddIdentityを構成するために何をする必要があるかについて、オンラインで相反する話をあまりにも多く見たので、そこには行きたくありません。 AddDefaultIdentityは、多くの流暢な構成を追加しなくても、ほとんどのことを正しく実行しているようです。

ところで、私はそれに答えるつもりでこの質問をしています...誰かが私が投稿する準備ができているものよりも良い答えを私に与えない限り。 数週間の検索の後、ASP.NET Core Identity 2でロールとクレームを作成および使用するための優れたエンドツーエンドの例をまだ見つけていませんという理由で、この質問もしています。うまくいけば、この質問のコード例は、それにつまずいた他の誰かを助けるかもしれません...

セットアップ:新しいASP.NETコアWebアプリケーションを作成し、Webアプリケーション(Model-View-Controller)を選択して、認証を個別のユーザーアカウントに変更します。結果のプロジェクトでは、次のことを行います。

  • パッケージマネージャーコンソールで、スキャフォールド移行に一致するようにデータベースを更新します。

    データベースを更新する

  • ApplicationUserを拡張するIdentityUserクラスを追加します。これには、クラスの追加、ApplicationDbContextへのコード行の追加、およびプロジェクト内のすべての場所での<IdentityUser>のすべてのインスタンスの<ApplicationUser>への置き換えが含まれます。

    新しいApplicationUserクラス:

    public class ApplicationUser : IdentityUser
    {
        public string FullName { get; set; }
    }
    

    更新されたApplicationDbContextクラス:

    public class ApplicationDbContext : IdentityDbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        { }
    
        // Add this line of code
        public DbSet<ApplicationUser> ApplicationUsers { get; set; }
    }
    
  • パッケージマネージャーコンソールで、新しい移行を作成し、データベースを更新してApplicationUsersエンティティを組み込みます。

    add-migration m_001
    データベースを更新する

  • Startup.csに次のコード行を追加して、RoleManagerを有効にします

    services.AddDefaultIdentity<ApplicationUser>()
        .AddRoles<IdentityRole>() // <-- Add this line
        .AddEntityFrameworkStores<ApplicationDbContext>();
    
  • シードの役割、クレーム、およびユーザーにコードを追加します。このサンプルコードの基本的な概念は、2つの主張があるということです。can_reportは所有者がレポートを作成できるようにし、can_testは所有者がテストを実行できるようにします。 AdminTesterの2つの役割があります。 Testerロールはテストを実行できますが、レポートを作成することはできません。 Adminロールは両方を実行できます。そこで、ロールにクレームを追加し、1人のAdminテストユーザーと1人のTesterテストユーザーを作成します。

    まず、この例の他の場所で使用されている定数を含めることが人生の唯一の目的であるクラスを追加します。

    // Contains constant strings used throughout this example
    public class MyApp
    {
        // Claims
        public const string CanTestClaim = "can_test";
        public const string CanReportClaim = "can_report";
    
        // Role names
        public const string AdminRole = "admin";
        public const string TesterRole = "tester";
    
        // Authorization policy names
        public const string CanTestPolicy = "can_test";
        public const string CanReportPolicy = "can_report";
    }
    

    次に、自分の役割、クレーム、およびユーザーをシードします。便宜上、このコードをメインのランディングページコントローラーに配置しました。それは実際には「startup」Configureメソッドに属しますが、それは余分な半ダース行のコードです...

    public class HomeController : Controller
    {
        const string Password = "QwertyA1?";
    
        const string AdminEmail = "[email protected]";
        const string TesterEmail = "[email protected]";
    
        private readonly RoleManager<IdentityRole> _roleManager;
        private readonly UserManager<ApplicationUser> _userManager;
    
        // Constructor (DI claptrap)
        public HomeController(RoleManager<IdentityRole> roleManager, UserManager<ApplicationUser> userManager)
        {
            _roleManager = roleManager;
            _userManager = userManager;
        }
    
        public async Task<IActionResult> Index()
        {
            // Initialize roles
            if (!await _roleManager.RoleExistsAsync(MyApp.AdminRole)) {
                var role = new IdentityRole(MyApp.AdminRole);
                await _roleManager.CreateAsync(role);
                await _roleManager.AddClaimAsync(role, new Claim(MyApp.CanTestClaim, ""));
                await _roleManager.AddClaimAsync(role, new Claim(MyApp.CanReportClaim, ""));
            }
    
            if (!await _roleManager.RoleExistsAsync(MyApp.TesterRole)) {
                var role = new IdentityRole(MyApp.TesterRole);
                await _roleManager.CreateAsync(role);
                await _roleManager.AddClaimAsync(role, new Claim(MyApp.CanTestClaim, ""));
            }
    
            // Initialize users
            var qry = _userManager.Users;
            IdentityResult result;
    
            if (await qry.Where(x => x.UserName == AdminEmail).FirstOrDefaultAsync() == null) {
                var user = new ApplicationUser {
                    UserName = AdminEmail,
                    Email = AdminEmail,
                    FullName = "Administrator"
                };
    
                result = await _userManager.CreateAsync(user, Password);
                if (!result.Succeeded) throw new InvalidOperationException(string.Join(" | ", result.Errors.Select(x => x.Description)));
    
                result = await _userManager.AddToRoleAsync(user, MyApp.AdminRole);
                if (!result.Succeeded) throw new InvalidOperationException(string.Join(" | ", result.Errors.Select(x => x.Description)));
            }
    
            if (await qry.Where(x => x.UserName == TesterEmail).FirstOrDefaultAsync() == null) {
                var user = new ApplicationUser {
                    UserName = TesterEmail,
                    Email = TesterEmail,
                    FullName = "Tester"
                };
    
                result = await _userManager.CreateAsync(user, Password);
                if (!result.Succeeded) throw new InvalidOperationException(string.Join(" | ", result.Errors.Select(x => x.Description)));
    
                result = await _userManager.AddToRoleAsync(user, MyApp.TesterRole);
                if (!result.Succeeded) throw new InvalidOperationException(string.Join(" | ", result.Errors.Select(x => x.Description)));
            }
    
            // Roles and Claims are in a cookie. Don't expect to see them in
            // the same request that creates them (i.e., the request that
            // executes the above code to create them). You need to refresh
            // the page to create a round-trip that includes the cookie.
            var admin = User.IsInRole(MyApp.AdminRole);
            var claims = User.Claims.ToList();
    
            return View();
        }
    
        [Authorize(policy: MyApp.CanTestPolicy)]
        public IActionResult Test()
        {
            return View();
        }
    
        [Authorize(policy: MyApp.CanReportPolicy)]
        public IActionResult Report()
        {
            return View();
        }
    
        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }
    

    services.AddMvcを呼び出した直後に、認証ポリシーを「スタートアップ」ConfigureServicesルーチンに登録します。

        // Register authorization policies
        services.AddAuthorization(options => {
            options.AddPolicy(MyApp.CanTestPolicy,   policy => policy.RequireClaim(MyApp.CanTestClaim));
            options.AddPolicy(MyApp.CanReportPolicy, policy => policy.RequireClaim(MyApp.CanReportClaim));
        });
    

ふぅ。さて、(上記のプロジェクトに追加した該当するコードをすべてメモしたと仮定して)アプリを実行すると、「組み込み」テストユーザーのどちらも/home/Testまたは/home/Reportにアクセスできないことに気付きました。ページ。さらに、Indexメソッドにブレークポイントを設定すると、ロールとクレームがUserオブジェクトに存在しないことがわかります。しかし、私はデータベースを見て、すべての役割と主張がそこにあるのを見ることができます。

したがって、要約すると、質問は、ASP.NET Core Webアプリケーションテンプレートによって提供されるコードが、ユーザーがログインしたときにロールまたはロールクレームをCookieにロードしない理由を尋ねます。

多くのグーグルと実験の後、ロールとロールクレームを機能させるために、テンプレート化されたコードに2つの変更を加える必要があるようです。

まず、RoleManagerを有効にするには、Startup.csに次のコード行を追加する必要があります。 (このちょっとした魔法はOPで言及されました。)

_services.AddDefaultIdentity<ApplicationUser>()
   .AddRoles<IdentityRole>() // <-- Add this line
    .AddEntityFrameworkStores<ApplicationDbContext>();
_

しかし、待ってください、もっとあります! GitHubに関するこのディスカッション によると、役割とクレームをCookieに表示するには、どちらかを_service.AddIdentity_初期化コード、または_service.AddDefaultIdentity_を使用して、このコード行をConfigureServicesに追加します。

_// Add Role claims to the User object
// See: https://github.com/aspnet/Identity/issues/1813#issuecomment-420066501
services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>>();
_

上記の説明を読むと、ロールとロールクレームは明らかに非推奨であるか、少なくとも熱心にサポートされていないことがわかります。個人的には、ロールにクレームを割り当て、ユーザーにロールを割り当て、クレーム(ロールに基づいてユーザーに付与される)に基づいて承認を決定することが非常に便利だと思います。これにより、たとえば、1つの関数に複数のロール(つまり、その関数を有効にするために使用されるクレームを含むすべてのロール)からアクセスできるようにする、簡単で宣言的な方法が得られます。

ただし、認証Cookieで運ばれる役割とクレームデータの量に注意を払う必要があります。より多くのデータは、各リクエストでサーバーに送信されるより多くのバイトを意味します。Cookieサイズのある種の制限にぶつかったときに何が起こるか、私にはわかりません。

ああ、ASP.NETCoreバージョン2.0から2.1への変更がいくつかあります。 AddDefaultIdentityが1つです。

コードからどこから始めればよいかわからないので、ユーザーロールを作成して取得する例を示します。

最初にUserRolesを作成しましょう:

_public enum UserRoles
{
    [Display(Name = "Quản trị viên")]
    Administrator = 0,

    [Display(Name = "Kiểm soát viên")]
    Moderator = 1,

    [Display(Name = "Thành viên")]
    Member = 2
}
_

:属性Displayを削除できます。

次に、RolesExtensionsクラスを作成します。

_public static class RolesExtensions
{
    public static async Task InitializeAsync(RoleManager<IdentityRole> roleManager)
    {
        foreach (string roleName in Enum.GetNames(typeof(UserRoles)))
        {
            if (!await roleManager.RoleExistsAsync(roleName))
            {
                await roleManager.CreateAsync(new IdentityRole(roleName));
            }
        }
    }
}
_

次に、_Startup.cs_クラスで、次のように実行します。

_    public void Configure(
        IApplicationBuilder app, 
        IHostingEnvironment env, 
        RoleManager<IdentityRole> roleManager)
    {
        // other settings...

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });

        var task = RolesExtensions.InitializeAsync(roleManager);
        task.Wait();
    }
_

Configureには戻り型voidが必要なので、ユーザーロールを初期化するタスクを作成する必要があり、Wait 方法。

返されたタイプを次のように変更しないでください

_public async void Configure(...)
{
    await RolesExtensions.InitializeAsync(roleManager);
}
_

出典: Async/Await-非同期プログラミングのベストプラクティス

ConfigureServicesメソッドでは、これらの構成は機能しません(_User.IsInRole_を正しく使用できません):

_services.AddDefaultIdentity<ApplicationUser>()
    //.AddRoles<IdentityRole>()
    //.AddRoleManager<RoleManager<IdentityRole>>()
    .AddEntityFrameworkStores<ApplicationDbContext>();
_

理由はわかりませんが、AddRolesAddRoleManagerはユーザーの役割の確認をサポートしていません(_User.IsInRole_)。

この場合、次のようなサービスを登録する必要があります。

_services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();
_

この方法を使用して、データベースに3つのユーザーロールを作成します。

1

新しいユーザーを登録するときは、次の電話をかけるだけです。

_await _userManager.AddToRoleAsync(user, nameof(UserRoles.Administrator));
_

最後に、[Authorize(Roles = "Administrator")]と次を使用できます。

_if (User.IsInRole("Administrator"))
{
    // authorized
}

// or
if (User.IsInRole(nameof(UserRoles.Administrator)))
{
    // authorized
}

// but
if (User.IsInRole("ADMINISTRATOR"))
{
    // authorized
}
_

P/S:この目標を達成するために実装する必要のあることがたくさんあります。したがって、この例では何かを見逃した可能性があります。

1
Foo

また、このように認証を修正することもできます

services.AddDefaultIdentity<ApplicationUser>()
    .AddRoles<IdentityRole>()
    .AddRoleManager<RoleManager<IdentityRole>>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
    options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
    options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
});
0
baur