在 ASP.NET Core 中启用跨源请求 (CORS) | Microsoft Learn

来源: 在 ASP.NET Core 中启用跨源请求 (CORS) | Microsoft Learn

作者:Rick Anderson 和 Kirk Larkin

本文介绍如何在 ASP.NET Core 应用中启用 CORS。

浏览器安全性可防止网页向不处理网页的域发送请求。 此限制称为同域策略。 同域策略可防止恶意站点从另一站点读取敏感数据。 有时,你可能希望允许其他网站向自己的应用发出跨源请求。 有关详细信息,请参阅 Mozilla CORS 文章

跨源资源共享 (CORS):

  • 是一种 W3C 标准,允许服务器放宽同源策略。
  • 不是安全功能,CORS 放松了安全限制。 允许 CORS 并不会使 API 更安全。 有关详细信息,请参阅 CORS 的工作原理
  • 允许服务器显式允许某些跨源请求,同时拒绝其他请求。
  • 比早期技术(如 JSONP)更安全、更灵活。

查看或下载示例代码如何下载

同源

如果两个 URL 具有相同的方案、主机和端口 (RFC 6454),则它们同源。

这两个 URL 同源:

  • https://example.com/foo.html
  • https://example.com/bar.html

这些 URL 的源与前两个 URL 不同:

  • https://example.net:不同的域
  • https://www.example.com/foo.html:不同的子域
  • http://example.com/foo.html:不同的方案
  • https://example.com:9000/foo.html:不同的端口

启用 CORS

有三种方法可以启用 CORS:

将 [EnableCors] 属性与命名策略一起使用可在限制支持 CORS 的终结点方面提供最佳控制。

 警告

UseCors 必须按正确的顺序调用。 有关详细信息,请参阅中间件顺序。 例如,使用 UseResponseCaching 时,必须在 UseResponseCaching 之前调用 UseCors

以下各部分详细介绍了每种方法。

具有命名策略和中间件的 CORS

CORS 中间件处理跨源请求。 以下代码将 CORS 策略应用于具有指定源的所有应用终结点:

C#

var  MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      policy  =>
                      {
                          policy.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

// services.AddResponseCaching();

builder.Services.AddControllers();

var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors(MyAllowSpecificOrigins);

app.UseAuthorization();

app.MapControllers();

app.Run();

前面的代码:

  • 将策略名称设置为 _myAllowSpecificOrigins。 策略名称是任意的。
  • 调用 UseCors 扩展方法并指定 _myAllowSpecificOrigins CORS 策略。 UseCors 添加 CORS 中间件。 对 UseCors 的调用必须放在 UseRouting 之后,但在 UseAuthorization 之前。 有关详细信息,请参阅中间件顺序
  • 使用 lambda 表达式调用 AddCors。 lambda 采用 CorsPolicyBuilder 对象。 本文稍后将介绍配置选项,如 WithOrigins
  • 为所有控制器终结点启用 _myAllowSpecificOrigins CORS 策略。 要将 CORS 策略应用于特定终结点,请参阅终结点路由
  • 使用响应缓存中间件时,请在 UseResponseCaching 之前调用 UseCors

对于终结点路由,CORS 中间件必须配置为在对 UseRouting 和 UseEndpoints 的调用之间执行。

有关测试与前面代码类似的代码的说明,请参阅测试 CORS

AddCors 方法调用将 CORS 服务添加到应用的服务容器:

C#

var  MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      policy  =>
                      {
                          policy.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

// services.AddResponseCaching();

builder.Services.AddControllers();

var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors(MyAllowSpecificOrigins);

app.UseAuthorization();

app.MapControllers();

app.Run();

有关详细信息,请参阅本文档中的 CORS 策略选项

可以链接 CorsPolicyBuilder 方法,如以下代码所示:

C#

var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(MyAllowSpecificOrigins,
                          policy =>
                          {
                              policy.WithOrigins("http://example.com",
                                                  "http://www.contoso.com")
                                                  .AllowAnyHeader()
                                                  .AllowAnyMethod();
                          });
});

builder.Services.AddControllers();

var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors(MyAllowSpecificOrigins);

app.UseAuthorization();

app.MapControllers();

app.Run();

注意:指定的 URL 不能包含尾部斜杠 (/)。 如果 URL 以 / 结尾,则比较返回 false,并且不返回任何标头。

 警告

UseCors 必须位于 UseRouting 之后和 UseAuthorization 之前。 这是为了确保 CORS 标头包含在已授权和未经授权的调用的响应中。

UseCors 和 UseStaticFiles 顺序

通常,在 UseCors 之前调用 UseStaticFiles。 使用 JavaScript 跨站点检索静态文件的应用必须在 UseStaticFiles 之前调用 UseCors

具有默认策略和中间件的 CORS

以下突出显示的代码启用默认 CORS 策略:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(
        policy =>
        {
            policy.WithOrigins("http://example.com",
                                "http://www.contoso.com");
        });
});

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.MapControllers();

app.Run();

前面的代码将默认 CORS 策略应用于所有控制器终结点。

通过终结点路由启用 Cors

对于终结点路由,可以使用 RequireCors 扩展方法集基于每个终结点启用 CORS:

C#

var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      policy =>
                      {
                          policy.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

builder.Services.AddControllers();
builder.Services.AddRazorPages();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/echo",
        context => context.Response.WriteAsync("echo"))
        .RequireCors(MyAllowSpecificOrigins);

    endpoints.MapControllers()
             .RequireCors(MyAllowSpecificOrigins);

    endpoints.MapGet("/echo2",
        context => context.Response.WriteAsync("echo2"));

    endpoints.MapRazorPages();
});

app.Run();

在上述代码中:

  • app.UseCors 启用 CORS 中间件。 由于尚未配置默认策略,因此单独的 app.UseCors() 不会启用 CORS。
  • /echo 和控制器终结点允许使用指定策略的跨源请求。
  • /echo2 和 Razor Pages 终结点不允许跨源请求,因为未指定默认策略。

[DisableCors] 属性不会禁用终结点路由已使用 RequireCors 启用的 CORS。

ASP.NET Core 7.0 中,[EnableCors] 属性必须传递参数,否则系统会根据路由上的不明确匹配生成 ASP0023 警告。 ASP.NET Core 8.0 及更高版本不会生成 ASP0023 警告。

C#

[Route("api/[controller]")]
[ApiController]
public class TodoItems2Controller : ControllerBase
{
    // OPTIONS: api/TodoItems2/5
    [HttpOptions("{id}")]
    public IActionResult PreflightRoute(int id)
    {
        return NoContent();
    }

    // OPTIONS: api/TodoItems2 
    [HttpOptions]
    public IActionResult PreflightRoute()
    {
        return NoContent();
    }

    [HttpPut("{id}")]
    public IActionResult PutTodoItem(int id)
    {
        if (id < 1)
        {
            return BadRequest();
        }

        return ControllerContext.MyDisplayRouteInfo(id);
    }

    // [EnableCors] // Not needed as OPTIONS path provided.
    [HttpDelete("{id}")]
    public IActionResult MyDelete(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);

    // [EnableCors] //  Warning ASP0023 Route '{id}' conflicts with another action route.
    //                  An HTTP request that matches multiple routes results in an ambiguous
    //                  match error.
    [EnableCors("MyPolicy")] // Required for this path.
    [HttpGet]
    public IActionResult GetTodoItems() =>
        ControllerContext.MyDisplayRouteInfo();

    [HttpGet("{action}")]
    public IActionResult GetTodoItems2() =>
        ControllerContext.MyDisplayRouteInfo();

    [EnableCors("MyPolicy")]  // Required for this path.
    [HttpDelete("{action}/{id}")]
    public IActionResult MyDelete2(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);
}

有关测试与前面代码类似的代码的说明,请参阅使用 [EnableCors] 属性和 RequireCors 方法测试 CORS

使用属性启用 CORS

使用 [EnableCors] 属性启用 CORS,并仅对需要 CORS 的终结点应用命名策略可提供最佳控制。

[EnableCors] 属性提供了全局应用 CORS 的替代方法。 [EnableCors] 属性为所选终结点(而不是所有终结点)启用 CORS:

  • [EnableCors] 指定默认策略。
  • [EnableCors("{Policy String}")] 指定命名策略。

[EnableCors] 属性可应用于:

  • Razor Page PageModel
  • 控制器
  • 控制器操作方法

可将不同的策略应用于具有 [EnableCors] 属性的控制器、页面模型或操作方法。 如果将 [EnableCors] 属性应用于控制器、页面模型或操作方法,并且在中间件中启用了 CORS,则会应用两种策略。 建议不要合并策略。 使用 [EnableCors]属性或中间件,两者不能位于同一应用中。

以下代码对每种方法应用不同的策略:

C#

[Route("api/[controller]")]
[ApiController]
public class WidgetController : ControllerBase
{
    // GET api/values
    [EnableCors("AnotherPolicy")]
    [HttpGet]
    public ActionResult<IEnumerable<string>> Get()
    {
        return new string[] { "green widget", "red widget" };
    }

    // GET api/values/5
    [EnableCors("Policy1")]
    [HttpGet("{id}")]
    public ActionResult<string> Get(int id)
    {
        return id switch
        {
            1 => "green widget",
            2 => "red widget",
            _ => NotFound(),
        };
    }
}

以下代码创建两个 CORS 策略:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("Policy1",
        policy =>
        {
            policy.WithOrigins("http://example.com",
                                "http://www.contoso.com");
        });

    options.AddPolicy("AnotherPolicy",
        policy =>
        {
            policy.WithOrigins("http://www.contoso.com")
                                .AllowAnyHeader()
                                .AllowAnyMethod();
        });
});

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.MapControllers();

app.Run();

为了最精细地控制 CORS 请求的限制:

  • 将 [EnableCors("MyPolicy")] 与命名策略一起使用。
  • 不要定义默认策略。
  • 不要使用终结点路由

下一部分中的代码符合前面的列表。

有关测试与前面代码类似的代码的说明,请参阅测试 CORS

禁用 CORS

[DisableCors] 属性不会禁用终结点路由已启用的 CORS。

以下代码定义 CORS 策略 "MyPolicy"

C#

var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: "MyPolicy",
        policy =>
        {
            policy.WithOrigins("http://example.com",
                                "http://www.contoso.com")
                    .WithMethods("PUT", "DELETE", "GET");
        });
});

builder.Services.AddControllers();
builder.Services.AddRazorPages();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.UseEndpoints(endpoints => {
    endpoints.MapControllers();
    endpoints.MapRazorPages();
});

app.Run();

以下代码为 GetValues2 操作禁用 CORS:

C#

[EnableCors("MyPolicy")]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // GET api/values
    [HttpGet]
    public IActionResult Get() =>
        ControllerContext.MyDisplayRouteInfo();

    // GET api/values/5
    [HttpGet("{id}")]
    public IActionResult Get(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);

    // PUT api/values/5
    [HttpPut("{id}")]
    public IActionResult Put(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);


    // GET: api/values/GetValues2
    [DisableCors]
    [HttpGet("{action}")]
    public IActionResult GetValues2() =>
        ControllerContext.MyDisplayRouteInfo();

}

前面的代码:

有关测试前面代码的说明,请参阅测试 CORS

CORS 策略选项

本部分介绍可以在 CORS 策略中设置的各种选项:

AddPolicy 在 Program.cs 中调用。 对于某些选项,先阅读 CORS 的工作原理部分可能会有所帮助。

设置允许的源

AllowAnyOrigin:允许具有任何方案(http 或 https)的所有源的 CORS 请求。 AllowAnyOrigin 不安全,因为任何网站都可以向应用发出跨源请求。

 备注

指定 AllowAnyOrigin,且 AllowCredentials 是不安全的配置,可能会导致跨站点请求伪造。 当应用使用这两种方法进行配置时,CORS 服务将返回无效的 CORS 响应。

AllowAnyOrigin 影响预检请求和 Access-Control-Allow-Origin 头。 有关详细信息,请参阅预检请求部分。

SetIsOriginAllowedToAllowWildcardSubdomains:将策略的 IsOriginAllowed 属性设置为一个函数,当计算是否允许源时,此函数允许源匹配已配置的通配符域。

C#

var MyAllowSpecificOrigins = "_MyAllowSubdomainPolicy";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
        policy =>
        {
            policy.WithOrigins("https://*.example.com")
                .SetIsOriginAllowedToAllowWildcardSubdomains();
        });
});

builder.Services.AddControllers();

var app = builder.Build();

设置允许的 HTTP 方法

AllowAnyMethod

  • 允许任何 HTTP 方法:
  • 影响预检请求和 Access-Control-Allow-Methods 头。 有关详细信息,请参阅预检请求部分。

设置允许的请求头

要允许在称为作者请求头的 CORS 请求中发送特定头,请调用 WithHeaders 并指定允许的头:

C#

using Microsoft.Net.Http.Headers;

var MyAllowSpecificOrigins = "_MyAllowSubdomainPolicy";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
       policy =>
       {
           policy.WithOrigins("http://example.com")
                  .WithHeaders(HeaderNames.ContentType, "x-custom-header");
       });
});

builder.Services.AddControllers();

var app = builder.Build();

要允许所有 作者请求头,请调用 AllowAnyHeader

C#

var MyAllowSpecificOrigins = "_MyAllowSubdomainPolicy";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
        policy =>
        {
            policy.WithOrigins("https://*.example.com")
                   .AllowAnyHeader();
        });
});

builder.Services.AddControllers();

var app = builder.Build();

AllowAnyHeader 影响预检请求和 Access-Control-Request-Headers 头。 有关详细信息,请参阅预检请求部分。

仅当在 Access-Control-Request-Headers 中发送的头与 WithHeaders 中所述的头完全匹配时,才能与 WithHeaders 指定的特定头匹配 CORS 中间件策略。

例如,考虑按如下方式配置的应用:

C#

app.UseCors(policy => policy.WithHeaders(HeaderNames.CacheControl));

CORS 中间件拒绝具有以下请求头的预检请求,因为 Content-Language (HeaderNames.ContentLanguage) 未列在 WithHeaders 中:

Access-Control-Request-Headers: Cache-Control, Content-Language

应用返回 200 OK 响应,但不发回 CORS 头。 因此,浏览器不会尝试跨源请求。

设置公开的响应头

默认情况下,浏览器不会向应用公开所有响应头。 有关详细信息,请参阅 W3C 跨源资源共享(术语):简单响应头

默认情况下可用的响应头包括:

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

CORS 规范将这些头称为简单响应头。 要使其他头可用于应用,请调用 WithExposedHeaders

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MyExposeResponseHeadersPolicy",
        policy =>
        {
            policy.WithOrigins("https://*.example.com")
                   .WithExposedHeaders("x-custom-header");
        });
});

builder.Services.AddControllers();

var app = builder.Build();

跨源请求中的凭据

凭据需要在 CORS 请求中进行特殊处理。 默认情况下,浏览器不会使用跨源域请求发送凭据。 凭据包括 cookie 和 HTTP 身份验证方案。 要使用跨源请求发送凭据,客户端必须将 XMLHttpRequest.withCredentials 设置为 true

直接使用 XMLHttpRequest

JavaScript

var xhr = new XMLHttpRequest();
xhr.open('get', 'https://www.example.com/api/test');
xhr.withCredentials = true;

使用 JQuery

JavaScript

$.ajax({
  type: 'get',
  url: 'https://www.example.com/api/test',
  xhrFields: {
    withCredentials: true
  }
});

使用 Fetch API

JavaScript

fetch('https://www.example.com/api/test', {
    credentials: 'include'
});

服务器必须允许凭据。 要允许跨源凭据,请调用 AllowCredentials

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MyMyAllowCredentialsPolicy",
        policy =>
        {
            policy.WithOrigins("http://example.com")
                   .AllowCredentials();
        });
});

builder.Services.AddControllers();

var app = builder.Build();

HTTP 响应包含一个 Access-Control-Allow-Credentials 头,它告诉浏览器服务器允许跨源请求的凭据。

如果浏览器发送凭据,但响应不包含有效的 Access-Control-Allow-Credentials 头,则浏览器不会向应用公开响应,而且跨源请求会失败。

允许跨源凭据会带来安全风险。 另一个域中的网站可以在用户不知情的情况下代表用户将登录用户的凭据发送到应用。

CORS 规范还指出,如果存在 Access-Control-Allow-Credentials 头,则将源设置为 "*"(所有源)是无效的。

预检请求

对于某些 CORS 请求,浏览器会在发出实际请求之前发送额外的 OPTIONS 请求。 此请求称为预检请求。 如果满足以下所有条件,浏览器可以跳过预检请求:

  • 请求方法为 GET、HEAD 或 POST。
  • 应用不会设置 AcceptAccept-LanguageContent-LanguageContent-Type 或 Last-Event-ID 以外的请求头。
  • Content-Type 头(如果已设置)具有以下值之一:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

为客户端请求设置的请求头规则适用于应用通过在 XMLHttpRequest 对象上调用 setRequestHeader 设置的头。 CORS 规范将这些头称为作者请求头。 此规则不适用于浏览器可以设置的头,如 User-AgentHost 或 Content-Length

以下是与本文档测试 CORS 部分中的 [Put test] 按钮发出的预检请求类似的示例响应。

General:
Request URL: https://cors3.azurewebsites.net/api/values/5
Request Method: OPTIONS
Status Code: 204 No Content

Response Headers:
Access-Control-Allow-Methods: PUT,DELETE,GET
Access-Control-Allow-Origin: https://cors1.azurewebsites.net
Server: Microsoft-IIS/10.0
Set-Cookie: ARRAffinity=8f8...8;Path=/;HttpOnly;Domain=cors1.azurewebsites.net
Vary: Origin

Request Headers:
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Access-Control-Request-Method: PUT
Connection: keep-alive
Host: cors3.azurewebsites.net
Origin: https://cors1.azurewebsites.net
Referer: https://cors1.azurewebsites.net/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0

预检请求使用 HTTP OPTIONS 方法。 它可能包含以下头:

如果预检请求被拒绝,应用将返回 200 OK 响应,但不会设置 CORS 头。 因此,浏览器不会尝试跨源请求。 有关被拒绝的预检请求的示例,请参阅本文档的测试 CORS 部分。

使用 F12 工具时,控制台应用会显示类似于以下内容之一的错误,具体取决于浏览器:

  • Firefox:跨源请求被阻止:同源策略不允许读取 https://cors1.azurewebsites.net/api/TodoItems1/MyDelete2/5 上的远程资源。 (原因:CORS 请求不成功)。 了解详细信息
  • 基于 Chromium:从源“https://cors3.azurewebsites.net”中的“https://cors1.azurewebsites.net/api/TodoItems1/MyDelete2/5”提取的访问已被 CORS 策略阻止:对预检请求的响应未通过访问控制检查:请求的资源上不存在 Access-Control-Allow-Origin 头。 如果不透明响应满足你的需求,请将请求的模式设置为“no-cors”以提取禁用 CORS 的资源。

要允许特定的头,请调用 WithHeaders

C#

using Microsoft.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MyAllowHeadersPolicy",
        policy =>
        {
        policy.WithOrigins("http://example.com")
                   .WithHeaders(HeaderNames.ContentType, "x-custom-header");
        });
});

builder.Services.AddControllers();

var app = builder.Build();

要允许所有 作者请求头,请调用 AllowAnyHeader

C#

using Microsoft.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MyAllowAllHeadersPolicy",
        policy =>
        {
            policy.WithOrigins("https://*.example.com")
                   .AllowAnyHeader();
        });
});

builder.Services.AddControllers();

var app = builder.Build();

浏览器设置 Access-Control-Request-Headers 的方式不一致。 如果:

  • 头设置为 "*" 以外的任何内容
  • 调用 AllowAnyHeader:至少包括 AcceptContent-Type 和 Origin,以及你想要支持的任何自定义头。

自动预检请求代码

通过以下方式应用 CORS 策略时:

  • 通过在 Program.cs 中调用 app.UseCors 在全局范围内应用。
  • 使用 [EnableCors] 属性。

ASP.NET Core 响应预检 OPTIONS 请求。

本文档的测试 CORS 部分演示了这种行为。

用于预检请求的 [HttpOptions] 属性

当使用适当的策略启用 CORS 时,ASP.NET Core 通常会自动响应 CORS 预检请求。

以下代码使用 [HttpOptions] 属性为 OPTIONS 请求创建终结点:

C#

[Route("api/[controller]")]
[ApiController]
public class TodoItems2Controller : ControllerBase
{
    // OPTIONS: api/TodoItems2/5
    [HttpOptions("{id}")]
    public IActionResult PreflightRoute(int id)
    {
        return NoContent();
    }

    // OPTIONS: api/TodoItems2 
    [HttpOptions]
    public IActionResult PreflightRoute()
    {
        return NoContent();
    }

    [HttpPut("{id}")]
    public IActionResult PutTodoItem(int id)
    {
        if (id < 1)
        {
            return BadRequest();
        }

        return ControllerContext.MyDisplayRouteInfo(id);
    }

有关测试上述代码的说明,请参阅使用 [EnableCors] 属性和 RequireCors 方法测试 CORS

设置预检过期时间

Access-Control-Max-Age 头指定对预检请求的响应可以缓存多长时间。 要设置此头,请调用 SetPreflightMaxAge

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MySetPreflightExpirationPolicy",
        policy =>
        {
            policy.WithOrigins("http://example.com")
                   .SetPreflightMaxAge(TimeSpan.FromSeconds(2520));
        });
});

builder.Services.AddControllers();

var app = builder.Build();

在终结点上启用 CORS

CORS 的工作原理

本节介绍 HTTP 消息级别的 CORS 请求中发生的情况。

  • CORS 不是一项安全功能。 CORS 是一种 W3C 标准,允许服务器放宽同源策略。
    • 例如,恶意行为者可能对你的网站使用跨站脚本 (XSS),并向其启用了 CORS 的网站执行跨站请求以窃取信息。
  • 允许 CORS 并不会使 API 更安全。
    • 由客户端(浏览器)执行 CORS。 服务器执行请求并返回响应,客户端返回错误并阻止响应。 例如,以下任何工具都会显示服务器响应:
  • 通过这种方式,服务器允许浏览器执行跨源 XHR 或 Fetch API 请求,否则会被禁止。
    • 没有 CORS 的浏览器无法执行跨源请求。 在 CORS 之前,使用 JSONP 来规避此限制。 JSONP 不使用 XHR,它使用 <script> 标记来接收响应。 允许跨源加载脚本。

CORS 规范介绍了几个新的 HTTP 标头,它们支持跨源请求。 如果浏览器支持 CORS,则会自动为跨源请求设置这些头。 启用 CORS 不需要自定义 JavaScript 代码。

已部署示例上的 PUT test 按钮

以下是从 Values 测试按钮到 https://cors1.azurewebsites.net/api/values 的跨源请求示例。 Origin 头:

  • 提供发出请求的网站的域。
  • 是必需项,并且必须与主机不同。

通用头

Request URL: https://cors1.azurewebsites.net/api/values
Request Method: GET
Status Code: 200 OK

响应头

Content-Encoding: gzip
Content-Type: text/plain; charset=utf-8
Server: Microsoft-IIS/10.0
Set-Cookie: ARRAffinity=8f...;Path=/;HttpOnly;Domain=cors1.azurewebsites.net
Transfer-Encoding: chunked
Vary: Accept-Encoding
X-Powered-By: ASP.NET

请求标头

Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Host: cors1.azurewebsites.net
Origin: https://cors3.azurewebsites.net
Referer: https://cors3.azurewebsites.net/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0 ...

在 OPTIONS 请求中,服务器在响应中设置 Access-Control-Allow-Origin: {allowed origin} 响应头。 例如,已部署示例Delete [EnableCors] 按钮 OPTIONS 请求包含以下头:

通用头

Request URL: https://cors3.azurewebsites.net/api/TodoItems2/MyDelete2/5
Request Method: OPTIONS
Status Code: 204 No Content

响应头

Access-Control-Allow-Headers: Content-Type,x-custom-header
Access-Control-Allow-Methods: PUT,DELETE,GET,OPTIONS
Access-Control-Allow-Origin: https://cors1.azurewebsites.net
Server: Microsoft-IIS/10.0
Set-Cookie: ARRAffinity=8f...;Path=/;HttpOnly;Domain=cors3.azurewebsites.net
Vary: Origin
X-Powered-By: ASP.NET

请求标头

Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: DELETE
Connection: keep-alive
Host: cors3.azurewebsites.net
Origin: https://cors1.azurewebsites.net
Referer: https://cors1.azurewebsites.net/test?number=2
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0

在前面的响应头中,服务器在响应中设置了 Access-Control-Allow-Origin 头。 此头的 https://cors1.azurewebsites.net 值与请求中的 Origin 头一致。

如果调用 AllowAnyOrigin,则返回通配符值 Access-Control-Allow-Origin: *。 AllowAnyOrigin 允许任何源。

如果响应不包含 Access-Control-Allow-Origin 头,则跨源请求失败。 具体来说,浏览器不允许该请求。 即使服务器返回成功的响应,浏览器也不会将响应提供给客户端应用。

HTTP 重定向到 HTTPS 会导致 CORS 预检请求出现 ERR_INVALID_REDIRECT

UseHttpsRedirection 使用 HTTP(但重定向到 HTTPS)对终结点进行的请求失败,并返回 ERR_INVALID_REDIRECT on the CORS preflight request

API 项目可以拒绝 HTTP 请求,而不是使用 UseHttpsRedirection 将请求重定向到 HTTPS。

IIS 中的 CORS

部署到 IIS 时,如果服务器未配置为允许匿名访问,则 CORS 必须在 Windows 身份验证之前运行。 要支持此方案,需要为应用安装和配置 IIS CORS 模块

测试 CORS

示例下载包含测试 CORS 的代码。 请参阅如何下载。 该示例是一个 API 项目,其中添加了 Razor Pages:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: "MyPolicy",
        policy =>
        {
            policy.WithOrigins("http://example.com",
                    "http://www.contoso.com",
                    "https://cors1.azurewebsites.net",
                    "https://cors3.azurewebsites.net",
                    "https://localhost:44398",
                    "https://localhost:5001")
                .WithMethods("PUT", "DELETE", "GET");
        });
});

builder.Services.AddControllers();
builder.Services.AddRazorPages();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.MapControllers();
app.MapRazorPages();

app.Run();

 警告

WithOrigins("https://localhost:<port>"); 应仅用于测试类似于下载示例代码的示例代码。

以下 ValuesController 提供了用于测试的终结点:

C#

[EnableCors("MyPolicy")]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // GET api/values
    [HttpGet]
    public IActionResult Get() =>
        ControllerContext.MyDisplayRouteInfo();

    // GET api/values/5
    [HttpGet("{id}")]
    public IActionResult Get(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);

    // PUT api/values/5
    [HttpPut("{id}")]
    public IActionResult Put(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);


    // GET: api/values/GetValues2
    [DisableCors]
    [HttpGet("{action}")]
    public IActionResult GetValues2() =>
        ControllerContext.MyDisplayRouteInfo();

}

MyDisplayRouteInfo 由 Rick.Docs.Samples.RouteInfo NuGet 包提供,会显示路由信息。

使用以下方法之一测试上述示例代码:

  • 使用 https://cors3.azurewebsites.net/ 上部署的示例应用。 无需下载示例。
  • 使用 https://localhost:5001 的默认 URL 运行带有 dotnet run 的示例。
  • 从 Visual Studio 运行示例,并针对 https://localhost:44398 的 URL 将端口设置为 44398。

使用带有 F12 工具的浏览器:

  • 选择“值”按钮并在“网络”选项卡中查看头。
  • 选择“PUT test”按钮。 有关显示 OPTIONS 请求的说明,请参阅显示 OPTIONS 请求。 PUT test 会创建两个请求,一个 OPTIONS 预检请求和一个 PUT 请求。
  • 选择此 GetValues2 [DisableCors] 按钮可触发失败的 CORS 请求。 如文档中所述,响应返回 200 成功,但没有发出 CORS 请求。 选择“控制台”选项卡可查看 CORS 错误。 根据浏览器的不同,将显示类似于以下内容的错误:

    从源 'https://cors3.azurewebsites.net' 中的 'https://cors1.azurewebsites.net/api/values/GetValues2' 提取的访问已被 CORS 策略阻止:请求的资源上不存在 Access-Control-Allow-Origin 头。 如果不透明响应满足你的需求,请将请求的模式设置为“no-cors”以提取禁用 CORS 的资源。

启用了 CORS 的终结点可以使用 curlFiddler 或 Postman 等工具进行测试。 使用工具时,Origin 头指定的请求源必须与接收请求的主机不同。 如果根据 Origin 头的值,请求未跨源,则:

  • 无需 CORS 中间件来处理请求。
  • 不会在响应中返回 CORS 头。

以下命令使用 curl 发出包含信息的 OPTIONS 请求:

Bash

curl -X OPTIONS https://cors3.azurewebsites.net/api/TodoItems2/5 -i

使用 [EnableCors] 属性和 RequireCors 方法测试 CORS

请考虑以下代码,该代码使用终结点路由,通过 RequireCors 按终结点启用 CORS:

C#

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: "MyPolicy",
        policy =>
        {
            policy.WithOrigins("http://example.com",
                    "http://www.contoso.com",
                    "https://cors1.azurewebsites.net",
                    "https://cors3.azurewebsites.net",
                    "https://localhost:44398",
                    "https://localhost:5001")
                .WithMethods("PUT", "DELETE", "GET");
        });
});

builder.Services.AddControllers();
builder.Services.AddRazorPages();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/echo",
        context => context.Response.WriteAsync("echo"))
        .RequireCors("MyPolicy");

    endpoints.MapControllers();
    endpoints.MapRazorPages();
});

app.Run();

请注意,只有 /echo 终结点使用 RequireCors 来允许使用指定策略的跨域请求。 下面的控制器使用 [EnableCors] 属性来启用 CORS。

以下 TodoItems1Controller 提供了用于测试的终结点:

C#

[Route("api/[controller]")]
[ApiController]
public class TodoItems1Controller : ControllerBase 
{
    // PUT: api/TodoItems1/5
    [HttpPut("{id}")]
    public IActionResult PutTodoItem(int id) {
        if (id < 1) {
            return Content($"ID = {id}");
        }

        return ControllerContext.MyDisplayRouteInfo(id);
    }

    // Delete: api/TodoItems1/5
    [HttpDelete("{id}")]
    public IActionResult MyDelete(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);

    // GET: api/TodoItems1
    [HttpGet]
    public IActionResult GetTodoItems() =>
        ControllerContext.MyDisplayRouteInfo();

    [EnableCors("MyPolicy")]
    [HttpGet("{action}")]
    public IActionResult GetTodoItems2() =>
        ControllerContext.MyDisplayRouteInfo();

    // Delete: api/TodoItems1/MyDelete2/5
    [EnableCors("MyPolicy")]
    [HttpDelete("{action}/{id}")]
    public IActionResult MyDelete2(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);
}

从已部署示例测试页测试前面的代码。

Delete [EnableCors] 和 GET [EnableCors] 按钮成功,因为终结点具有 [EnableCors] 并会响应预检请求。 其他终结点失败。 GET 按钮失败,因为 JavaScript 发送:

JavaScript

 headers: {
      "Content-Type": "x-custom-header"
 },

以下 TodoItems2Controller 提供了类似的终结点,但包含响应 OPTIONS 请求的显式代码:

C#

[Route("api/[controller]")]
[ApiController]
public class TodoItems2Controller : ControllerBase
{
    // OPTIONS: api/TodoItems2/5
    [HttpOptions("{id}")]
    public IActionResult PreflightRoute(int id)
    {
        return NoContent();
    }

    // OPTIONS: api/TodoItems2 
    [HttpOptions]
    public IActionResult PreflightRoute()
    {
        return NoContent();
    }

    [HttpPut("{id}")]
    public IActionResult PutTodoItem(int id)
    {
        if (id < 1)
        {
            return BadRequest();
        }

        return ControllerContext.MyDisplayRouteInfo(id);
    }

    // [EnableCors] // Not needed as OPTIONS path provided.
    [HttpDelete("{id}")]
    public IActionResult MyDelete(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);

    // [EnableCors] //  Warning ASP0023 Route '{id}' conflicts with another action route.
    //                  An HTTP request that matches multiple routes results in an ambiguous
    //                  match error.
    [EnableCors("MyPolicy")] // Required for this path.
    [HttpGet]
    public IActionResult GetTodoItems() =>
        ControllerContext.MyDisplayRouteInfo();

    [HttpGet("{action}")]
    public IActionResult GetTodoItems2() =>
        ControllerContext.MyDisplayRouteInfo();

    [EnableCors("MyPolicy")]  // Required for this path.
    [HttpDelete("{action}/{id}")]
    public IActionResult MyDelete2(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);
}

从已部署示例的测试页测试前面的代码。 在“控制器”下拉列表中,选择“预检”,然后选择“设置控制器”。 对 TodoItems2Controller 终结点的所有 CORS 调用都成功。

赞(0) 打赏
分享到: 更多 (0)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏