浅析Cross Origin Resource Sharing

在前面我们已经简单介绍了如何利用XMLHttpRequest Object来进行客户端与服务器之间的通信,但是,基于这种XMLHttpRequest Object的AJAX通信技术有一个局限,出于对于数据安全性的考虑,XMLHttpRequest只能够访问同一个站点的数据(相同的请求协议,相同的域名,相同的服务器端口)。但是在日常的开发过程中,我们又的的确确有很多的地方需要跨越站点之间传输数据,比如银行网站,需要通过证监会或者金管局或者其他的第三方金融机构获取一些金融方面的信息,例如最新的金融规则,股票信息等等。由于这种需求跟呼声越来越大,W3C起草了Cross Origin Resource Sharing Specification,可以让站点之间更加便捷又不失安全的跨域访问数据。

W3C的Cross Origin Resource Sharing,它的最核心的地方在于:通过一些Http Header信息,可以让浏览器跟服务器之间更加充分的‘认识’对方,继而决定是否支持这个跨域的资源请求。

这里简单的介绍一个一些常用的用于实现跨域资源访问的一些方法。

XDomainRequest

Microsoft公司在IE8+的浏览器中新添了XDomainRequest Object来支持跨域资源的共享策略,这个新生的对象跟XMLHttpRequest相比有很多相似的地方,但是也有一些明显的不同。与传统的XMLHttpRequest对象相比,XDomainRequest对象的使用有更加严格的限制,具体如下所示:

 - 使用XDomainRequest对象发送请求的时候,不能在请求当中发送Cookie信息,同时,在响应当中也不会返回Cookie信息

 - 除了Content-Type,不可以对其他的HTTP Request Header进行任何的操作 (例如XDomainRequest.contentType = 'application/x-www-form-urlencoded')

 - 不可以获取response的Header信息

 - 只能够支持GET跟POST方法

使用XDomainRequest进行跨域操作,跟XMLHttpRequest对象一样,follow以下三个步骤:

 - 创建一个XDomainRequest对象

 - 初始化请求: xDomainRequest.open(method, url); (注意:与XMLHttpRequest不同的是,该方法只有两个参数,只能够支持异步通信)

 - 发送请求: xDomainRequest.send(data/null);

使用XDomainRequest进行跨域资源访问的时候,我们可以通过这个创建的XDomainRequest所触发的一些event事件,捕获事件的处理结果:

 - load : 请求完成,我们可以通过responseText属性获取返回的结果; (注意:XDR的status跟statusText属性不可用)

 - error : 请求出现错误,但是遗憾的是除了触发这个error事件之外,没有其他任何的错误信息返回; (推荐:务必绑定此方法,因为导致XDomainRequest失败的原因各种各样,如果不绑定此事件,我们无法知道请求成功与否,failed siliently)

 - timeout : 我们可以对XDomainRequest对象设置一个timeout阀值(XDomainRequest.timeout=<timeout ms>),当请求超时时,会触发此事件

另外我们可以使用abort()方法强行终止请求。

下面是这个方法的一个具体使用代码片段:

function createCORSRequest() {
    var xdr = new XDomainRequest();
    
    xdr.onload = function () {
        console.debug('Response : ' + xdr.responseText);
    };
    
    xdr.onerror = function () {
        console.error('Failed to retrieve data!');
    };
    
    xdr.timeout = 5000;
    xdr.ontimeout = function () {
        console.debug('Request took too long!');
    };
    
    return xdr;
}

使用XDomainRequest对象发送HTTP请求会在Header头部信息中添加Origin属性,例如Origin: http://www.cnblog.andycbluo,用以告诉服务器端这个跨域请求的发起站点信息,服务器端通过这个信息可以决定是否支持次跨域请求,如果通过,服务器就会返回一个Access-Control-Allow-Origin:http://www.cnblog.andycbluo或者*的头部信息最为响应。

注意:IE11当中已经抹掉了这个XDomainRequest对象,改用与其它浏览使用标准的XMLHttpRequest对象

XMLHttpRequest Object

Firefox,Safari,Chrome等浏览器则是从一开始便是选择了通过改进传统的XMLHttpRequest对象来实现这种操作。

使用XMLHttpRequest对象进行跨域资源的请求操作时,它的具体使用方式与之前介绍的XMLHttpRequest操作完全一致,唯一不同的是,当我们在调用open method的时候,必须使用绝对路径(譬如:http://www.cnblog.andycbluo/cors/index.html)

与传统的XMLHttpRequest请求相比较,出于资源请求的安全性,跨域请求有更为严格的限制:

 - 不可以通过setRequestHeader来设置请求头部信息

 - 请求与响应不带任何的Cookie信息

 - 响应的getALLResponseHeaders方法永远返回empty

与XDomainRequest相比较,XMLHttpRequest更为灵活,它可以支持同步跟异步请求。

下面我们来看看具体的操作例子:

function createCORSRequest() {
    var xhr = new XMLHttpRequest();
    
    xhr.onload = function () {
        console.log('Response : ' + xdr.responseText);
    };
    
    xhr.onerror = function () {
        console.log('Failed to retrieve data!');
    };

    return xhr;
}
<!DOCTYPE html>
<html>
    <head>
        <title>Cross Origin Resource Sharing Demo</title>
        <script type='text/javascript' src='cors.js'></script>
    </head>
    <body>
        <script type='text/javascript'>
            window.onload = function () {
                var xhr = createCORSRequest();
                xhr.open('GET', 'https://www.hsbc.com.hk/zh-cn/index.html', true);
                xhr.send(null);
            };
        </script>
    </body>
</html>

我们可以通过Fiddler很清楚的看到发送的HTTP Request Header信息中包含了请求的Origin站点信息

 1 GET https://www.hsbc.com.hk/zh-cn/index.html HTTP/1.1
 2 Host: www.hsbc.com.hk
 3 Connection: keep-alive
 4 Cache-Control: max-age=0
 5 User-Agent: Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727)
 6 Origin: http://www.test.qualityassurance.ebanking.hsbc.com.hk
 7 Accept: */*
 8 Referer: http://www.test.qualityassurance.ebanking.hsbc.com.hk/mobile/cors.html
 9 Accept-Encoding: gzip, deflate, sdch
10 Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4
11 Range: bytes=13416-13416
12 If-Range: "25d9d-17a36-52a75a284a570"

很遗憾的是,测试的对方站点不支持该跨域请求,在响应头部中我们并没有看到服务器返回任何Access-Control-Allow-Origin信息,所以这个跨域请求跪了

 1 HTTP/1.1 206 Partial Content
 2 Date: Fri, 29 Jan 2016 11:17:42 GMT
 3 Last-Modified: Fri, 29 Jan 2016 09:26:49 GMT
 4 ETag: "25d9d-17a36-52a75a284a570"
 5 Accept-Ranges: bytes
 6 Cache-Control: max-age=43200
 7 Expires: Fri, 29 Jan 2016 23:17:42 GMT
 8 Vary: Accept-Encoding,User-Agent
 9 S: hkp1v-pwsgw_tk02-hkp2vl0030
10 Content-Range: bytes 13416-13416/20228
11 Content-Length: 0
12 Keep-Alive: timeout=5, max=100
13 Connection: Keep-Alive
14 Content-Type: text/html
15 Set-Cookie: HKWGTK=175296522.17781.0000; path=/

Console输出:

Credentialed Request

默认的情况之下,利用XMLHttpRequest对象进行跨域资源请求的时候,我们并不会发送任何的认证信息,譬如Cookie,SSL Certificates,HTTP Authentication等等,但是我们可以通过对其withCredentials属性进行设置来发送此信息,具体如下所示:

<!DOCTYPE html>
<html>
    <head>
        <title>Cross Origin Resource Sharing Demo</title>
        <script type='text/javascript' src='cors.js'></script>
    </head>
    <body>
        <script type='text/javascript'>
            window.onload = function () {
                var invocation = new XMLHttpRequest();
                invocation.withCredentials = true;
                invocation.onload = function () {
                    console.log(invocation.responseText);
                };
                invocation.onerror = function () {
                    console.log('Request failed : ' + invocation.status);
                };
                invocation.open('GET', 'https://infrequently.org/2006/03/comet-low-latency-data-for-the-browser/', true);
                invocation.send(null);
            };
        </script>
    </body>
</html>

注意: 当我们使用了Cedentialed Request进行跨域资源请求的时候,服务器端必须返回Access-Control-Allow-Credentials:true跟Access-Control-Allow-Origin:<your requested origin>,否则请求失败。

Preflighted Request (预请求)

HTTP 1.1规范当中标明,如果请求不是以GET/POST/Head的方式,或者是内容类型(content-type)不是常规的普通文本格式的时候,浏览器会通过HTTP 1.1规范当中的OPTIONS方法向服务器发送一个Preflighted Request(预请求)。

浏览器使用OPTIONS方法并且发送以下的Headers信息跟对方站点的服务器:

 - Origin : 请求源站点信息

 - Access-Control-Request-Method : 该跨域请求将要使用的方法

 - Access-Control-Request-Headers : 该跨域请求将会使用到的customized headers,这个信息并非必须的,当有多个header值时,用逗号,隔开

如果服务器支持该跨域请求,就会在response header中返回以下的信息:

 - Access-Control-Allow-Origin

 - Access-Control-Allow-Methods : 例如GET, POST

 - Access-Control-Allow-Headers : 例如Username, Password

 - Access-Control-Max-Age : 该预请求将会被Cache的时间(seconds)

下面的例子就会触发一个Preflighted Request:

<!DOCTYPE html>
<html>
    <head>
        <title>Cross Origin Resource Sharing Demo</title>
        <script type='text/javascript' src='cors.js'></script>
    </head>
    <body>
        <script type='text/javascript'>
            window.onload = function () {
                var invocation = new XMLHttpRequest();
                invocation.withCredential = true;
                invocation.onload = function () {
                    console.log(invocation.responseText);
                };
                invocation.onerror = function () {
                    console.log('Request failed : ' + invocation.status);
                };
                invocation.setRequestHeader('content-type', 'application/xml');
                invocation.open('POST', 'https://infrequently.org/2006/03/comet-low-latency-data-for-the-browser/', true);
                invocation.send(null);
            };
        </script>
    </body>
</html>

Cross Browsers - CORS

综上所述,由于不同的浏览器对于Cross Origin Resource Sharing的支持不同,我们可以开发一个跨浏览器的具体实现,以方便我们进行跨域资源请求操作,具体可见以下:

/****************************************************************************************************
 * This method return the normonized cross origin resource sharing request object
 *
 ****************************************************************************************************/
function createCORSRequest (method, url) {
    var xhr = createXMLHttpRequest();
    
    if ('withCredentials' in xhr) {
        // standar method
        xhr.open(method, url, true);
    } else if (typeof XDomainRequest !== undefined) {
        // IE8+ use XDomainRequest
        xhr = new XDomainRequest();
        xhr.open(method, url);
    } else {
        throw new Error('Cross-Origin-Resource-Sharing is not suported for this type of browser!');
    }
    
    return xhr;
};

/****************************************************************************************************
 * This method return the normonized XMLHttpRequest object
 *
 ****************************************************************************************************/
function createXMLHttpRequest () {
    var xmlHttpReq;
    
    if (typeof XMLHttpRequest !== undefined) {
        // standard method
        xmlHttpReq = new XMLHttpRequest();
    } else if (typeof ActiveXObject !== undefined) {
        // IE version : ActiveXObject to create the XMLHttpRequest object
        var vers = ['MS2XML.XMLHTTP.6.0', 'MS2XML.XMLHTTP.3.0', 'MS2XML.XMLHTTP', 'Microsoft.XMLHttp'];
        for (var i = 0; i < vers.length; i++) {
            try {
                xmlHttpReq = new ActiveXObject(vers[i]);
            } catch (e) {
                continue;
            }
        }
    } else {
        // not supported browser
        throw new Error('XMLHttpRequest object is not supported for this type of browser!');
    }
    
    return xmlHttpReq;
}

CORS之外的一些跨域资源访问的方法

除了上面我们所提到的W3C跨域资源共享的规范之外,我们还可以通过以下一些常用的方法来进行跨域资源的访问和通信。

  • Image Ping

Image Ping是我们常用的一种用于跨域资源访问的方法之一,它的实现依靠的是DOM的<img>标签可以任意的访问任何站点的资源而不受限制的特性,使用Image Ping有以下的有点:

 - 适用性广泛,所有的browser都支持这种方式

 - 通过设置<img>标签的src属性,可以任意访问其他站点的资源不受限制

 - 可以通过在src属性后面添加参数,向服务器端发送额外的data

 - 我们可以通过image.onload跟image.onerror来监听请求是否成功

Image Ping的使用也具有以下的局限性:

 - 只可以通过HTTP GET的方式发送HTTP请求,向服务器发送的数据的总长度有限

 - 数据传输的单向性,不可以对服务器返回的响应进行任何操作

通过以上的描述,Image Ping最常被用于网站站点数据的收集,特别是对某一些广告信息的收集,比如银行推出某一个理财产品,当由用户点击到这个理财广告的时候,客户端就把当前所点击的广告信息收集,通过Image Ping的方式发送给服务器后台,后台通过数据分析,就可以分析出哪一个系列的理财产品受众最广,哪一个系列的产品适用某一类人群等等。

Image Ping的实现方式可以参考以下例子:

var img = new Image();
img.onload = function () {
    console.debug('request successfully.');
};

img.onerror = function () {
    console.debug('request failed');
}

img.src = 'http://www.somesite.com/test?username=AndyLuo&ad=wealth';
  • JSONP

另外一个最为常用的跨域资源共享的方法便是JSONP(JSON with Padding,使用这种方法,服务器端返回的数据是以JSON的格式,但是包含在一个Javascript的回调函数之中,例如callback({JSON_Data}))。该方法利用的是HTML的<script> DOM元素可以跨域加载资源的特性,通过指定script的src属性,实现资源的跨域共享。

使用该方法的好处:

 - 可以实现客户端/服务器双向数据通信

 - 所有的浏览器都支持<script>标签的跨域资源共享

当然,使用这种方法也有一些弊端:

 - 服务器返回的是一个回调可执行函数,有可能存在恶意代码的风险 (因此,当我们使用这种方法进行跨域资源请求时我们必须保证第三方是受信任的)

 - 我们没有一种有效的机制得知请求的结果,虽然HTML5规范中赋予了onerror属性,但是到目前为止,没有任何浏览器实现了这个接口 (我们可以通过设置timeout值来进行处理,当时间超过我们设置的阀值时,我们可以把它作为一个失败请求来处理)

JSONP的实现可以参考以下的例子:

var scriptElem = document.createElement('script');
scriptElem.src = 'http://www.somesite.com/test?callback=handleResponse' // 我们通常把回调函数以请求parameter的方式发送给服务器,服务器接受后以该回调函数返回数据, handleResponse({JSON_Data});
document.body.appendChild(scriptElem, document.body.firstChild);
posted @ 2016-01-29 21:35  AndyCBLuo  阅读(1079)  评论(0编辑  收藏  举报