如何在ASPNET.Core Web应用程序中发送带有CORS标头的HTTP 4xx-5xx响应?

6
我有一个标准的ASP.NET Core 2 Web应用程序,用作REST/WebApi。对于我的其中一个端点,当用户提供错误的搜索/筛选查询字符串参数时,我返回一个HTTP 400
在POSTMAN中运行得很好。但是当我尝试使用我的SPA应用程序进行测试(实际上现在跨域,并因此进行CORS请求),我在Chrome中失败了。
当对返回HTTP 200响应的端点进行CORS请求时,一切都正常。
看起来我的错误处理没有考虑CORS的事情(即没有添加任何CORS头),并且没有包括那个。 我猜测我弄乱了响应负载管道的东西。 问:是否有一种方法可以在自定义错误处理程序中返回任何CORS头信息,而不是硬编码头信息,而是使用在Startup.cs中设置的头信息? 伪代码...
public void ConfigureServices(IServiceCollection services)
{
    ... snip ...

    services.AddMvcCore()
        .AddAuthorization()
        .AddFormatterMappings()
        .AddJsonFormatters(options =>
        {
            options.ContractResolver = new CamelCasePropertyNamesContractResolver();
            options.Formatting = Formatting.Indented;
            options.DateFormatHandling = DateFormatHandling.IsoDateFormat;
            options.NullValueHandling = NullValueHandling.Ignore;
            options.Converters.Add(new StringEnumConverter());
        })
        .AddCors(); // REF: https://learn.microsoft.com/en-us/aspnet/core/security/cors#setting-up-cors

    ... snip ...
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    ... snip ...

    app.UseExceptionHandler(options => options.Run(async httpContext => await ExceptionResponseAsync(httpContext, true)));

    app.UseCors(builder => builder//.WithOrigins("http://localhost:52383", "http://localhost:49497")
                                .AllowAnyOrigin()
                                .AllowAnyHeader()
                                .AllowAnyMethod());

    ... snip ...
}

private static async Task ExceptionResponseAsync(HttpContext httpContext, bool isDevelopmentEnvironment)
{
    var exceptionFeature = httpContext.Features.Get<IExceptionHandlerPathFeature>();
    if (exceptionFeature == null)
    {
        // An unknow and unhandled exception occured. So this is like a fallback.
        exceptionFeature = new ExceptionHandlerFeature
        {
            Error = new Exception("An unhandled and unexpected error has occured. Ro-roh :~(.")
        };
    }

    await ConvertExceptionToJsonResponseAsyn(exceptionFeature,
                                                httpContext.Response, 
                                                isDevelopmentEnvironment);
}

private static Task ConvertExceptionToJsonResponseAsyn(IExceptionHandlerPathFeature exceptionFeature,
    HttpResponse response,
    bool isDevelopmentEnvironment)
{
    if (exceptionFeature == null)
    {
        throw new ArgumentNullException(nameof(exceptionFeature));
    }

    if (response == null)
    {
        throw new ArgumentNullException(nameof(response));
    }

    var exception = exceptionFeature.Error;
    var includeStackTrace = false;
    var statusCode = HttpStatusCode.InternalServerError;
    var error = new ApiError();

    if (exception is ValidationException)
    {
        statusCode = HttpStatusCode.BadRequest;
        foreach(var validationError in ((ValidationException)exception).Errors)
        {
            error.AddError(validationError.PropertyName, validationError.ErrorMessage);
        }
    }
    else
    {
        // Final fallback.
        includeStackTrace = true;
        error.AddError(exception.Message);
    }

    if (includeStackTrace &&
        isDevelopmentEnvironment)
    {
        error.StackTrace = exception.StackTrace;
    }

    var json = JsonConvert.SerializeObject(error, JsonSerializerSettings);
    response.StatusCode = (int)statusCode;
    response.ContentType = JsonContentType;
    // response.Headers.Add("Access-Control-Allow-Origin", "*"); <-- Don't want to hard code this.
    return response.WriteAsync(json);
}

祝愿大家好运!

1个回答

6
ExceptionHandler 中间件中,Response 在传递到你自己的中间件函数之前被清除,如 source 中所示。
try
{
    await _next(context);
}
catch (Exception ex)
{
    // ...
    context.Response.Clear();

    // ...
    await _options.ExceptionHandler(context);

    // ..
}

当然,这意味着任何可能已针对CORS设置的响应标头也被清除了。
下面的代码插入到通用CORS系统中,我相信它似乎大部分满足您的要求,即可以使用从ConfigureServices配置:
var corsService = httpContext.RequestServices.GetService<ICorsService>();
var corsPolicyProvider = httpContext.RequestServices.GetService<ICorsPolicyProvider>();
var corsPolicy = await corsPolicyProvider.GetPolicyAsync(httpContext, null);

corsService.ApplyResult(
    corsService.EvaluatePolicy(httpContext, corsPolicy),
    httpContext.Response);
GetPolicyAsync函数将策略名称作为第二个参数传入,如果该参数为null(如本示例中),则使用默认策略(如果已设置)。示例代码未包含空值检查或其他内容,以保持重点,但此方法在我构建的测试项目中有效。此方法受到Microsoft.AspNetCore.Mvc.Cors中CorsAuthorizationFilter源代码的强烈影响。编辑:示例代码中未使用命名策略,但您可以使用以下代码切换到命名策略。
.AddCors(corsOptions => corsOptions.AddPolicy(
    "Default",
    corsPolicyBuilder => corsPolicyBuilder
        .AllowAnyOrigin()
        .AllowAnyHeader()
        .AllowAnyMethod()));

这里使用了 AddPolicy - 在评论中提到了 AddDefaultPolicy,但看起来它不在当前版本中,因此还不能使用。通过上述更改,您只需这样调用 UseCors

app.UseCors("Default");

最后一个更改是在异常处理代码中更新为以下内容:
await corsPolicyProvider.GetPolicyAsync(httpContext, "Default");

你最好使用某种常量字符串,特别是因为它很可能全部都在同一个文件中运行。这里的主要更改是不再尝试使用默认命名策略,因为我正在查看GitHub上尚未发布的当前版本的源代码。

哇!太好了,很棒的发现!我尝试了 corsPolicy = await corsPolicyProvider.GetPolicyAsync(httpContext, null); 这一部分,但它一直返回 null(导致 ApplyResult 崩溃)。我没有手动设置任何策略名称,我认为这意味着代码默认使用 DefaultPolicyName ... 因此将 null 传递给 GetPolicyAsync 应该会使用默认名称 ... 并从中解析出来? - Pure.Krome
看起来你最终会到达这里,在这里你可以看到名称被设置为null而不是默认值。为了让它正常工作,看起来你可以在AddCors中使用AddDefaultPolicy而不是AddPolicy(假设你正在使用AddPolicy)。 - Kirk Larkin
1
嗯...我以为如果我没有添加策略名称(实际上我没有),那么它会默认使用默认策略名称?此外,我也找不到任何AddDefaultPolicy.... - Pure.Krome
已更新原帖并附上代码。我只是按照微软文档中的说明进行操作:https://learn.microsoft.com/en-us/aspnet/core/security/cors#setting-up-cors - Pure.Krome
1
有趣的是 - CORS 存储库中已经存在这个问题。参考:https://github.com/aspnet/CORS/issues/90 我也向该问题添加了一些更多的信息。 - Pure.Krome
有没有类似的解决方案适用于ASP.NET? - yBother

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接