使用 Windows 身份验证的 Intranet 应用程序是否需要 ASP.NET 核心身份

Is ASP.NET Core Identity needed for Intranet app using Windows Authentication

在 Intranet web 应用程序中使用 Windows 身份验证我想实现以下目标:

在我寻找答案时,建议我需要将 ClaimsTransformation 添加到我的申请中:

虽然我不完全理解解决方案以及为什么每个请求都会发生 ClaimsTransformation,所以我正在寻找以下问题的答案:

  1. ASP.NET Core Identity 是 ClaimsTransformation 工作所必需的吗?
  2. ClaimsTransformation 是在仅使用 Windows 身份验证还是基于表单的身份验证的每个请求上发生?
  3. 每个请求都必须这样做吗?
  4. 像 GivenName、Surname 这样的缓存声明看起来很简单,但是角色呢?需要采取哪些步骤来确保不会每次都访问数据库,但角色会在发生更改时得到更新。
  5. 对于我正在尝试做的事情,是否有更简单的替代方案?

这个 article 给了我一些想法,这里是一个可能的解决方案。

控制器将从基础控制器继承,该控制器具有需要 Authenticated 声明的策略。当它不存在时,它会转到 AccessDeniedPath 并静默执行登录,添加 Authenticated 声明以及任何其他声明,如果它已经存在,则会出现“拒绝访问”消息。

在创建新的 ClaimsIdentity 时,我不得不删除原始身份中的大部分声明,因为我收到 HTTP 400 - Bad Request (Request Header too long) 错误消息。

这种方法有什么明显的问题吗?

Startup.cs

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();

        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie(options =>
            {
                options.LoginPath = "/Home/Login";
                options.AccessDeniedPath = "/Home/AccessDenied";
            });

        services.AddAuthorization(options =>
        {
            options.AddPolicy("Authenticated",
                policy => policy.RequireClaim("Authenticated"));
            options.AddPolicy("Admin",
                policy => policy.RequireClaim("Admin"));
        });
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseBrowserLink();
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }

        app.UseStaticFiles();
        app.UseAuthentication();

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

控制器

[Authorize(Policy = "Authenticated")]
public class HomeController : Controller
{
    public IActionResult Index()
    {
        return View();
    }

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

    [AllowAnonymous]
    public async Task<IActionResult> Login(string returnUrl)
    {
        var identity = ((ClaimsIdentity)HttpContext.User.Identity);

        var claims = new List<Claim>
        {
            new Claim("Authenticated", "True"),
            new Claim(ClaimTypes.Name,
                identity.FindFirst(c => c.Type == ClaimTypes.Name).Value),
            new Claim(ClaimTypes.PrimarySid,
                identity.FindFirst(c => c.Type == ClaimTypes.PrimarySid).Value)
        };

        var claimsIdentity = new ClaimsIdentity(
            claims,
            identity.AuthenticationType,
            identity.NameClaimType,
            identity.RoleClaimType);

        await HttpContext.SignInAsync(
            CookieAuthenticationDefaults.AuthenticationScheme,
            new ClaimsPrincipal(claimsIdentity),
            new AuthenticationProperties());

        return Redirect(returnUrl);
    }

    [AllowAnonymous]
    public IActionResult AccessDenied(string returnUrl)
    {
        if (User.FindFirst("Authenticated") == null)
            return RedirectToAction("Login", new { returnUrl });

        return View();
    }
}

这是一个使用 IClaimsTransformation 的替代方法(使用 .NET 6)

一些注意事项:

在 ClaimsTransformer class 中,必须克隆现有的 ClaimsPrincipal 并将您的声明添加到 that,而不是尝试修改现有的。然后必须在 ConfigureServices() 中将其注册为单例。

mheptinstall 的答案中用于设置 AccessDeniedPath 的技术在这里不起作用,相反,我必须使用 UseStatusCodePages() 方法才能重定向到自定义页面以获取 403 错误。

必须使用类型 newIdentity.RoleClaimType 创建新声明,而不是 System.Security.Claims.ClaimTypes.Role,否则 AuthorizeAttribute(例如 [Authorize(Roles = "Admin")])将不起作用

显然,应用程序将设置为使用 Windows 身份验证。

ClaimsTransformer.cs

public class ClaimsTransformer : IClaimsTransformation
{
    // Can consume services from DI as needed, including scoped DbContexts
    public ClaimsTransformer(IHttpContextAccessor httpAccessor) { }

    public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        // Clone current identity
        var clone = principal.Clone();
        var newIdentity = (ClaimsIdentity)clone.Identity;

        // Get the username
        var username = principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier || c.Type == ClaimTypes.Name).Value;

        if (username == null)
        {
            return principal;
        }

        // Get the user roles from the database using the username we've just obtained
        // Ideally these would be cached where possible
        
        // ...

        // Add role claims to cloned identity
        foreach (var roleName in roleNamesFromDatabase)
        {
            var claim = new Claim(newIdentity.RoleClaimType, roleName);
            newIdentity.AddClaim(claim);
        }

        return clone;
    }
}

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(IISDefaults.AuthenticationScheme);
    services.AddAuthorization();
    services.AddSingleton<IClaimsTransformation, ClaimsTransformer>();

    services.AddMvc().AddRazorRuntimeCompilation();

    // ...
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseStatusCodePages(async context => {
        if (context.HttpContext.Response.StatusCode == 403)
        {
            context.HttpContext.Response.Redirect("/Home/AccessDenied");
        }
    });

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}

示例HomeController.cs

[Authorize]
public class HomeController : Controller
{
    public HomeController()
    { }

    public IActionResult Index()
    {
        return View();
    }

    [Authorize(Roles = "Admin")]
    public IActionResult AdminOnly()
    {
        return View();
    }

    [AllowAnonymous]
    public IActionResult AccessDenied()
    {
        return View();
    }
}