CORS

CORS 概述

出于安全原因,浏览器遵循同源策略(Same-Origin Policy, SOP),阻止一些“跨域”请求。CORS (cross-origin sharing standard 跨域资源共享标准)新增了一些 HTTP 首部字段,请求首部字段声明了跨域请求的源的信息,响应首部字段声明了允许哪些源有权跨域请求服务器上的资源,最后由浏览器决定这段跨域是否被允许。

浏览器实际上并没有限制发起跨域请求,跨域请求被正常发送到服务器,服务器并没有对跨域请求做出限制,当 Request 得到 Response 之后,是由浏览器决定阻止不安全的跨域请求,但对于遵循 CORS 的请求,浏览器根据 Request 和 Response 的首部字段来判断这个跨域请求是否安全,以决定是否阻止该请求。

请求报文头和阻止跨域都由浏览器完成,浏览器需要跨域请求时,会自动加上跨域共享请求标头(cross-origin sharing request headers),所以实际前端开发人员再使用跨站点XMLHttpRequest功能时,不需要以编程方式设置任何跨域共享请求标头,而后端程序员需要在响应报头里声明共享请求标头。

CORS规范包含在WHATWGFetch Living标准中。较早的规范已发布为W3C建议书。

CORS 定义了9个新的报头:

Request Response
(CORS) Orgin Access-Control-Allow-Origin
(CORS-preflight) Access-Control-Request-Method Access-Control-Allow-Methods
(CORS-preflight) Access-Control-Request-Headers Access-Control-Allow-Headers
Access-Control-Allow-Credentials
Access-Control-Max-Age
Access-Control-Expose-Headers

CORS适用场景


预检 CORS preflight

一些请求会触发 CORS 预检请求,一些请求不会触发。

触发 CORS 预检的请求,浏览器跨域的时候会自动发送一个 OPTIONS 方法的请求,这个请求被称之为预检(preflight),预检会携带 CORS 相关的报头,如果预检的结果是没有访问这个服务器的权限,实际请求不会被发送,如果预检通过了,才会发送实际请求。

不需要预检的请求通常被叫做“简单请求”,简单请求直接发送实际请求,并且实际请求报头就会携带 CORS 相关报头,简单请求只需要携带 Orgin 报头就可以了。满足下面2个条件的请求就是简单请求。

(1) 请求方法是以下3个方法之一:
     HEAD
     GET
     POST
(2)HTTP的头信息不超出以下5个字段,且Content-Type只能以下3个值之一:
     Accept
     Accept-Language
     Content-Language
     Last-Event-ID
     Content-Type: application/x-www-form-urlencoded、 multipart/form-data、text/plain

为什么需要预检:

https://stackoverflow.com/questions/15381105/cors-what-is-the-motivation-behind-introducing-preflight-requests


简单请求

假设 yoursite.com 要跨域访问 lucio.cn,发起的是一个简单请求:

Client yoursite.com                  Server lucio.cn
  ------------------------------------------>  
  GET /path HTTP/1.1  
  Orgin: http://yoursite.com
  
  <------------------------------------------
                   HTTP/1.1 200 OK
                   Access-Control-Allow-Origin:http://yoursite.com

简单请求,客户端会在请求中声明 Orgin 报头,服务器的响应需要声明 Access-Control-Allow-Origin 报头,


浏览器对跨域的请求会自动加上 Orgin 报头,用来声明请求的“源”。而服务器的响应需要声明 Access-Control-Allow-Origin 报头,用来告知浏览器允许哪些“源”的跨域请求。

OrginAccess-Control-Allow-Origin 的内容一致则浏览器认为这是一个安全的跨域请求,但要求“一致”就有个问题,有时候有些浏览器发送的是 http://www.yoursite.com,那样会导致跨域失败。


一次失败的跨域请求

使用 XMLHttpRequest 跨域请求一个没有开启 CORS 的接口

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://lucio.cn/HttpTest/GetDisableCors');
xhr.send();

浏览器发起跨域请求并且接受到响应,但是响应报头没有声明 CORS 报头字段Access-Control-Allow-Origin,浏览器认为这是一个不安全的跨域请求,所以浏览器会阻止响应,并且报错。报错信息如下:

Access to XMLHttpRequest at 'https://lucio.cn/HttpTest/GetEnableCors' from origin 'https:/www.yoursite.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
GET https://localhost:44385/HttpTest/GetDisableCors net::ERR_FAILED

HTTP信息如下:

GET /HttpTest/GetDisableCors HTTP/1.1
Host: lucio.cn
Origin: http://yoursite.com
HTTP/1.1 200 OK

混合内容 Mixed content

除了服务器没有开启 CORS ,还有一种错误就是 OrginHTTPS 协议要跨域访问 HTTP 协议的目标,也是不允许的。报错信息如下:

Mixed Content: The page at 'https://yoursite.com' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint 'http://lucio.cn/HttpTest/GetDisableCors'. This request has been blocked; the content must be served over HTTPS.

https://developer.mozilla.org/zh-CN/docs/Security/MixedContent


Access-Control-Allow-Origin

Orgin:该请求报头声明客户端请求的“源”。 Orgin = scheme + hostname + port

Access-Control-Allow-Origin:该响应报头声明了服务器允许被哪些“源”访问,需要指定具体的“源”或者使用“*”通配符表示允许所有来源。

浏览器发现请求是跨域请求时,会自动加上 CORS 定义的报头,所以前端程序员不需要做什么。但后端需要添加响应报头字段。以 .NET MVC 为例:

public string GetEnableCors()
{
    // 服务器响应添加 Access-Control-Allow-Origin 报头,声明允许哪些源访问资源。
    HttpContext.Response.AppendHeader("Access-Control-Allow-Origin", "*");
    //HttpContext.Response.AppendHeader("Access-Control-Allow-Origin", "https://yoursite.com");
    return "ok";
}

完成服务器代码之后,再发起跨域请求,这个请求不会被浏览器阻止。

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://lucio.cn/HttpTest/GetEnableCors');
xhr.send();

使用浏览器的“开发者工具”查看 HTTP,我们发现响应报头多了一个 Access-Control-Allow-Origin 字段。

GET /HttpTest/GetEnableCors HTTP/1.1
Host: lucio.cn
Origin: http://yoursite.com
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *

需要预检的请求

发起的是一个非简单请求,浏览器会先发起一个 Option 方法的请求检测接口是否可以跨域,这称之为预检。预检不会携带实际信息,会携带 CORS 定义的报头。预检通过之后才会发起实际请求,实际请求会携带实际数据,但不会携带 CORS 报头。(和MDN上画的图有点不同)

浏览器发起的预检请求会自动携带3个请求报头:

  • Orgin
  • Access-Control-Request-Method
  • Access-Control-Request-Headers

服务器响应也应该声明对应的3个响应报头:

  • Access-Control-Allow-Origin
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Headers
Client yoursite.com                        Server licio.com
    ------------------------------------------>
    OPTION /path HTTP/1.1
    Orgin: http://yoursite.com
    Access-Control-Request-Method: POST
    Access-Control-Request-Headers: X-Say-Bye,Content-Type

    <------------------------------------------
                  HTTP/1.1 200 OK
                  Access-Control-Allow-Origin: http://yoursite.com
                  Access-Control-Allow-Methods: POST, GET OPTIONS
                  Access-Control-Allow-Headers: X-Say-Bye, Content-Type

    ------------------------------------------>
    GET /path HTTP/1.1
    Origin: http://yoursite.com
    X-Say-Bye: June
    Content-Type: application/xml

    <------------------------------------------
                  HTTP/1.1 200 OK
                  Access-Control-Allow-Origin: http://yoursite.com

一次失败的预检请求

使用 XMLHttpRequest 发起一个复杂请求,访问刚才定义的接口,该接口只声明了 Access-Control-Allow-Origin 报头。

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://lucio.cn/HttpTest/GetEnableCors');
xhr.setRequestHeader('X-Say-Bye', 'June');
xhr.send();

正如所料,报错了:自定义的请求报头是不被允许的。显然,光声明 Access-Control-Allow-Origin 还不够。

Access to XMLHttpRequest at 'http://lucio.cn/HttpTest/GetEnableCors' from origin 'https://yoursite.com' has been blocked by CORS policy: Request header field x-say-bye is not allowed by Access-Control-Allow-Headers in preflight response.
GET http://lucio.cn/HttpTest/GetEnableCors net::ERR_FAILED

Access-Control-Allow-Headers

Access-Control-Request-Headers:该请求报头用于预检请求,声明了实际请求会发送哪些HTTP报头。

Access-Control-Allow-Headers:该响应报头声明在进行实际跨域请求时允许携带的HTTP报头,用于响应预检请求,对于非简单请求,该报头是必须的。

public string GetNonsimpleCorsSayBye()
{
    HttpContext.Response.AppendHeader("Access-Control-Allow-Origin", "*");
    // 添加 Access-Control-Allow-Headers 请求头,指明了实际请求中允许携带的首部字段。
    HttpContext.Response.AppendHeader("Access-Control-Allow-Headers", "X-Say-Bye");
    return "GetNonsimpleCorsSayBye";
}

使用 XMLHttpRequestGetNonsimpleCorsSayBye 接口发起需要预检的请求。

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://lucio.cn/HttpTest/GetNonsimpleCorsSayBye');
xhr.setRequestHeader('X-Say-Bye', 'June');
xhr.send();

Access-Control-Allow-Methods

非简单请求,浏览器会自动添加一个 Access-Control-Request-Method 请求头部字段,对应的响应头部字段是 Access-Control-Allow-Methods ,由于它的默认值就是 “ GET, POST, HEAD”,所以我们并没有添加。

Access-Control-Request-Method:该请求报头用于预检请求,声明了实际请求的HTTP方法。

Access-Control-Allow-Methods:该响应报头声明实际请求允许的一种或多种方法,用于响应预检请求。

public string GetNonSimpleCors()
{
    // Access-Control-Allow-Origin 使用通配符“*”应该没啥太大的问题
    HttpContext.Response.AppendHeader("Access-Control-Allow-Origin", "*");
    // Access-Control-Allow-Headers 不建议使用通配符“*”
    HttpContext.Response.AppendHeader("Access-Control-Allow-Headers", "*");
    // Access-Control-Allow-Method: GET, POST, HEADER  其实就是默认值
    HttpContext.Response.AppendHeader("Access-Control-Allow-Method", "GET, POST, HEADER");
    return "GetNonSimpleCors";
}

此时Header使用通配符“*”声明,所以我们已经可以接受任意的headers了,但是不建议这么使用。

继续测试,发起一个请求测试一下:

var body = '<?xml version="1.0"?><message>Hello June</message>';

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://lucio.cn/HttpTest/GetNonSimpleCors');
// 添加一个自定义头部字段,这是非简单请求
xhr.setRequestHeader('X-Say-Bye', 'June');
// Content-Type: application/xml 也是非简单请求
xhr.setRequestHeader('Content-Type', 'application/xml');
xhr.send(body);

非简单请求会发起2次请求,在浏览器的“开发者工具”可以看到发起了2个请求(但是chrome只能看到一次实际请求,不知道怎么设置)。第一个是“预检”:

OPTIONS /HttpTest/GetNonSimpleCors HTTP/1.1
Host: lucio.cn
Access-Control-Request-Method: GET
Access-Control-Request-Headers: content-type,x-say-bye
Origin: http://yoursite.com
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Method: GET, POST, HEADER

第二个是实际请求:

GET /HttpTest/GetNonSimpleCors HTTP/1.1
Host: lucio.cn
X-Say-Bye: June
Content-Type: application/xml
Origin: http://yoursite.com
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Method: GET, POST, HEADER

.NET MVC Config

.NET的服务器可以在web.config的 <customHeaders> 节点内配置,这样每一个response header都会带有这些节点,而不用再每个方法单独使用 HttpContext.Response.AppendHeader 方法

<system.webServer>
   <httpProtocol>
     <customHeaders>
        <add name="Access-Control-Allow-Origin" value="*" />
        <add name="Access-Control-Allow-Headers" value="X-Say-Bye, Content-Type" />
        <add name="Access-Control-Allow-Methods" value="GET, POST, HEADER" />
     </customHeaders>
   </httpProtocol>
</system.webServer>

https://www.cnblogs.com/gdpw/p/9236661.html


带凭证的请求

默认情况下,跨站点 XMLHttpRequestFetch 调用中,浏览器“不”发送凭据。request 必须设置一个特定的标志 withCredentials 才会发送凭证,服务器需要声明 Access-Control-Allow-Credentials 头部字段表示允许跨域发送带凭证的请求。

Client yoursite.com                  Server lucio.cn
  ------------------------------------------>  
  GET /path HTTP/1.1  
  Orgin: http://yoursite.com
  Cookie: account=mongogorilla;level=admin
  
  <------------------------------------------
                   HTTP/1.1 200 OK
                   Access-Control-Allow-Origin: http://yoursite.com
                   Access-Control-Allow-Credentials: true

开始测试

首先创建一个携带 Cookie 的 Response。

public string GetEnableCorsSetCookie()
{
    // 设置允许跨域请求
    HttpContext.Response.AppendHeader("Access-Control-Allow-Origin", "*");

    // 设置 response 的 Cookie
    HttpContext.Response.SetCookie(new HttpCookie("account", "mongogorilla"));

    return "GetEnableCorsSetCookie";
}

然后通过3种方式访问这个资源:http://lucio.cn/httptext/GetEnableCorsSetCookie

  • 直接在浏览器的地址栏输入url访问这个资源。
  • http://lucio.cn 源下使用 XMLHttpRequest 访问这个资源。
  • 在其它源使用 XMLHttpRequest 跨源访问这个资源。
// 使用 XMLHttpRequest
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://lucio.cn/httptest/GetEnableCorsSetCookie');
xhr.send();

通过“开发者工具”查看 Request,发现只有跨源访问的时候,Request 头部一直都不携带 Cookie 字段。如果想要使用 XMLHttpRequest 跨源访问时候携带 Cookie 头部字段,需要设置 XMLHttpRequest 的属性 withCredentials 为 true。


withCredentials

withCredentials:指示了是否该使用类似 cookies, authorization headers (头部授权)或者TLS客户端证书这一类资格证书来创建一个跨站点访问控制(cross-site Access-Control)请求。在同一个站点下使用 withCredentials 属性是无效的。

// 使用 XMLHttpRequest
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://lucio.cn/httptest/GetEnableCorsSetCookie');
xhr.withCredentials = true;
xhr.send();

会出现1个警告和2个报错

cookie associated with a cross-site resource at http://lucio.cn/ was set without the `SameSite` attribute. It has been blocked, as Chrome now only delivers cookies with cross-site requests if they are set with `SameSite=None` and `Secure`. You can review cookies in developer tools under Application>Storage>Cookies and see more details at https://www.chromestatus.com/feature/5088147346030592 and https://www.chromestatus.com/feature/5633521622188032.

Access to XMLHttpRequest at 'http://lucio.cn/httptest/GetEnableCorsSetCookie' from origin 'http://yoursite.com' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

GET http://lucio.cn/httptest/GetEnableCorsSetCookie net::ERR_FAILED

先忽略这个警告,报错的大概意思是 withCredentials = true 的时候,Access-Control-Allow-Origin 不能是 “*” 通配符。所以我们修改这个报头声明。

HttpContext.Response.AppendHeader("Access-Control-Allow-Origin", "http://yoursite.com");

然后再次运行,又报错:

Access to XMLHttpRequest at 'http://lucio.cn/httptest/GetEnableCorsAllowCredentials' from origin 'http://yoursite.com' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

GET http://lucio.cn/httptest/GetEnableCorsAllowCredentials net::ERR_FAILED

报错大概意思是带凭证的跨域请求必须声明报头字段 Access-Control-Allow-Credentials = true

Access-Control-Allow-Credentials

Access-Control-Allow-Credentials:指示是否对所述请求的响应可以在被暴露credentials标记为真。当用作对预检请求的响应的一部分时,这表明是否可以使用凭据发出实际请求。

public string GetEnableCorsAllowCredentials()
{
    // 不太懂,为什么request一定要在response前面,不然设置response,request里也会有这个值。
    // 从 request 得到 Cookie
    var account = HttpContext.Request.Cookies["account"]?.Value;
    var origin = HttpContext.Request.Headers["origin"];
    // 声明允许跨域请求
    HttpContext.Response.AppendHeader("Access-Control-Allow-Origin", origin);
    // 声明允许跨域请求携带凭证
    HttpContext.Response.AppendHeader("Access-Control-Allow-Credentials", "true");

    // 设置 response 的 Cookie
    HttpContext.Response.Cookies.Add(new HttpCookie("account", "mongogorilla"));

    if (!string.IsNullOrWhiteSpace(account))
    {
        return "succeed!Cookie传送成功。" + account;
    }
    return "failure!Cookie传送失败";
}

再一次发起带 Cookie 的请求

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://lucio.cn/httptest/GetEnableCorsAllowCredentials');
xhr.withCredentials = true;
xhr.send();

发现运行成功,但此时浏览器一般都会发出一个警告,这时候不同的浏览器可能会有不同的结果,火狐会发出警告,但是 Request 会携带 Cookie,而 Chome 会发出警告,并且 Request 不会携带 Cookie

A cookie associated with a cross-site resource at http://lucio.cn/ was set without the `SameSite` attribute. It has been blocked, as Chrome now only delivers cookies with cross-site requests if they are set with `SameSite=None` and `Secure`. You can review cookies in developer tools under Application>Storage>Cookies and see more details at https://www.chromestatus.com/feature/5088147346030592 and https://www.chromestatus.com/feature/5633521622188032.

上面是Chrome的警告,大概意思是必须要 SameSite=NoneSecure,而我用的是老版本的 .NET MVC,并且域名没有 https ,所以这个2个属性不方便测试。因为Chrome必须要这2个属性才能跨域携带 Cookie,而火狐是虽然有警告,但是是可以携带 Cookie 的,所以只能使用火狐的“F12开发者工具”查看 http

GET /httptest/GetEnableCorsAllowCredentials HTTP/1.1
Host: lucio.cn
Origin: http://yoursite.com
Cookie: account=mongogorilla
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://yoursite.com
Access-Control-Allow-Credentials: true
Set-Cookie: account=mongogorilla; path=/


火狐对localhost地址不支持CORS,除此之外没有问题,不需要额外设置来开启CORS。(当前时间为2020年8月24日)

https://stackoverflow.com/questions/25504851/how-to-enable-cors-on-firefox/25507329#25507329

https://stackoverflow.com/questions/17088609/disable-firefox-same-origin-policy

原文地址:https://www.cnblogs.com/luciolu/p/13602154.html