• 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

  • 相关阅读:
    103
    101
    102
    100
    ByteView和Sink
    二叉排序树删除、搜索、插入的迭代实现
    怎样就地反转单链表?
    有序单链表的合并
    有序数组的合并
    静态表之整型数组的插入、删除、查找
  • 原文地址:https://www.cnblogs.com/luciolu/p/13602154.html
Copyright © 2020-2023  润新知