You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

AspNetCore中Windows身份认证结合Identity双登录方案咨询

问题描述

我正在开发一款内网Web应用,需满足以下需求:

  • 支持通过Windows身份认证实现SSO,并生成包含权限声明等信息的JWT令牌,用于访问应用资源;
  • 可将域用户添加至应用自定义角色,并通过Identity的AspNetRoleClaims表为角色分配自定义权限;
  • 支持通过标准账号密码表单登录(如LDAP服务器故障时),域账户需持续同步至AspNetCore.Identity.AspNetUsers数据表。

已完成的配置:

  1. Windows身份认证配置(Program.cs):
builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
            .AddNegotiate();
  1. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 10:00:24