.NET Core 6.0 SignalR本地正常部署后遇CORS错误求助
SignalR部署后CORS错误排查与解决
问题描述
本地运行SignalR实时消息功能正常,部署后触发CORS错误:
8DUpdate.html:1 从源“http://localhost:8080”访问“https://test.test.test.com/chatHub/negotiate?negotiateVersion=1”时被CORS策略阻止:预检请求未通过访问控制检查,当请求的凭证模式为“include”时,响应中的“Access-Control-Allow-Origin”头值不能为通配符“*”。预检请求详情:请求URL为https://test.test.test.com/chatHub/negotiate?negotiateVersion=1,请求方法为OPTIONS,状态码405 Not Allowed。
已尝试多种CORS配置(允许特定源、所有源),怀疑中间件顺序错误,原Program.cs代码如下:
using System.IdentityModel.Tokens.Jwt; using DotNetSmartLogger; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.OpenApi.Models; using NLog; using NLog.Web; using Quality.EightD.API; using Quality.EightD.API.Behaviour; using Quality.EightD.API.ChatHub; var builder = WebApplication.CreateBuilder(args); // QA Appsetting testing //builder.Configuration // .AddJsonFile("appsettings.Development.json", optional: true, reloadOnChange: true); // Add services to the container. builder .Services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; }); builder .Services.AddSignalR(hubOptions => { hubOptions.EnableDetailedErrors = true; hubOptions.KeepAliveInterval = TimeSpan.FromSeconds(15); // Reduce from 1 minute hubOptions.ClientTimeoutInterval = TimeSpan.FromSeconds(30); // Reduce from 2 minutes hubOptions.HandshakeTimeout = TimeSpan.FromSeconds(15); // Add handshake timeout hubOptions.MaximumReceiveMessageSize = 102400; // Increased to 100KB hubOptions.StreamBufferCapacity = 10; }) .AddJsonProtocol(options => { options.PayloadSerializerOptions.PropertyNamingPolicy = null; }); builder.Services.AddCors(options => { options.AddPolicy( "CORSPolicy", builder => { builder .WithOrigins("http://localhost:8080", "https://test.test.test.com") .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); } ); }); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.ConfigureRepositories(); builder.Services.AddSwaggerGen(setup => { // Include 'SecurityScheme' to use JWT Authentication var jwtSecurityScheme = new OpenApiSecurityScheme { Scheme = "bearer", BearerFormat = "JWT", Name = "JWT Authentication", In = ParameterLocation.Header, Type = SecuritySchemeType.Http, Description = "Put **_ONLY_** your JWT Bearer token on textbox below!", Reference = new OpenApiReference { Id = JwtBearerDefaults.AuthenticationScheme, Type = ReferenceType.SecurityScheme, }, }; setup.SwaggerDoc("v1", new OpenApiInfo { Title = "Quality.EightD.API", Version = "v1" }); setup.AddSecurityDefinition(jwtSecurityScheme.Reference.Id, jwtSecurityScheme); setup.AddSecurityRequirement( new OpenApiSecurityRequirement { { jwtSecurityScheme, Array.Empty<string>() } } ); }); // Configure smart logger builder.Services.AddLogging(loggingBuilder => { loggingBuilder.ClearProviders(); loggingBuilder.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace); loggingBuilder.AddNLogWeb(); }); // Configure Kestrel server options builder.Services.Configure<KestrelServerOptions>(options => { options.Limits.MaxRequestBodySize = long.MaxValue; // if don't set default value is: 30 MB }); // Configure form options builder.Services.Configure<FormOptions>(o => { o.ValueLengthLimit = int.MaxValue; o.MultipartBodyLengthLimit = int.MaxValue; o.MultipartBoundaryLengthLimit = int.MaxValue; o.MultipartHeadersCountLimit = int.MaxValue; o.MultipartHeadersLengthLimit = int.MaxValue; o.BufferBodyLengthLimit = int.MaxValue; o.BufferBody = true; o.ValueCountLimit = int.MaxValue; }); builder.Services.AddHttpContextAccessor(); // Configure JWT Authentication builder .Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Events = new JwtBearerEvents { OnMessageReceived = context => { var accessToken = context.Request.Query["access_token"]; var path = context.HttpContext.Request.Path; if ( ( !string.IsNullOrEmpty(accessToken) || context.Request.Headers.ContainsKey("Authorization") ) && path.StartsWithSegments("/chatHub") ) { context.Token = accessToken; } return Task.CompletedTask; }, }; options.RequireHttpsMetadata = false; // For development - set to true in production options.SaveToken = true; }); var app = builder.Build(); // Configure Swagger app.UseSwagger(c => { c.RouteTemplate = "eightd-api-svc/{documentName}/swagger.json"; }); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/eightd-api-svc/v1/swagger.json", "API Service V1"); c.RoutePrefix = "eightd-api-svc"; c.ConfigObject.AdditionalItems["syntaxHighlight"] = new Dictionary<string, object> { ["activated"] = false, }; }); LogManager.Configuration.Variables["logPath"] = builder.Configuration.GetValue<string>("logPath"); // Update the order of middleware // First, essential middleware app.UseRouting(); app.UseCors("CORSPolicy"); app.UseAuthentication(); app.UseAuthorization(); // Add buffering middleware early in the pipeline app.Use( async (context, next) => { // Enable buffering for all requests context.Request.EnableBuffering(); await next(); } ); // Then your custom middleware app.UseMiddleware<JwtAuthenticationBehaviour>(); app.UseMiddleware<ResponseMiddleware>(); app.UseMiddleware<NLogRequestPostedBodyMiddleware>(); app.UseMiddleware<SmartLoggingMiddleware>(); app.UseMiddleware<ExceptionHandlingMiddleware>(); // Finally endpoints app.UseEndpoints(endpoints => { endpoints.MapControllers(); endpoints.MapHub<ChatHub>("/chatHub"); }); app.UseHttpsRedirection(); app.MapControllers(); app.Run();
问题分析
- 中间件顺序混乱:原代码中
UseHttpsRedirection和重复的MapControllers被放在UseEndpoints之后,导致请求到达SignalR Hub前未经过正确的HTTPS重定向和CORS校验,还可能引发路由冲突。 - OPTIONS预检请求被拦截:405状态码说明SignalR的协商预检请求未被正确处理,大概率是自定义中间件(如
JwtAuthenticationBehaviour)提前拦截了OPTIONS请求,未让其通过CORS中间件处理。 - CORS策略应用时机错误:CORS中间件需在路由、认证之后,自定义中间件和端点映射之前执行,确保所有请求(包括SignalR的协商请求)都能触发CORS校验。
解决方案
1. 调整中间件顺序
- 将
UseHttpsRedirection移至最前端,确保所有请求先完成HTTPS重定向; - 移除重复的
MapControllers,统一在UseEndpoints中配置路由; - 确保
UseEndpoints是最后一个端点配置中间件,放在所有自定义中间件之后。
2. 放行OPTIONS请求
在JwtAuthenticationBehaviour中间件中添加逻辑,直接放行OPTIONS请求,避免拦截预检请求:
public async Task InvokeAsync(HttpContext context) { // 放行OPTIONS预检请求 if (context.Request.Method == HttpMethod.Options.Method) { context.Response.StatusCode = StatusCodes.Status200OK; return; } // 原有认证逻辑 await _next(context); }
3. 修改后的完整Program.cs代码
using System.IdentityModel.Tokens.Jwt; using DotNetSmartLogger; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.OpenApi.Models; using NLog; using NLog.Web; using Quality.EightD.API; using Quality.EightD.API.Behaviour; using Quality.EightD.API.ChatHub; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder .Services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; }); builder .Services.AddSignalR(hubOptions => { hubOptions.EnableDetailedErrors = true; hubOptions.KeepAliveInterval = TimeSpan.FromSeconds(15); hubOptions.ClientTimeoutInterval = TimeSpan.FromSeconds(30); hubOptions.HandshakeTimeout = TimeSpan.FromSeconds(15); hubOptions.MaximumReceiveMessageSize = 102400; hubOptions.StreamBufferCapacity = 10; }) .AddJsonProtocol(options => { options.PayloadSerializerOptions.PropertyNamingPolicy = null; }); builder.Services.AddCors(options => { options.AddPolicy( "CORSPolicy", policyBuilder => { policyBuilder .WithOrigins("http://localhost:8080", "https://test.test.test.com") .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); } ); }); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.ConfigureRepositories(); builder.Services.AddSwaggerGen(setup => { var jwtSecurityScheme = new OpenApiSecurityScheme { Scheme = "bearer", BearerFormat = "JWT", Name = "JWT Authentication", In = ParameterLocation.Header, Type = SecuritySchemeType.Http, Description = "Put **_ONLY_** your JWT Bearer token on textbox below!", Reference = new OpenApiReference { Id = JwtBearerDefaults.AuthenticationScheme, Type = ReferenceType.SecurityScheme, }, }; setup.SwaggerDoc("v1", new OpenApiInfo { Title = "Quality.EightD.API", Version = "v1" }); setup.AddSecurityDefinition(jwtSecurityScheme.Reference.Id, jwtSecurityScheme); setup.AddSecurityRequirement( new OpenApiSecurityRequirement { { jwtSecurityScheme, Array.Empty<string>() } } ); }); // Configure smart logger builder.Services.AddLogging(loggingBuilder => { loggingBuilder.ClearProviders(); loggingBuilder.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace); loggingBuilder.AddNLogWeb(); }); // Configure Kestrel server options builder.Services.Configure<KestrelServerOptions>(options => { options.Limits.MaxRequestBodySize = long.MaxValue; }); // Configure form options builder.Services.Configure<FormOptions>(o => { o.ValueLengthLimit = int.MaxValue; o.MultipartBodyLengthLimit = int.MaxValue; o.MultipartBoundaryLengthLimit = int.MaxValue; o.MultipartHeadersCountLimit = int.MaxValue; o.MultipartHeadersLengthLimit = int.MaxValue; o.BufferBodyLengthLimit = int.MaxValue; o.BufferBody = true; o.ValueCountLimit = int.MaxValue; }); builder.Services.AddHttpContextAccessor(); // Configure JWT Authentication builder .Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Events = new JwtBearerEvents { OnMessageReceived = context => { var accessToken = context.Request.Query["access_token"]; var path = context.HttpContext.Request.Path; if ( (!string.IsNullOrEmpty(accessToken) || context.Request.Headers.ContainsKey("Authorization")) && path.StartsWithSegments("/chatHub") ) { context.Token = accessToken; } return Task.CompletedTask; }, }; options.RequireHttpsMetadata = false; options.SaveToken = true; }); var app = builder.Build(); // Configure Swagger app.UseSwagger(c => { c.RouteTemplate = "eightd-api-svc/{documentName}/swagger.json"; }); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/eightd-api-svc/v1/swagger.json", "API Service V1"); c.RoutePrefix = "eightd-api-svc"; c.ConfigObject.AdditionalItems["syntaxHighlight"] = new Dictionary<string, object> { ["activated"] = false, }; }); LogManager.Configuration.Variables["logPath"] = builder.Configuration.GetValue<string>("logPath"); // 调整后的中间件顺序 app.UseHttpsRedirection(); // 提前到最前面,确保HTTPS重定向先执行 app.UseRouting(); app.UseCors("CORSPolicy"); // CORS在路由之后,认证之前 app.UseAuthentication(); app.UseAuthorization(); // 启用请求缓冲 app.Use(async (context, next) => { context.Request.EnableBuffering(); await next(); }); // 自定义中间件 app.UseMiddleware<JwtAuthenticationBehaviour>(); app.UseMiddleware<ResponseMiddleware>(); app.UseMiddleware<NLogRequestPostedBodyMiddleware>(); app.UseMiddleware<SmartLoggingMiddleware>(); app.UseMiddleware<ExceptionHandlingMiddleware>(); // 端点配置放在最后 app.UseEndpoints(endpoints => { endpoints.MapControllers(); endpoints.MapHub<ChatHub>("/chatHub"); }); app.Run();
验证要点
- 确认CORS策略中的源与前端实际请求源完全匹配(包括协议、域名、端口);
- 检查SignalR客户端是否开启了
withCredentials(对应后端的AllowCredentials()配置); - 确保部署环境中没有反向代理(如Nginx)覆盖CORS响应头。
内容的提问来源于stack exchange,提问作者Ishan
相关产品推荐
相关产品推荐

