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

.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();

问题分析

  1. 中间件顺序混乱:原代码中UseHttpsRedirection和重复的MapControllers被放在UseEndpoints之后,导致请求到达SignalR Hub前未经过正确的HTTPS重定向和CORS校验,还可能引发路由冲突。
  2. OPTIONS预检请求被拦截:405状态码说明SignalR的协商预检请求未被正确处理,大概率是自定义中间件(如JwtAuthenticationBehaviour)提前拦截了OPTIONS请求,未让其通过CORS中间件处理。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 19:54:52