AspNetCore中Windows身份认证结合Identity双登录方案咨询
问题描述
我正在开发一款内网Web应用,需满足以下需求:
- 支持通过Windows身份认证实现SSO,并生成包含权限声明等信息的JWT令牌,用于访问应用资源;
- 可将域用户添加至应用自定义角色,并通过Identity的
AspNetRoleClaims表为角色分配自定义权限; - 支持通过标准账号密码表单登录(如LDAP服务器故障时),域账户需持续同步至
AspNetCore.Identity.AspNetUsers数据表。
已完成的配置:
- Windows身份认证配置(Program.cs):
builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme) .AddNegotiate();
- Identity基础配置(Program.cs):
builder.Services.AddIdentity<IdentityUser, IdentityRole>() .AddEntityFrameworkStores<GeoLinkDbContext>();
已实现的授权端点:
[Authorize(AuthenticationSchemes = NegotiateDefaults.AuthenticationScheme)] [HttpGet("token")] public async Task<ActionResult<List<string>>> GetToken() { var userName = User.Identity?.Name; if (userName is null) return Unauthorized(); var exists = await _userManager.FindByNameAsync(userName); var userGroups = GetUserADGroups(userName); // TODO: Firstly we should check here if user has proper AD role assigned to. // TODO: Then we should check user privileges against Identity DB and prepare the JWT Token and Claims and return it return userGroups; } private static List<string> GetUserADGroups(string userName) { if (userName is null) return new List<string>(); using var ctx = new PrincipalContext(ContextType.Domain); using var user = UserPrincipal.FindByIdentity(ctx, userName); if (user is null) return new List<string>(); return user .GetGroups() .Select(x => x.SamAccountName) .ToList(); }
遇到的问题:
当用户无法通过Windows身份认证时,不知如何引导至登录页面,实现基于Identity数据库的账号密码验证。此外,想咨询上述结合Windows身份认证与AspNetCore Identity的整体流程是否为合理方案?
解决方案与方案合理性分析
一、整体方案合理性判断
你的方案是内网应用的典型合理架构:
- Windows身份认证(Negotiate/Kerberos)满足内网SSO需求,无需用户手动输入账号密码;
- AspNetCore Identity作为本地用户权限存储,实现自定义角色与权限的灵活管理,同时在AD故障时提供降级登录方案;
- JWT令牌作为应用资源的统一授权凭证,适配前后端分离或微服务场景,避免每次请求都走AD验证,提升性能。
这种"AD身份源 + Identity权限存储 + JWT凭证"的模式,既利用了内网域环境的优势,又保留了应用自身的权限管控灵活性,完全符合需求。
二、解决Windows认证失败时引导至表单登录的问题
1. 配置多认证方案与默认策略
修改Program.cs中的认证配置,同时启用Negotiate(Windows认证)和Identity Cookie认证,并设置默认认证/挑战策略,让系统在Windows认证失败时自动跳转至表单登录:
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Negotiate; // 先配置Identity builder.Services.AddIdentity<IdentityUser, IdentityRole>() .AddEntityFrameworkStores<GeoLinkDbContext>() .AddDefaultTokenProviders(); // 配置多认证方案 builder.Services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; }) .AddNegotiate() .AddCookie(options => { options.LoginPath = "/Account/Login"; // 指定表单登录页面路径 options.AccessDeniedPath = "/Account/AccessDenied"; }); // 配置授权策略,允许两种认证方式 builder.Services.AddAuthorization(options => { options.DefaultPolicy = new AuthorizationPolicyBuilder() .AddAuthenticationSchemes(NegotiateDefaults.AuthenticationScheme, CookieAuthenticationDefaults.AuthenticationScheme) .RequireAuthenticatedUser() .Build(); });
2. 实现Identity表单登录逻辑
创建AccountController处理登录请求,验证Identity数据库中的账号密码:
public class AccountController : Controller { private readonly SignInManager<IdentityUser> _signInManager; public AccountController(SignInManager<IdentityUser> signInManager) { _signInManager = signInManager; } [HttpGet] public IActionResult Login(string returnUrl = null) { ViewData["ReturnUrl"] = returnUrl; return View(); } [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null) { returnUrl ??= Url.Content("~/"); if (ModelState.IsValid) { var result = await _signInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, lockoutOnFailure: false); if (result.Succeeded) { return LocalRedirect(returnUrl); } if (result.IsLockedOut) { return RedirectToAction(nameof(Lockout)); } else { ModelState.AddModelError(string.Empty, "无效的登录尝试。"); return View(model); } } return View(model); } public IActionResult Lockout() { return View(); } } // 配套LoginViewModel public class LoginViewModel { [Required] public string UserName { get; set; } [Required] [DataType(DataType.Password)] public string Password { get; set; } [Display(Name = "记住我?")] public bool RememberMe { get; set; } }
3. 域用户自动同步到Identity数据库
在Windows认证成功后,自动将域用户同步到AspNetUsers表,避免手动添加:
修改GetToken方法,补充同步与JWT生成逻辑:
[Authorize] // 使用默认授权策略,支持两种认证方式 [HttpGet("token")] public async Task<ActionResult<string>> GetToken() { var userName = User.Identity?.Name; if (userName is null) return Unauthorized(); // 同步域用户到Identity var identityUser = await _userManager.FindByNameAsync(userName); if (identityUser == null) { using var ctx = new PrincipalContext(ContextType.Domain); using var adUser = UserPrincipal.FindByIdentity(ctx, userName); if (adUser != null) { identityUser = new IdentityUser { UserName = userName, Email = adUser.Email, EmailConfirmed = true }; var createResult = await _userManager.CreateAsync(identityUser); if (!createResult.Succeeded) { return BadRequest(createResult.Errors); } } else { return Unauthorized("无法获取域用户信息"); } } // 收集用户所有权限声明 var allClaims = new List<Claim>(); // 添加AD组声明 var adGroups = GetUserADGroups(userName); foreach (var group in adGroups) { allClaims.Add(new Claim("ad_group", group)); } // 添加Identity角色与自定义权限声明 var identityRoles = await _userManager.GetRolesAsync(identityUser); foreach (var role in identityRoles) { allClaims.Add(new Claim(ClaimTypes.Role, role)); var roleClaims = await _roleManager.GetClaimsAsync(await _roleManager.FindByNameAsync(role)); allClaims.AddRange(roleClaims); } // 生成JWT令牌 var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(builder.Configuration["Jwt:Secret"]); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(allClaims), Expires = DateTime.UtcNow.AddHours(8), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature), Issuer = builder.Configuration["Jwt:Issuer"], Audience = builder.Configuration["Jwt:Audience"] }; var token = tokenHandler.CreateToken(tokenDescriptor); return Ok(tokenHandler.WriteToken(token)); }
4. 补充JWT验证配置
在Program.cs中添加JWT Bearer认证,用于验证后续请求中的令牌:
builder.Services.AddAuthentication() // ... 已有的Negotiate和Cookie配置 .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidAudience = builder.Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(builder.Configuration["Jwt:Secret"])) }; });
三、额外优化建议
- 新增定时任务,定期同步AD用户信息到Identity数据库,避免用户信息变更后不同步;
- 对敏感操作启用双因素认证,提升账号安全性;
- 在AD故障时,可通过配置开关临时禁用Windows认证,只允许表单登录。
内容的提问来源于stack exchange,提问作者XardasLord
相关产品推荐
相关产品推荐

