React如何通过请求头发送JWT?.NET 7 API授权问题排查
问题描述
我有一个基于.NET 7的Web API,采用JWT进行身份验证与授权。通过SwaggerUI和Postman测试时,无JWT的请求会被正确拦截,带JWT的请求可正常访问;但通过React发送请求时无法正常工作。
React请求代码
const getTopics = () => { const url = API_ROUTES.TOPICS_GET_ALL_TOPICS; fetch(url, { method: "GET", headers: { 'Authorization': `Bearer ${jwt}` } }) .then(res => res.json()) .then(topicsFromServer => { console.log(topicsFromServer); setTopics(topicsFromServer); }) .catch((error => { console.error(error); })) }
控制台响应信息
Response {type: 'cors', url: 'https://localhost:7174/Account/Login?ReturnUrl=%2Ftopic%2Fget-all-topics', redirected: true, status: 405, ok: false, …} body : (...) bodyUsed : false headers : Headers {} ok : false redirected : true status : 405 statusText : "" type : "cors" url : "https://localhost:7174/Account/Login?ReturnUrl=%2Ftopic%2Fget-all-topics" [[Prototype]] : Response
该响应中的URL并非存在的端点,推测是.NET的[Authorize]特性自动生成的。
.NET CORS策略配置
builder.Services.AddCors(options => { options.AddPolicy("CORSPolicy", builder => { builder .AllowAnyMethod() .AllowAnyHeader() .WithOrigins("http://localhost:3000", "https://localhost:3000"); }); });
Swagger生成配置
builder.Services.AddSwaggerGen(options => { options.AddSecurityDefinition("auth0", new OpenApiSecurityScheme { In = ParameterLocation.Header, Name = "Authorization", Type = SecuritySchemeType.ApiKey }); options.OperationFilter<SecurityRequirementsOperationFilter>(); });
补充说明
进一步测试后发现,JWT并未被实际使用。用户登录后,浏览器(SwaggerUI)或Postman中会存储一个Cookie,无需JWT即可访问API。
使用的Cookie:
.AspNetCore.Identity.Application=ReallyLongString; Path=/; Secure; HttpOnly; Expires=Fri, 31 Mar 2023 23:07:00 GMT;
Program.cs完整代码
var builder = WebApplication.CreateBuilder(args); builder.Services.AddCors(options => { options.AddPolicy("CORSPolicy", builder => { builder .AllowAnyMethod() .AllowAnyHeader() .WithOrigins("http://localhost:3000", "https://localhost:3000", "https://appname.asurestaticapps.net"); }); }); // Add services to the container. builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { options.AddSecurityDefinition("auth0", new OpenApiSecurityScheme { In = ParameterLocation.Header, Name = "Authorization", Type = SecuritySchemeType.ApiKey }); options.OperationFilter<SecurityRequirementsOperationFilter>(); }); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters() { ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidAudience = builder.Configuration["Jwt:Audience"], ValidIssuer = builder.Configuration["Jwt:Issuer"], IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]) ) }; }); builder.Services.AddAuthorization(); builder.Services.AddDbContext<SdeResearchDbContext>(); builder.Services.AddIdentity<ApplicationUser, IdentityRole>() .AddEntityFrameworkStores<SdeResearchDbContext>() .AddDefaultTokenProviders(); builder.Services.AddScoped<JwtService>(); builder.Services.Configure<IdentityOptions>(options => { // Password Settings. options.Password.RequireDigit= true; options.Password.RequireLowercase= true; options.Password.RequireNonAlphanumeric= true; options.Password.RequireUppercase= true; options.Password.RequiredLength= 8; options.Password.RequireLowercase= true; options.Password.RequiredUniqueChars = 6; // User settings options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+!"; options.User.RequireUniqueEmail= true; }); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseRouting(); app.UseCors("CORSPolicy"); app.UseAuthentication(); app.UseAuthorization(); app.MapControllerRoute( name: "Topic", pattern: "topic/*{action}" ); app.Run();
TopicController顶部添加了[Authorize]特性。
解决方案
1. 核心问题定位
你的API同时启用了JWT Bearer认证和ASP.NET Core Identity Cookie认证,但默认情况下,Identity会将未授权请求重定向到登录页(即你看到的/Account/Login),而JWT认证的默认行为是直接返回401而非重定向。
Swagger/Postman能正常工作是因为它们在登录后自动携带了Identity的AspNetCore.Identity.Application Cookie,实际走的是Cookie认证而非JWT;而React请求只带了JWT,却被Identity的重定向逻辑干扰,导致返回405(重定向后的登录页不支持GET请求)。
2. 修复步骤
步骤一:修改JWT认证配置,禁用重定向
在AddJwtBearer配置中添加事件处理,当认证失败时直接返回401,避免被Identity重定向:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters() { ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidAudience = builder.Configuration["Jwt:Audience"], ValidIssuer = builder.Configuration["Jwt:Issuer"], IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]) ) }; // 添加以下代码 options.Events = new JwtBearerEvents { OnChallenge = context => { // 禁止重定向,直接返回401 context.HandleResponse(); context.Response.StatusCode = StatusCodes.Status401Unauthorized; context.Response.ContentType = "application/json"; return context.Response.WriteAsync(JsonSerializer.Serialize(new { Message = "未授权访问,请提供有效的JWT令牌" })); } }; });
步骤二:明确设置默认认证方案(可选但推荐)
由于同时注册了JWT和Identity认证,需要明确指定默认使用JWT Bearer方案,避免Identity Cookie优先生效:
builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { /* 原配置 */ });
步骤三:验证React请求的JWT有效性
确保React中携带的jwt变量是完整有效的令牌,没有截断或格式错误。可以在控制台打印jwt值,复制到JWT校验工具中验证签名和有效期。
步骤四:检查CORS配置(可选)
当前CORS配置允许指定Origin的任意头和方法,基本没问题,但如果后续遇到OPTIONS预检请求问题,可以根据需求调整,JWT场景下无需启用AllowCredentials()。
3. 验证修复
修改配置后重启API,在React中重新发送请求:
- 如果JWT有效,应正常返回数据;
- 如果JWT无效或未携带,应返回401 JSON响应,而非重定向到登录页。
内容的提问来源于stack exchange,提问作者Morgan Bradford

