ASP.NET Core 8.0中JWT与Identity认证跳转及登录循环问题
ASP.NET Core 8.0 洋葱架构下JWT+Identity认证问题排查
问题背景
开发基于洋葱架构的ASP.NET Core 8.0项目,搭配JWT和Identity实现认证体系。API端生成Token后可通过Postman正常访问,但MVC端登录流程存在以下两个问题:
问题1:未授权请求重定向到默认登录页
使用以下Program.cs配置时,未授权请求会跳转到默认的Account/Login页面,而非自定义的Login/Index:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddCookie(JwtBearerDefaults.AuthenticationScheme, options => { options.LoginPath = "/Login/Index"; options.LogoutPath = "/Login/Index"; options.AccessDeniedPath = "/Error/Error404"; options.Cookie.SameSite = SameSiteMode.Strict; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; options.Cookie.Name = "AuthToken"; });
问题2:登录后无法跳转至授权页面
使用以下配置时,未授权请求能正确跳转到Login/Index,但登录后跳转Home/Index时会被重定向回登录页,无法完成授权:
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options => { options.LoginPath = "/Login/Index"; options.LogoutPath = "/Login/Index"; options.AccessDeniedPath = "/Error/Error404"; options.Cookie.SameSite = SameSiteMode.Strict; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; options.Cookie.Name = "AuthToken"; });
完整项目代码
Program.cs
using Domain.Entities; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; using Persistence.Context; using PresentationUI.Handlers; var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext<ApplicationContext>(); builder.Services.AddIdentity<AppUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationContext>() .AddDefaultTokenProviders(); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddCookie(JwtBearerDefaults.AuthenticationScheme, options => { options.LoginPath = "/Login/Index"; options.LogoutPath = "/Login/Index"; options.AccessDeniedPath = "/Error/Error404"; options.Cookie.SameSite = SameSiteMode.Strict; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; options.Cookie.Name = "AuthToken"; }); builder.Services.AddHttpClient(); builder.Services.AddHttpContextAccessor(); builder.Services.AddTransient<AuthorizedHttpClientHandler>(); builder.Services.AddHttpClient("AuthorizedClient") .AddHttpMessageHandler<AuthorizedHttpClientHandler>(); builder.Services.AddControllersWithViews(); builder.Logging.ClearProviders(); builder.Logging.AddConsole(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); // 默认HSTS值为30天,生产环境可按需调整 app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); app.Run();
LoginController.cs
using Domain.Entities; using DtoLayer.LoginDtos; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using PresentationUI.Models; using System.Text.Json; namespace PresentationUI.Controllers { public class LoginController : Controller { private readonly IHttpClientFactory _httpClientFactory; private readonly UserManager<AppUser> _userManager; private readonly SignInManager<AppUser> _signInManager; public LoginController(IHttpClientFactory httpClientFactory, UserManager<AppUser> userManager, SignInManager<AppUser> signInManager) { _httpClientFactory = httpClientFactory; _userManager = userManager; _signInManager = signInManager; } [HttpGet] public IActionResult Index() { return View(); } [HttpPost] public async Task<IActionResult> Index(CreateLoginDto createLoginDto) { var user = await _userManager.FindByNameAsync(createLoginDto.UserName); if (user != null) { var result = await _signInManager.CheckPasswordSignInAsync(user, createLoginDto.Password, true); if (result.Succeeded) { if (!await _userManager.IsEmailConfirmedAsync(user)) { return RedirectToAction("Index", "Confirmation"); } else { var login = await _signInManager.PasswordSignInAsync(createLoginDto.UserName, createLoginDto.Password, true, true); if (login.Succeeded) { if (await _userManager.IsInRoleAsync(user, "User")) { var client = _httpClientFactory.CreateClient(); var content = new StringContent(JsonSerializer.Serialize(createLoginDto), System.Text.Encoding.UTF8, "application/json"); var response = await client.PostAsync("https://localhost:7125/api/Login", content); if (response.IsSuccessStatusCode) { var jsonData = await response.Content.ReadAsStringAsync(); var tokenModel = JsonSerializer.Deserialize<JwtResponseModel>(jsonData, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); if (tokenModel != null && tokenModel.Token != null) { HttpContext.Response.Cookies.Append("AuthToken", tokenModel.Token, new CookieOptions { HttpOnly = true, Secure = true, }); return RedirectToAction("Index", "Home"); } } } else { ModelState.AddModelError("", "您没有访问该页面的权限。"); return View(); } } else if (login.IsLockedOut) { ModelState.AddModelError("", "因多次错误登录,您的账户已锁定。请稍后重试,或通过“忘记密码”重置密码。"); } else { ModelState.AddModelError("", "用户名或密码错误"); } } } else if (result.IsLockedOut) { ModelState.AddModelError("", "因多次错误登录,您的账户已锁定。请稍后重试,或通过“忘记密码”重置密码。"); } else { ModelState.AddModelError("", "用户名或密码错误"); } } else { ModelState.AddModelError("", "未找到该账户"); } return View(); } } }
HomeController.cs
using DtoLayer.BrandDtos; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; namespace PresentationUI.Controllers { [Authorize] public class HomeController : Controller { private readonly IHttpClientFactory _clientFactory; public HomeController(IHttpClientFactory clientFactory) { _clientFactory = clientFactory; } public async Task<IActionResult> Index() { var client = _clientFactory.CreateClient("AuthorizedClient"); var response = await client.GetAsync("https://localhost:7125/api/Brand"); if (response.IsSuccessStatusCode) { var jsonData = await response.Content.ReadAsStringAsync(); var values = JsonConvert.DeserializeObject<List<ResultBrandDto>>(jsonData); return View(values); } return View(); } } }
AuthorizedHttpClientHandler.cs
using System.Net.Http.Headers; namespace PresentationUI.Handlers { public class AuthorizedHttpClientHandler : DelegatingHandler { private readonly IHttpContextAccessor _httpContextAccessor; public AuthorizedHttpClientHandler(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var token = _httpContextAccessor.HttpContext.Request.Cookies["AuthToken"]; if (!string.IsNullOrEmpty(token)) { request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); } return await base.SendAsync(request, cancellationToken); } } }
问题原因及解决方案
问题1:重定向到默认登录页的原因及解决
原因:将默认认证方案设置为JwtBearerDefaults.AuthenticationScheme时,JWT认证中间件不处理重定向逻辑(JWT为无状态认证,通常返回401而非重定向),此时Identity的默认Cookie认证会接管流程,导致跳转到默认的Account/Login。
解决方法:
明确设置Cookie认证为默认方案,同时配置JWT认证用于API调用,MVC端依赖Cookie认证完成授权。修改Program.cs的认证配置:
builder.Services.AddAuthentication(options => { // 设置默认认证/挑战方案为Cookie options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; }) .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => { options.LoginPath = "/Login/Index"; options.LogoutPath = "/Login/Index"; options.AccessDeniedPath = "/Error/Error404"; options.Cookie.SameSite = SameSiteMode.Strict; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; options.Cookie.Name = "AuthToken"; }) .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => { // 配置JWT验证参数,需与API端保持一致 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.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"])) }; });
问题2:登录后重定向回登录页的原因及解决
原因:
SignInManager.PasswordSignInAsync会创建Identity的认证Cookie,而代码中手动添加的JWT Cookie与Identity的Cookie名称冲突,导致授权验证失败;HomeController的[Authorize]属性依赖默认Cookie认证,但Identity的Cookie未正确配置,导致认证状态不被识别。
解决步骤:
- 统一Identity的Cookie名称:
修改Identity配置,使其Cookie名称与认证配置一致:
builder.Services.AddIdentity<AppUser, IdentityRole>(options => { options.Cookies.ApplicationCookie.Name = "AuthToken"; }) .AddEntityFrameworkStores<ApplicationContext>() .AddDefaultTokenProviders();
- 避免Cookie名称冲突:
若需同时保留JWT用于API调用,修改JWT的Cookie名称,避免与Identity的Cookie冲突:
// LoginController中修改Cookie名称 HttpContext.Response.Cookies.Append("ApiJwtToken", tokenModel.Token, new CookieOptions { HttpOnly = true, Secure = true, }); // AuthorizedHttpClientHandler中读取新的Cookie名称 var token = _httpContextAccessor.HttpContext.Request.Cookies["ApiJwtToken"];
- 简化登录逻辑:
MVC端的授权依赖Identity的Cookie,SignInManager.PasswordSignInAsync已完成认证流程,无需手动添加JWT到Cookie(仅当需要调用API时才保留JWT的Cookie)。
内容的提问来源于stack exchange,提问作者Gökmen Ada
相关产品推荐
相关产品推荐

