如何为ASP.NET Core 3的IdentityServer4自定义认证流程
Hey there! Let's break down how you can add those custom validation rules to your IdentityServer4 setup in ASP.NET Core 3.x. You've already got the base configuration working, so we'll build on that to enforce your specific access requirements.
1. Prep: Ensure Your User Entity Has Required Fields
First, make sure your AppUser class includes a BirthYear property (to validate the 1968 birth rule) if it doesn't already:
public class AppUser : IdentityUser { public int BirthYear { get; set; } }
Don't forget to create and apply a database migration to add this field to your user table.
2. Implement a Custom Resource Owner Password Validator
Since you're using the password flow (via /connect/token), the most direct way to add custom rules is to implement IResourceOwnerPasswordValidator—this is where IdentityServer checks credentials for password grant requests.
Create a new class:
using IdentityServer4.Models; using IdentityServer4.Validation; using Microsoft.AspNetCore.Identity; using System.Linq; using System.Threading.Tasks; namespace AuthServer { public class CustomResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator { private readonly UserManager<AppUser> _userManager; private readonly IActiveFolderChecker _activeFolderChecker; // We'll define this next public CustomResourceOwnerPasswordValidator(UserManager<AppUser> userManager, IActiveFolderChecker activeFolderChecker) { _userManager = userManager; _activeFolderChecker = activeFolderChecker; } public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) { // Step 1: Run default Identity password validation first var user = await _userManager.FindByNameAsync(context.UserName); if (user == null) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Invalid username or password"); return; } var isPasswordValid = await _userManager.CheckPasswordAsync(user, context.Password); if (!isPasswordValid) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Invalid username or password"); return; } // Step 2: Apply your custom rules one by one // Rule 1: Username must contain "admin" if (!context.UserName.Contains("admin", System.StringComparison.OrdinalIgnoreCase)) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Username must include 'admin'"); return; } // Rule 2: Password must consist only of the letter "p" if (!context.Password.All(c => c == 'p')) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Password must only contain the letter 'p'"); return; } // Rule 3: User must be born in 1968 if (user.BirthYear != 1968) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "User must be born in 1968"); return; } // Rule 4: User has an active folder on the server var hasActiveFolder = await _activeFolderChecker.HasActiveFolderAsync(user.Id); if (!hasActiveFolder) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "User does not have an active server folder"); return; } // All checks passed: return valid grant context.Result = new GrantValidationResult( subject: user.Id, authenticationMethod: "custom-password-validation", claims: null); } } }
3. Create a Service to Check for Active Folders
Next, define an interface and implementation to handle the active folder check (replace the sample logic with your actual server/file system check):
using System.Threading.Tasks; namespace AuthServer { public interface IActiveFolderChecker { Task<bool> HasActiveFolderAsync(string userId); } public class ActiveFolderChecker : IActiveFolderChecker { public async Task<bool> HasActiveFolderAsync(string userId) { // Replace this with your real logic: // e.g., Check if a folder exists for the user ID, or query a database flag // var folderPath = Path.Combine("/server/folders", userId); // return Directory.Exists(folderPath) && IsFolderMarkedActive(folderPath); return await Task.FromResult(true); // Temporary placeholder } } }
4. Register Custom Services in Startup.cs
Update your ConfigureServices method to inject our new services and replace IdentityServer's default password validator:
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<AppUserDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("Default"))); services.AddIdentity<AppUser, IdentityRole>( options => { options.Password.RequiredLength = 6; options.Password.RequireLowercase = true; options.Password.RequireDigit = false; options.Password.RequireNonAlphanumeric = false; options.Password.RequireUppercase = false; } ) .AddEntityFrameworkStores<AppUserDbContext>() .AddDefaultTokenProviders(); // Register our active folder checker services.AddScoped<IActiveFolderChecker, ActiveFolderChecker>(); // Replace IdentityServer's default password validator with our custom one services.AddScoped<IResourceOwnerPasswordValidator, CustomResourceOwnerPasswordValidator>(); services.AddIdentityServer() .AddInMemoryIdentityResources(Config.Ids) .AddInMemoryApiResources(Config.Apis) .AddInMemoryClients(Config.Clients) .AddDeveloperSigningCredential() .AddAspNetIdentity<AppUser>(); services.AddCors(o => o.AddPolicy("MyPolicy", builder => { builder.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); })); services.AddControllers(); }
5. Optional: Enforce Rules During User Registration/Updates
If you want to block invalid users from being registered in the first place, extend the UserValidator<AppUser>:
using Microsoft.AspNetCore.Identity; using System.Threading.Tasks; namespace AuthServer { public class CustomUserValidator : UserValidator<AppUser> { public override async Task<IdentityResult> ValidateAsync(UserManager<AppUser> manager, AppUser user) { var baseResult = await base.ValidateAsync(manager, user); // Block users without "admin" in username if (!user.UserName.Contains("admin", System.StringComparison.OrdinalIgnoreCase)) { return IdentityResult.Failed(new IdentityError { Description = "Username must include 'admin'" }); } // Block users not born in 1968 if (user.BirthYear != 1968) { return IdentityResult.Failed(new IdentityError { Description = "User must be born in 1968" }); } return baseResult; } } }
Then register it in your AddIdentity chain:
services.AddIdentity<AppUser, IdentityRole>(...) .AddEntityFrameworkStores<AppUserDbContext>() .AddDefaultTokenProviders() .AddUserValidator<CustomUserValidator>();
With this setup, any request to /connect/token will go through your full set of custom rules—only users who meet all requirements will receive an access token.
内容的提问来源于stack exchange,提问作者Ch3shire

