如何在ASP.NET Core中实现分步登录(先验证用户名再输密码)
Hey there! I totally get how tricky this split login flow can be when you're just starting out with ASP Identity and IdentityServer4—let's break this down step by step so you can build it out smoothly.
IdentityServer4 uses a single default login page, so we first need to replace that with our two-step flow:
- Create two new pages in your IdentityServer project:
Account/Username(for username input) andAccount/Password(for password input). Use Razor Pages or MVC views depending on your project setup. - Update your IdentityServer configuration to point to the username page as the new login entry point:
services.AddIdentityServer(options => { options.UserInteraction.LoginUrl = "/Account/Username"; }) // Keep your existing configuration (AddInMemoryClients, AddAspNetIdentity, etc.)
Next, we'll add logic to check if the username exists in either ASP Identity's database or Active Directory.
2.1 Create an AD Validation Service
First, build a reusable service to handle AD checks (use System.DirectoryServices.AccountManagement package):
public interface IActiveDirectoryService { Task<bool> UserExistsAsync(string username); Task<bool> ValidateCredentialsAsync(string username, string password); string GetDomainSuffix(); } public class ActiveDirectoryService : IActiveDirectoryService { private readonly string _adDomain; public ActiveDirectoryService(IConfiguration config) { _adDomain = config["ActiveDirectory:Domain"]; // Pull domain from appsettings.json } public async Task<bool> UserExistsAsync(string username) { return await Task.Run(() => { using var context = new PrincipalContext(ContextType.Domain, _adDomain); return UserPrincipal.FindByIdentity(context, username) != null; }); } public async Task<bool> ValidateCredentialsAsync(string username, string password) { return await Task.Run(() => { using var context = new PrincipalContext(ContextType.Domain, _adDomain); return context.ValidateCredentials(username, password); }); } public string GetDomainSuffix() { return string.Join(".", _adDomain.Split('.').Skip(1)); // e.g., "company.com" from "corp.company.com" } }
Register this service in your DI container:
services.AddScoped<IActiveDirectoryService, ActiveDirectoryService>();
2.2 Handle Username Submission
In your Username.cshtml.cs (Razor Pages) or AccountController (MVC), add logic to validate the username and redirect to the password page:
private readonly UserManager<ApplicationUser> _userManager; private readonly IActiveDirectoryService _adService; public UsernameModel(UserManager<ApplicationUser> userManager, IActiveDirectoryService adService) { _userManager = userManager; _adService = adService; } [BindProperty] public string Username { get; set; } public async Task<IActionResult> OnPostAsync() { if (!ModelState.IsValid) return Page(); // Check ASP Identity database first var identityUser = await _userManager.FindByNameAsync(Username); if (identityUser != null) { TempData["Username"] = Username; TempData["AuthSource"] = "Identity"; // Mark source for later password check return RedirectToPage("/Account/Password", new { returnUrl = Request.Query["returnUrl"] }); } // Check Active Directory bool adUserExists = await _adService.UserExistsAsync(Username); if (adUserExists) { TempData["Username"] = Username; TempData["AuthSource"] = "AD"; return RedirectToPage("/Account/Password", new { returnUrl = Request.Query["returnUrl"] }); } // Username not found in either source ModelState.AddModelError(string.Empty, "用户名不存在,请检查后重试"); return Page(); }
Now build the password page to validate credentials based on the source we marked earlier.
In Password.cshtml.cs:
private readonly UserManager<ApplicationUser> _userManager; private readonly SignInManager<ApplicationUser> _signInManager; private readonly IActiveDirectoryService _adService; public PasswordModel(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager, IActiveDirectoryService adService) { _userManager = userManager; _signInManager = signInManager; _adService = adService; } [BindProperty] public string Password { get; set; } public string Username { get; set; } public IActionResult OnGet() { // Retrieve username from TempData; redirect to username page if missing Username = TempData["Username"] as string; if (string.IsNullOrEmpty(Username)) { return RedirectToPage("/Account/Username", new { returnUrl = Request.Query["returnUrl"] }); } // Re-store TempData values (they're cleared after first read) TempData["Username"] = Username; TempData["AuthSource"] = TempData["AuthSource"]; return Page(); } public async Task<IActionResult> OnPostAsync() { var username = TempData["Username"] as string; var authSource = TempData["AuthSource"] as string; var returnUrl = Request.Query["returnUrl"]; if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(authSource)) { return RedirectToPage("/Account/Username", new { returnUrl }); } bool isAuthenticated = false; ApplicationUser user = null; if (authSource == "Identity") { user = await _userManager.FindByNameAsync(username); if (user != null) { var result = await _signInManager.CheckPasswordSignInAsync(user, Password, lockoutOnFailure: false); isAuthenticated = result.Succeeded; // Handle lockout if needed if (result.IsLockedOut) { ModelState.AddModelError(string.Empty, "账户已锁定,请15分钟后重试"); Username = username; return Page(); } } } else if (authSource == "AD") { isAuthenticated = await _adService.ValidateCredentialsAsync(username, Password); if (isAuthenticated) { // Create ASP Identity user if they don't exist (for consistent claims handling) user = await _userManager.FindByNameAsync(username); if (user == null) { user = new ApplicationUser { UserName = username, Email = $"{username}@{_adService.GetDomainSuffix()}" }; var createResult = await _userManager.CreateAsync(user); if (!createResult.Succeeded) { ModelState.AddModelError(string.Empty, "创建用户失败,请联系管理员"); Username = username; return Page(); } } // Sign in the user via ASP Identity await _signInManager.SignInAsync(user, isPersistent: false); } } if (isAuthenticated) { // Redirect back to IdentityServer's requested resource return LocalRedirect(returnUrl ?? "/"); } else { ModelState.AddModelError(string.Empty, "密码错误,请重试"); Username = username; // Re-store TempData for re-render TempData["Username"] = username; TempData["AuthSource"] = authSource; return Page(); } }
Username Page (Username.cshtml)
@page @model UsernameModel @{ ViewData["Title"] = "输入用户名"; } <div class="container mt-5"> <div class="row justify-content-center"> <div class="col-md-4"> <form method="post"> <h3 class="mb-3">请输入您的用户名</h3> <hr /> <div asp-validation-summary="All" class="text-danger mb-3"></div> <div class="form-group"> <label asp-for="Username" class="form-label"></label> <input asp-for="Username" class="form-control" required /> <span asp-validation-for="Username" class="text-danger"></span> </div> <button type="submit" class="btn btn-primary w-100">下一步</button> </form> </div> </div> </div>
Password Page (Password.cshtml)
@page @model PasswordModel @{ ViewData["Title"] = "输入密码"; } <div class="container mt-5"> <div class="row justify-content-center"> <div class="col-md-4"> <form method="post"> <h3 class="mb-3">欢迎回来,@Model.Username</h3> <hr /> <div asp-validation-summary="All" class="text-danger mb-3"></div> <div class="form-group"> <label asp-for="Password" class="form-label"></label> <input asp-for="Password" type="password" class="form-control" required /> <span asp-validation-for="Password" class="text-danger"></span> </div> <button type="submit" class="btn btn-primary w-100">登录</button> </form> </div> </div> </div>
- Secure TempData: TempData uses cookies by default—configure it to be secure in your startup code:
services.Configure<CookieTempDataProviderOptions>(options => { options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.Cookie.SameSite = SameSiteMode.Lax; options.Cookie.HttpOnly = true; });
- Account Lockout: Enable lockout for ASP Identity users in your
AddIdentityconfiguration:
services.AddIdentity<ApplicationUser, IdentityRole>(options => { options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15); }) .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders();
- Input Sanitization: Add server-side validation to prevent malicious input (e.g., check for invalid characters in usernames).
内容的提问来源于stack exchange,提问作者zubairvo

