【问题标题】:ASP.NET Core Identity 2: User.IsInRole always returns falseASP.NET Core Identity 2:User.IsInRole 总是返回 false
【发布时间】:2019-04-15 17:23:57
【问题描述】:

问题:我调用RoleManager.CreateAsync()RoleManager.AddClaimAsync() 来创建角色和相关的角色声明。然后我调用UserManager.AddToRoleAsync() 将用户添加到这些角色。但是当用户登录时,角色和相关声明都不会出现在ClaimsPrincipal(即控制器的User 对象)中。这样做的结果是User.IsInRole()总是返回false,User.Claims返回的Claims集合不包含角色声明,[Authorize(policy: xxx)]注解不起作用。

我还应该补充一点,一种解决方案是从使用新的services.AddDefaultIdentity()(由模板化代码提供)恢复为调用services.AddIdentity().AddSomething().AddSomethingElse()。我不想去那里,因为我在网上看到了太多相互矛盾的故事,关于我需要做什么来为各种用例配置AddIdentityAddDefaultIdentity 似乎可以正确完成大多数事情,而无需添加大量流畅的配置。

顺便说一句,我问这个问题的目的是为了回答它......除非有人给我一个比我准备发布的更好的答案。我也在问这个问题,因为经过几周的搜索,我还没有找到在 ASP.NET Core Identity 2 中创建和使用角色和声明的良好端到端示例。希望这个问题中的代码示例可以帮助其他偶然发现它的人......

设置: 我创建了一个新的 ASP.NET Core Web 应用程序,选择 Web 应用程序(模型-视图-控制器),并将身份验证更改为个人用户帐户。在生成的项目中,我执行以下操作:

  • 在包管理器控制台中,更新数据库以匹配脚手架迁移:

    更新数据库

  • 添加一个扩展IdentityUserApplicationUser 类。这包括添加类,向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 实体。

    添加迁移 m_001
    更新数据库

  • Startup.cs中添加以下代码行启用RoleManager

    services.AddDefaultIdentity<ApplicationUser>()
        .AddRoles<IdentityRole>() // <-- Add this line
        .AddEntityFrameworkStores<ApplicationDbContext>();
    
  • 添加一些代码来播种角色、声明和用户。这个示例代码的基本概念是我有两个声明:can_report 允许持有者创建报告,can_test 允许持有者运行测试。我有两个角色,AdminTesterTester 角色可以运行测试,但不能创建报告。 Admin 角色可以两者兼得。因此,我将声明添加到角色中,并创建一个 Admin 测试用户和一个 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";
    }
    

    接下来,我播种我的角色、声明和用户。我把这段代码放在主登陆页面控制器中只是为了方便;它确实属于“启动”Configure 方法,但那是额外的六行代码......

    public class HomeController : Controller
    {
        const string Password = "QwertyA1?";
    
        const string AdminEmail = "admin@example.com";
        const string TesterEmail = "tester@example.com";
    
        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 });
        }
    }
    

    我在“启动”ConfigureServices 例程中注册了我的身份验证策略,就在调用 services.AddMvc 之后

        // 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 对象中不存在我的角色和声明。但我可以查看数据库并查看所有角色和声明。

【问题讨论】:

  • 用户如何认证?您的任何代码都没有表明应用程序如何知道当前用户是谁。
  • 用户登录。这是我从模板中得到的部分代码。
  • 是否有包含用户 ID 的声明?
  • 当用户登录时,User.Claims 列表包含 nameidentifier(带标点符号 [破折号] 的 GUID)、name(用户名)和 SecurityStamp(不带标点符号的 GUID )。但是,因为我已将角色与用户相关联,并且该角色具有关联的声明,所以我还应该在集合中看到这些声明。 (您必须等待答案才能找出我的自定义声明不存在的原因。;)
  • 您是否尝试添加自定义声明

标签: asp.net-core asp.net-core-mvc asp.net-identity


【解决方案1】:

啊,从 ASP.NET Core 2.0 版到 2.1 版有一些变化。 AddDefaultIdentity 就是那个。

我不知道从你的代码哪里开始,所以,我将提供一个示例来创建和获取用户角色。

让我们先创建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 - Best Practices in Asynchronous Programming

ConfigureServices 方法中,这些配置不会工作(我们不能正确使用User.IsInRole):

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

我不知道为什么,但 AddRolesAddRoleManager 不支持检查用户的角色 (User.IsInRole)。

在这种情况下,我们需要这样注册服务:

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

通过这种方式,我们在数据库中创建了 3 个用户角色:

注册新用户时,我们只需要调用:

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:要实现这个目标,有很多事情需要实施。所以也许我在这个例子中遗漏了一些东西。

【讨论】:

  • 嘿谭,很好的答案。我想我应该补充一点,我想使用新奇的services.AddDefaultIdentity 而不是services.AddIdentity()…,因为我在网上看到太多相互矛盾的故事,关于需要遵循哪些流畅的方法AddIdentity 才能使其适用于各种用例.
  • 另外,我认为您对 InitializeAsync 的调用在 ASP.NET 中很容易出现死锁问题。看看this SO answer。这个从同步启动方法调用异步方法的问题是我的示例代码将所有种子逻辑放入控制器代码的原因(因此我不必包含从启动代码中正确执行此操作所需的额外代码)。
【解决方案2】:

因此,回顾一下,问题询问为什么 ASP.NET Core Web 应用程序模板提供的代码不会在用户登录时将角色或角色声明加载到 cookie 中。

经过大量谷歌搜索和试验,似乎必须对模板代码进行两项修改才能使角色和角色声明正常工作:

首先,您必须在 Startup.cs 中添加以下代码行以启用 RoleManager。 (OP中提到了这个魔法。)

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

但是等等,还有更多!根据this discussion on GitHub,让角色和声明出现在cookie中涉及要么恢复到service.AddIdentity初始化代码,或者坚持使用service.AddDefaultIdentity并将这行代码添加到@987654325 @:

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

如果您阅读了上面引用的讨论,您会发现角色和角色声明显然已被弃用,或者至少不被热切支持。就个人而言,我发现将声明分配给角色,将角色分配给用户,然后根据声明(根据角色授予用户)做出授权决策非常有用。这为我提供了一种简单的声明方式,例如,允许多个角色(即包含用于启用该功能的声明的所有角色)访问一个功能。

但您确实需要注意身份验证 cookie 中携带的角色和声明数据的数量。更多数据意味着每个请求发送到服务器的字节数更多,我不知道当您遇到某种 cookie 大小限制时会发生什么。

【讨论】:

    【解决方案3】:

    您也可以尝试像这样修复身份验证

    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;
    });
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-05-12
      • 1970-01-01
      • 1970-01-01
      • 2020-08-02
      • 1970-01-01
      • 1970-01-01
      • 2015-08-02
      • 1970-01-01
      相关资源
      最近更新 更多