ASP.NET Core身份认证实战:从Cookie到JWT

📅 2026/7/19 8:21:08
ASP.NET Core身份认证实战:从Cookie到JWT
1. 为什么需要身份认证在Web开发中身份认证Authentication是确认用户身份的过程就像进入办公楼需要刷卡一样。ASP.NET Core提供了灵活的身份认证系统可以集成多种认证方式。我最近在一个电商项目中实现了基于Cookie的身份认证发现很多开发者对基础认证流程存在误解。ASP.NET Core的身份认证系统基于中间件Middleware构建通过认证方案Authentication Scheme来组织认证逻辑。最常见的三种认证方式是Cookie认证适合传统Web应用JWT适合API和SPA应用OAuth/OpenID Connect适合第三方登录提示选择认证方式时要考虑应用类型Web/API/混合、安全需求和用户体验。例如纯API服务用JWT更合适而需要持久会话的Web应用则更适合Cookie。2. 搭建基础认证环境2.1 创建ASP.NET Core项目首先用命令行创建新项目dotnet new webapp -n AuthDemo cd AuthDemo2.2 添加必要的NuGet包对于基础认证需要添加dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore dotnet add package Microsoft.EntityFrameworkCore.SqlServer2.3 配置Startup类在Program.cs中添加认证服务var builder WebApplication.CreateBuilder(args); // 添加认证服务 builder.Services.AddAuthentication(options { options.DefaultScheme CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme CookieAuthenticationDefaults.AuthenticationScheme; }).AddCookie(); var app builder.Build(); // 启用认证中间件 app.UseAuthentication(); app.UseAuthorization();注意UseAuthentication必须放在UseRouting之后、UseEndpoints之前这是常见的配置错误点。3. 实现用户登录功能3.1 创建用户模型添加Models/User.cspublic class User { public int Id { get; set; } public string Username { get; set; } public string Password { get; set; } // 实际项目应使用哈希存储 }3.2 创建登录页面在Pages文件夹下添加Login.cshtmlpage model AuthDemo.Pages.LoginModel h2Login/h2 form methodpost div label asp-forInput.Username/label input asp-forInput.Username / /div div label asp-forInput.Password/label input asp-forInput.Password typepassword / /div button typesubmitLogin/button /form3.3 实现登录逻辑在Login.cshtml.cs中public class LoginModel : PageModel { [BindProperty] public InputModel Input { get; set; } public class InputModel { [Required] public string Username { get; set; } [Required] public string Password { get; set; } } public async TaskIActionResult OnPostAsync() { if (!ModelState.IsValid) return Page(); // 实际项目应从数据库验证 if (Input.Username admin Input.Password 123456) { var claims new ListClaim { new Claim(ClaimTypes.Name, Input.Username), new Claim(CustomClaim, ExampleValue) }; var identity new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); await HttpContext.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity), new AuthenticationProperties { IsPersistent true // 持久化Cookie }); return RedirectToPage(/Index); } ModelState.AddModelError(string.Empty, Invalid login attempt.); return Page(); } }4. 保护页面和授权4.1 添加授权属性在需要保护的页面或控制器上添加[Authorize] public class SecureModel : PageModel { public void OnGet() { } }4.2 获取用户信息在受保护的页面中可以通过以下方式获取用户信息var username User.Identity.Name; var customClaim User.FindFirst(CustomClaim)?.Value;4.3 登出功能添加登出端点public async TaskIActionResult OnPostLogoutAsync() { await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return RedirectToPage(/Index); }5. 常见问题与解决方案5.1 Cookie安全性配置生产环境必须配置安全Cookieservices.ConfigureApplicationCookie(options { options.Cookie.HttpOnly true; options.Cookie.SecurePolicy CookieSecurePolicy.Always; options.SlidingExpiration true; options.ExpireTimeSpan TimeSpan.FromDays(30); });5.2 认证失败处理自定义认证失败返回services.ConfigureApplicationCookie(options { options.AccessDeniedPath /AccessDenied; options.LoginPath /Login; });5.3 多角色授权添加角色声明var claims new ListClaim { new Claim(ClaimTypes.Name, username), new Claim(ClaimTypes.Role, Admin) };然后使用角色授权[Authorize(Roles Admin)] public class AdminModel : PageModel { // ... }6. 进阶集成Entity Framework Core6.1 创建DbContextpublic class AppDbContext : IdentityDbContext { public AppDbContext(DbContextOptionsAppDbContext options) : base(options) { } }6.2 配置Identity服务替换之前的简单认证builder.Services.AddDbContextAppDbContext(options options.UseSqlServer(Configuration.GetConnectionString(DefaultConnection))); builder.Services.AddDefaultIdentityIdentityUser(options { options.Password.RequireDigit true; options.Password.RequiredLength 8; }).AddEntityFrameworkStoresAppDbContext();6.3 使用Identity登录修改登录逻辑var result await _signInManager.PasswordSignInAsync( Input.Username, Input.Password, Input.RememberMe, lockoutOnFailure: false); if (result.Succeeded) { return LocalRedirect(returnUrl); }7. 性能优化技巧7.1 会话存储优化将会话数据存储在分布式缓存中builder.Services.AddDistributedMemoryCache(); builder.Services.AddSession(options { options.IdleTimeout TimeSpan.FromMinutes(20); options.Cookie.HttpOnly true; });7.2 声明转换减少Cookie大小services.AddClaimsTransformation(principal { var claims principal.Claims.ToList(); // 转换或过滤声明 return Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity(claims))); });7.3 认证方案缓存对于高并发场景services.AddSingletonIAuthenticationSchemeProvider, CachingSchemeProvider();8. 安全最佳实践8.1 密码策略强制使用强密码services.ConfigureIdentityOptions(options { options.Password.RequireDigit true; options.Password.RequireLowercase true; options.Password.RequireNonAlphanumeric true; options.Password.RequireUppercase true; options.Password.RequiredLength 8; });8.2 CSRF防护确保表单有防伪令牌form methodpost Html.AntiForgeryToken() !-- 表单内容 -- /form8.3 安全头设置添加安全HTTP头app.Use(async (context, next) { context.Response.Headers.Add(X-Content-Type-Options, nosniff); context.Response.Headers.Add(X-Frame-Options, DENY); await next(); });9. 测试与调试9.1 单元测试示例测试受保护端点[Fact] public async Task SecurePage_RequiresAuthentication() { var client _factory.CreateClient(); var response await client.GetAsync(/Secure); Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.StartsWith(http://localhost/Login, response.Headers.Location.OriginalString); }9.2 调试认证流程启用详细日志Logging: { LogLevel: { Microsoft.AspNetCore.Authentication: Debug } }9.3 使用Postman测试配置Cookie测试先发送登录请求从响应头获取Set-Cookie值在后续请求中添加Cookie头10. 实际项目经验分享在最近的项目中我们遇到了Cookie在子域名间共享的问题。解决方案是配置options.Cookie.Domain .example.com;另一个常见问题是用户会话并发控制。可以通过以下方式实现services.ConfigureSecurityStampValidatorOptions(options { options.ValidationInterval TimeSpan.FromMinutes(30); });对于高安全要求的系统建议添加二次认证。可以使用ASP.NET Core的Authenticator库services.AddAuthentication() .AddGoogleAuthenticator(options { options.SecretLength 20; });最后别忘了定期审计认证日志。可以创建一个中间件来记录关键认证事件app.Use(async (context, next) { if (context.Request.Path.StartsWithSegments(/login)) { var logger context.RequestServices.GetRequiredServiceILoggerProgram(); logger.LogInformation(Login attempt from {IP}, context.Connection.RemoteIpAddress); } await next(); });