ASP.NET Core 3.1 中基于角色的授权与身份验证和外部登录

3
我对.NET Core很陌生,正试图在.NET Core 3.1项目中设置基于角色的授权。我查看了在线上所有关于此内容的教程和讨论帖,但似乎只有教程才能简单地实现我的需求。根据我找到的教程,所要做的就是在数据库中为用户分配一个角色,然后在控制器操作之前使用 [Authorize(Roles="roleName")]。然而,当我这样做时,即使我已经将指定的角色分配给用户,也会收到403错误。当我使用userManager.GetRolesAsync(user)时,我发现该用户拥有该角色。当我使用[Authorize]选项进行请求时,如果用户已登录,则它按预期工作。
我在当前用户的ClaimsPrincipal.Identity中检查了调试模式,并发现RoleClaimType = "role"。我查看了当前用户的声明并发现它没有使用“role”类型的声明。这就是[Authorize(Roles="...")]的工作原理吗?它是查看声明吗?如果是这样,请问如何为用户的角色添加声明?这个应用程序中用户唯一的登录方式是通过Google账户。那么,如果他们由Google登录管理,我该怎么添加声明呢?
下面是我的Startup.cs代码:
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection")));

    services.AddDefaultIdentity<ApplicationUser>()
        .AddRoles<ApplicationRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddIdentityServer()
        .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

    services.AddAuthentication()
        .AddGoogle(options =>
        {
            IConfigurationSection googleAuthNSection =
            Configuration.GetSection("Authentication:Google");

            options.ClientId = googleAuthNSection["ClientId"];
            options.ClientSecret = googleAuthNSection["ClientSecret"];
        })
        .AddIdentityServerJwt();

    services.AddControllersWithViews();
    services.AddRazorPages();
    services.AddSpaStaticFiles(configuration =>
    {
        configuration.RootPath = "ClientApp/dist";
    });
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    if (!env.IsDevelopment())
    {
        app.UseSpaStaticFiles();
    }
    app.UseRouting();
    app.UseIdentityServer();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller}/{action=Index}/{id?}");
        endpoints.MapRazorPages();
    });

    app.UseSpa(spa =>
    {
        spa.Options.SourcePath = "ClientApp";

            if (env.IsDevelopment())
            {
                spa.UseAngularCliServer(npmScript: "start");
            }
    });
}

这是控制器行为(Action)的一个示例:

[Authorize(Roles = "Admin")]
[HttpGet("userinformations")]
public async Task<UserInformations> GetCurrentUserInformations()
{
    string strUserId = this.User.FindFirstValue(ClaimTypes.NameIdentifier);

    ApplicationUser user = await userManager.FindByIdAsync(strUserId);

    string[] roles = (await userManager.GetRolesAsync(user)).ToArray();

    UserInformations userInfo = new UserInformations()
    {
        UserName = user.UserName,
        FirstName = user.FirstName,
        LastName = user.LastName,
        Email = user.Email,
        Organization = user.idDefaultOrganisation.HasValue ? user.DefaultOrganization.OrganizationName : "",
        Claims = this.User.Claims.Select(c => $"{c.Type} : {c.Value}").ToArray(),
        Roles = roles
    };

    return userInfo;
}

当我发出不带[Authorize(Roles = "Admin")]的请求时,我可以看到当前用户具有管理员角色,但当我添加此项时,我会收到403错误。

我做错了什么?我觉得我可能漏掉了一行或者类似的东西,因为在我找到的教程中,这一切似乎都很简单。

2个回答

3

你的假设是正确的,当你指定 [Authorize(Roles = "<role>")] 属性时,ASP 会在后台创建一个 RolesAuthorizationRequirement

然后授权处理程序将调用 this.HttpContext.User.IsInRole(<role>) 来评估策略。

在你的情况下,调用是 this.HttpContext.User.IsInRole("Admin")

User.IsInRole 方法将查找名为 "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" 的声明,并将其值与 "Admin" 进行比较。

ASP 授权管道没有钩入你的 UserManager 逻辑,基本 API 只会观察和验证 JWT 令牌声明。

你应该创建自己的 AuthorizationHandler 来检查用户是否确实是 Admin

或者使用 RequireAssertion 的更不正式的方法:

services.AddAuthorization(options => options.AddPolicy("Admininstrators", builder =>
{
    builder.RequireAssertion(async context =>
    {
        string strUserId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
        var user = await userManager.FindByIdAsync(strUserId);
        string[] roles = (await userManager.GetRolesAsync(user)).ToArray();
        return roles.Contains("Admin");
    };
});


[Authorize("Admininstrators")]
[HttpGet("userinformations")]
public async Task<UserInformations> GetCurrentUserInformations()
{
   ...
}

首先,感谢您的回答。既然我知道它使用了那个声明,那么如何根据我的数据库内容添加声明呢? - Rémy Huot
你有几种方法可以实现这个功能: 最标准的方法是创建自己的AuthorizationRequirement和AuthorizarionHandler,并实现HandleRequirementAsync方法。你可以看看RolesAuthorizationRequirement是如何实现的 https://github.com/dotnet/aspnetcore/blob/b56f84131af2e1ece61241a016e191f5f2fe3fc0/src/Security/Authorization/Core/src/RolesAuthorizationRequirement.cs然后你定义自己的策略并添加这个要求。查看此指南 https://geeklearning.io/create-your-own-authorization-requirements-in-asp-net-core/ - Michael Shterenberg
在我的回答中添加了一个代码示例,使用更快的RequireAssertion解决方案。 - Michael Shterenberg
谢谢,我会尝试这个方法并在它起作用时将其标记为答案,但您认为有没有一种方法可以在用户登录时添加“角色”声明,以便我可以使用[Authorize(Roles = "Admin")]。这样,每当我的数据库中出现新角色时,我就不需要创建新的策略了。就像在身份验证期间,在数据库中检查用户角色以将它们添加到声明中一样。 - Rémy Huot
谢谢你的回答。它帮助我理解了机制。我尝试使用你的方法,但我还不能在Startup.cs的ConfigureServices(...)中使用userManager。 - Rémy Huot
显示剩余2条评论

0

我终于找到了一个可行的解决方案。 我尝试使用RequireAssertion调整@MichaelShterenberg的代码,但是由于我必须查询我的数据库并且无法在此解决方案中使用UserManager,因此我无法使其正常工作。 最终,我基于他回答中的这部分内容找到了解决方案:

您应该创建自己的AuthorizationHandler,检查用户是否确实为管理员

我遵循了这个线程的答案:DotNet Core中AuthorizationOptions要求的依赖注入


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接