跨域请求方案 - CORS

 提示:转载请注明原文链接

 本文链接:https://360us.net/article/31.html

CORS全称是Cross-Origin Resource Sharing(跨域资源共享),是W3C推荐的跨域请求方案。

同时也需要服务器端程序的配合。

作用主要是以下这些跨站http请求:

1、跨域的AJAX请求。

2、网络字体(Web Font),就是css里面 @font-face定义的字体,可以在服务器端配置哪些域允许跨站载入这些TrueType字体。

3、WebGL的纹理(texture)文件。

4、用HTML5的drawImage函数画到canvas里面的图片。


场景示例

简单的请求

一个简单的跨域请求像下面描述的那样:

1、只使用了GET, HEAD 或者POST。假如POST用来向服务器发送数据的话,HTTP头的Content-Type字段值是application/x-www-form-urlencoded, multipart/form-data,或者text/plain三个中的一个。

2、没有设置自定义的HTTP头,就是哪种以X-开头的,比如X-Modified。


例如域http://localhost的需要请求http://www.example.com的内容 :

我们可以很容易的在本地通过host文件来构造两个不同的域。

var xhr = new XMLHttpRequest();
var url = 'http://www.example.com/cors.php';

xhr.open('GET', url, true);
xhr.onreadystatechange = function(){
    if(xhr.readyState == 4 && xhr.status == 200) {
        alert(xhr.responseText);
    }
};
xhr.send();

浏览器发出和服务器响应的http头如下:

GET /cors.php HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:37.0) Gecko/20100101 Firefox/37.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://localhost/cors.html
Origin: http://localhost
Connection: keep-alive


HTTP/1.1 200 OK
Date: Thu, 23 Apr 2015 13:41:02 GMT
Server: Apache/2.4.9 (Win32) OpenSSL/1.0.1g PHP/5.5.11
X-Powered-By: PHP/5.5.11
Access-Control-Allow-Origin: http://localhost
Content-Length: 55
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: application/xml

请求头的Origin字段是说明当前请求是来自那个域。

响应头里面有个 Access-Control-Allow-Origin字段,Origin头和 Access-Control-Allow-Origin头展示了访问控制协议最简单的使用。

服务器返回 Access-Control-Allow-Origin: *意思是这台服务器上面的资源可以被任意域请求。

像上面那样,如果http://www.example.com只允许http://localhost跨域请求资源的话,要这样响应头:

Access-Control-Allow-Origin: http://localhost

还有一点就是Access-Control-Allow-Origin的值必须是请求头里Origin的值。


对应的服务器端代码如下(PHP为例):

<?php
if( $_SERVER['HTTP_ORIGIN'] == 'http://localhost' ) {
header('Access-Control-Allow-Origin: http://localhost');
    header('Content-type: application/xml');
    echo '<?xml version="1.0"?><person><name>Arun</name></person>';
}


Preflighted请求

和上面的简单请求不同,Preflighted请求是先用OPTIONS方法发送一个HTTP请求到目标域,目的是为了确保实际的请求可以安全发送。

一个Preflighted满足下列条件:

1、使用了GET, HEADPOST之外的HTTP方法。同样的,假如POST用来发送数据到服务器,请求头的Content-Type的值是application/x-www-form-urlencoded, multipart/form-data, 和text/plain三个之外的值。

比如用application/xml或者text/xml发送数据到服务器,那么这个请求就是Preflighted的。

2、请求设置了自定义的HTTP头。


一个示例:

var xhr = new XMLHttpRequest();
var url = 'http://www.example.com/cors.php';
var body = "<?xml version=\"1.0\"?><person><name>Arun</name></person>";

xhr.open('POST', url, true);
xhr.setRequestHeader("X-PINGOTHER", "pingpong");
xhr.setRequestHeader("Content-Type", "application/xml");
xhr.onreadystatechange = function(){
    if(xhr.readyState == 4 && xhr.status == 200) {
        alert(xhr.responseText);
    }
};
xhr.send(body);

上面的代码创建了一个包含xml数据,使用POST发送的请求,设置了一个自定义的请求头X-PINGOTHER: pingpong

因为这个使用了一个Content-Type值为application/xml的头,并且设置了一个自定义的请求头,所以这个请求是Preflighted的。


客户端和服务器通信如下:

OPTIONS /cors.php HTTP/1.1
Host: www.example.com
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Access-Control-Request-Method: POST
Origin: http://localhost
User-Agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36
Access-Control-Request-Headers: x-pingother, content-type
Accept: */*
Referer: http://localhost/cors.html
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8

HTTP/1.1 200 OK
Date: Thu, 23 Apr 2015 14:10:21 GMT
Server: Apache/2.4.9 (Win32) OpenSSL/1.0.1g PHP/5.5.11
X-Powered-By: PHP/5.5.11
Access-Control-Allow-Origin: http://localhost
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 1728000
Content-Length: 0
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/plain


POST /cors.php HTTP/1.1
Host: www.example.com
Connection: keep-alive
Content-Length: 55
Pragma: no-cache
Cache-Control: no-cache
X-PINGOTHER: pingpong
Origin: http://localhost
User-Agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36
Content-Type: application/xml
Accept: */*
Referer: http://localhost/cors.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8

HTTP/1.1 200 OK
Date: Thu, 23 Apr 2015 14:10:22 GMT
Server: Apache/2.4.9 (Win32) OpenSSL/1.0.1g PHP/5.5.11
X-Powered-By: PHP/5.5.11
Access-Control-Allow-Origin: http://localhost
Content-Length: 74
Keep-Alive: timeout=5, max=99
Connection: Keep-Alive
Content-Type: text/plain

上面第一个请求头是使用了OPTONS方法的preflight请求。浏览器是根据上面的那段js代码来决定是否需要发送这样一个请求的,服务器响应告诉给浏览器是否可以接受实际的请求。

第一个请求头里面还有另外两个头:

Access-Control-Request-Method: POST
Access-Control-Request-Headers: x-pingother, content-type

Access-Control-Request-Method头是通知服务器实际请求是一个POST请求。

Access-Control-Request-Headers头是通知服务器实际请求有一个X-PINGOTHER的自定义头。


第二个响应头是告诉浏览器POST请求和X-PINGOTHER头是可接受的。

看这段:

Access-Control-Allow-Origin: http://localhost
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER
Access-Control-Max-Age: 1728000

Access-Control-Allow-Methods是说POSTGETOPTIONS都是有效的可用来请求资源的HTTP方法。

Access-Control-Allow-Headers是说X-PINGOTHER是在实际的请求中可被接受的HTTP头。

Access-Control-Max-Age表示这次响应的数据在发送下一个不同的preflight请求前可以被缓存多久,单位是秒,这里1728000秒,在发送下一次不同的preflight请求之前可以被缓存20天。

我这里测试的结果时chrome41没有缓存,每次刷新都有OPTIONS发出,而firefox37是有缓存OPTIONS响应的,第一次发送过后,再刷新页面就不会再发OPTIONS请求了。


对应的服务器端代码(PHP为例):

if($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
    if($_SERVER['HTTP_ORIGIN'] == "http://localhost"){
    header('Access-Control-Allow-Origin: http://localhost');
    header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
    header('Access-Control-Allow-Headers: X-PINGOTHER, Content-Type');
    header('Access-Control-Max-Age: 1728000');
    header("Content-Length: 0");
    header("Content-Type: text/plain");
    
    }
    else{
    header("HTTP/1.1 403 Access Forbidden");
    header("Content-Type: text/plain");
    echo "You cannot repeat this request";
    }
}
elseif($_SERVER['REQUEST_METHOD'] == "POST"){
    if($_SERVER['HTTP_ORIGIN'] == 'http://localhost'){
            $postData = file_get_contents('php://input');
            //$document = simplexml_load_string($postData);
            
            // do something with POST data

            $ping = $_SERVER['HTTP_X_PINGOTHER'];
           
                       
            header('Access-Control-Allow-Origin: http://localhost');
            header('Content-Type: text/plain');
            echo 'Processed,',$ping,',',$postData;
    }
    else{
        die("POSTing Only Allowed from http://localhost");
    }
}
else {
    die("No Other Methods Allowed");
}


需要凭证的请求

默认情况下,跨域ajax请求浏览器是不会设置凭证信息的,当发起一个需要凭证的请求时,需要设置一个特殊的标志。

同样的http://localhost用一个带cookie的简单的GET请求跨域请求http://www.example.com域的资源。

js代码如下:

var xhr = new XMLHttpRequest();
var url = 'http://www.example.com/cors.php';

xhr.open('GET', url, true);
xhr.withCredentials = true;
xhr.onreadystatechange = function(){
    if(xhr.readyState == 4 && xhr.status == 200) {
        alert(xhr.responseText);
    }
};
xhr.send();


上面设置属性withCredentialstrue就是确保这个请求是带cookie的。默认的话是不带cookie的。

下面是浏览器服务器的请求响应头:

GET /cors.php HTTP/1.1
Host: www.example.com
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Origin: http://localhost
User-Agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36
Accept: */*
Referer: http://localhost/cors.html
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8
Cookie: pageAccess=2

HTTP/1.1 200 OK
Date: Thu, 23 Apr 2015 14:32:28 GMT
Server: Apache/2.4.9 (Win32) OpenSSL/1.0.1g PHP/5.5.11
X-Powered-By: PHP/5.5.11
Set-Cookie: pageAccess=3; expires=Sat, 23-May-2015 14:32:28 GMT; Max-Age=2592000
Access-Control-Allow-Origin: http://localhost
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 109
Keep-Alive: timeout=5, max=99
Connection: Keep-Alive
Content-Type: text/plain

请求头带了cookie,响应头里面包含Access-Control-Allow-Credentials: true

还有一点就是,这种请求服务器必须指定一个允许请求的域名,不能是*,否则会失败。

比如上面这个响应头, Access-Control-Allow-Origin: *不能是这样,而必须是Access-Control-Allow-Origin: http://localhost

第一次请求是没有cookie的,服务器会设置一个cookie,之后的请求全都带有cookie。


对应的服务器端代码(PHP为例):

if($_SERVER['REQUEST_METHOD'] == 'GET'){
    if (!isset($_COOKIE["pageAccess"])) {
        setcookie("pageAccess", 1, time()+2592000);
        header('Access-Control-Allow-Origin: http://localhost');
        header('Cache-Control: no-cache');
        header('Pragma: no-cache');
        header('Access-Control-Allow-Credentials: true');
        header('Content-Type: text/plain');
        echo 'I do not know you or anyone like you so I am going to mark you with a Cookie :-)';
    }
    else{
        $accesses = $_COOKIE['pageAccess'];
        setcookie('pageAccess', ++$accesses, time()+2592000);
        header('Access-Control-Allow-Origin: http://localhost');
        header('Access-Control-Allow-Credentials: true');
        header('Cache-Control: no-cache');
        header('Pragma: no-cache');
        header('Content-Type: text/plain');

        echo 'Hello -- I know you or something a lot like you!  You have been to ', $_SERVER['SERVER_NAME'], ' at least ', $acesses-1, ' time(s) before!';
    }
    
}
elseif($_SERVER['REQUEST_METHOD'] == "OPTIONS")
{
    // Tell the Client this preflight holds good for only 20 days
    if($_SERVER['HTTP_ORIGIN'] == 'http://localhost'){
        header('Access-Control-Allow-Origin: http://localhost');
        header('Access-Control-Allow-Methods: GET, OPTIONS');
        header('Access-Control-Allow-Credentials: true');
        header('Access-Control-Max-Age: 1728000');
        header("Content-Length: 0");
        header("Content-Type: text/plain");
    }
    else{
        header("HTTP/1.1 403 Access Forbidden");
        header("Content-Type: text/plain");
        echo "You cannot repeat this request";
    }
}
else{
    die("This HTTP Resource can ONLY be accessed with GET or OPTIONS");
}



下面分别介绍一下CORS里面可用的头


响应头

1、Access-Control-Allow-Origin

语法:

Access-Control-Allow-Origin: <origin> | *

这个头是指定可访问资源的URL。浏览器必须强制执行。

对于不需要认证的请求,服务器可以指定一个通配符*,意味着可以被任何人访问。

还可以设置为指定的域名。


2、Access-Control-Expose-Headers

这个头是服务器告诉浏览器那些头是可以被放在请求资源的头里面的。

就是服务器的一个头白名单。

比如:

Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header

允许the X-My-Custom-Header 头 X-Another-Custom-Header放在浏览器的请求头里面。


3、Access-Control-Max-Age

这个头的意思是一个preflight请求可以被缓存多久,单位是秒,如果在缓存过期之前又发起了一个另一个preflight请求,之前被缓存的请求也会过期。


4、Access-Control-Allow-Credentials

语法:

Access-Control-Allow-Credentials: true | false

用来确定响应是否有效,当作为preflight请求的响应时,可以用来确定实际的请求是否可以使用凭证。

一个非preflighted的简单请求,加入请求带了凭证,当时响应头里面没有Access-Control-Allow-Credentials头,响应会被浏览器忽略,并且没有返回内容。


5、Access-Control-Allow-Methods

用于指定可以使用的HTTP方法,这个头用在preflight请求的响应里面。


6、Access-Control-Allow-Headers

用在preflight请求的响应里面,声明在实际请求里面可以使用的HTTP头。


请求头

1、Origin

指明请求的来源。这个头可以设置为空字符串,但是在访问控制请求里面,这个头的值永远都需要设置。


2、Access-Control-Request-Method

这个头用在preflight请求里面,告诉服务器实际的请求是用的什么HTTP方法。


3、Access-Control-Request-Headers

也是用在preflight请求里面,告诉服务器实际的请求会使用什么HTTP头。



最后就是CORS的浏览器兼容性了,大部分的桌面浏览器和移动浏览器都是支持的。

遇到不支持的那就只能用其他方法了。


参考资料:HTTP access control


本来链接:https://360us.net/article/31.html