使用 Windows 身份验证的 Intranet 应用程序是否需要 ASP.NET 核心身份
Is ASP.NET Core Identity needed for Intranet app using Windows Authentication
在 Intranet web 应用程序中使用 Windows 身份验证我想实现以下目标:
- 从 AD 中收集额外的属性(姓名、员工编号)
- 从数据库中收集额外的属性table(工作时间、工资)
- 根据应用程序角色(而非 AD 组)授权
- 根据 AD 属性授权(有直接下属)
- 用户未提供 username/password
在我寻找答案时,建议我需要将 ClaimsTransformation
添加到我的申请中:
虽然我不完全理解解决方案以及为什么每个请求都会发生 ClaimsTransformation
,所以我正在寻找以下问题的答案:
- ASP.NET Core Identity 是
ClaimsTransformation
工作所必需的吗?
ClaimsTransformation
是在仅使用 Windows 身份验证还是基于表单的身份验证的每个请求上发生?
- 每个请求都必须这样做吗?
- 像 GivenName、Surname 这样的缓存声明看起来很简单,但是角色呢?需要采取哪些步骤来确保不会每次都访问数据库,但角色会在发生更改时得到更新。
- 对于我正在尝试做的事情,是否有更简单的替代方案?
这个 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();
}
}
在 Intranet web 应用程序中使用 Windows 身份验证我想实现以下目标:
- 从 AD 中收集额外的属性(姓名、员工编号)
- 从数据库中收集额外的属性table(工作时间、工资)
- 根据应用程序角色(而非 AD 组)授权
- 根据 AD 属性授权(有直接下属)
- 用户未提供 username/password
在我寻找答案时,建议我需要将 ClaimsTransformation
添加到我的申请中:
虽然我不完全理解解决方案以及为什么每个请求都会发生 ClaimsTransformation
,所以我正在寻找以下问题的答案:
- ASP.NET Core Identity 是
ClaimsTransformation
工作所必需的吗? ClaimsTransformation
是在仅使用 Windows 身份验证还是基于表单的身份验证的每个请求上发生?- 每个请求都必须这样做吗?
- 像 GivenName、Surname 这样的缓存声明看起来很简单,但是角色呢?需要采取哪些步骤来确保不会每次都访问数据库,但角色会在发生更改时得到更新。
- 对于我正在尝试做的事情,是否有更简单的替代方案?
这个 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();
}
}