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

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:登录后重定向回登录页的原因及解决

原因:

  1. SignInManager.PasswordSignInAsync会创建Identity的认证Cookie,而代码中手动添加的JWT Cookie与Identity的Cookie名称冲突,导致授权验证失败;
  2. HomeController的[Authorize]属性依赖默认Cookie认证,但Identity的Cookie未正确配置,导致认证状态不被识别。

解决步骤:

  1. 统一Identity的Cookie名称:
    修改Identity配置,使其Cookie名称与认证配置一致:
builder.Services.AddIdentity<AppUser, IdentityRole>(options =>
{
    options.Cookies.ApplicationCookie.Name = "AuthToken";
})
.AddEntityFrameworkStores<ApplicationContext>()
.AddDefaultTokenProviders();
  1. 避免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"];
  1. 简化登录逻辑:
    MVC端的授权依赖Identity的Cookie,SignInManager.PasswordSignInAsync已完成认证流程,无需手动添加JWT到Cookie(仅当需要调用API时才保留JWT的Cookie)。

内容的提问来源于stack exchange,提问作者Gökmen Ada

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 20:44:53