為什么需要跨域?
瀏覽器 同源策略 (域名、協(xié)議巾兆、端口相同)限制其他網(wǎng)頁對(duì)本網(wǎng)頁進(jìn)行非法篡改, 只有帶有 src 屬性的標(biāo)簽才允許跨域(<img>
<iframe>
<script>
)
JSONP
JSONP(JSON with Padding)
是一個(gè)非官方的協(xié)議路鹰,它允許在服務(wù)器端集成Script tags
返回至客戶端袜腥,通過javascript callback
的形式實(shí)現(xiàn)跨域訪問(這僅僅是JSONP簡單的實(shí)現(xiàn)形式)辛块。利用<script/>
標(biāo)簽對(duì)javascript
進(jìn)行動(dòng)態(tài)解析來實(shí)現(xiàn)(其實(shí)也可以用eval
函數(shù)),然后在服務(wù)端輸出JSON數(shù)據(jù)并執(zhí)行回調(diào)函數(shù)贼邓,從而解決了跨域的數(shù)據(jù)請(qǐng)求。
IE 兼容
P3P 規(guī)范讓 IE 跨域接受第三方 cookie
header('P3P:CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"');
原理
1.首先在客戶端注冊(cè)一個(gè)callback, 然后把callback的名字傳給服務(wù)器热鞍。
- 此時(shí)葫慎,服務(wù)器先生成 json 數(shù)據(jù)。
- 然后以 javascript 語法的方式薇宠,生成一個(gè)function , function 名字就是傳遞上來的參數(shù) jsonp.
- 最后將 json 數(shù)據(jù)直接以入?yún)⒌姆绞酵蛋欤胖玫?function 中,這樣就生成了一段 js 語法的文檔澄港,返回給客戶端椒涯。
- 客戶端瀏覽器,解析script標(biāo)簽回梧,并執(zhí)行返回的 javascript 文檔废岂,此時(shí)數(shù)據(jù)作為參數(shù),傳入到了客戶端預(yù)先定義好的 callback 函數(shù)里.(動(dòng)態(tài)執(zhí)行回調(diào)函數(shù))
缺點(diǎn)
- 只能采用
GET
請(qǐng)求,數(shù)據(jù)量大的跨域請(qǐng)求不能使用 - 如果使用
eval
來解析會(huì)容易出現(xiàn)安全問題 - 沒有
JSONP調(diào)用的錯(cuò)誤處理
,如果無效靜默失敗 - 被不信任的服務(wù)使用時(shí)會(huì)很危險(xiǎn),因?yàn)?JSONP 服務(wù)返回打包在函數(shù)調(diào)用中的 JSON 響應(yīng)狱意,而函數(shù)調(diào)用是由瀏覽器執(zhí)行的湖苞,這使宿主 Web 應(yīng)用程序更容易受到各類攻擊。
代碼示例
原生實(shí)現(xiàn)JSONP跨域
// 客戶端
<script>
// 這是回調(diào)方法
function cb(data){
alert(data.website);
}
</script>
<!--這是跨域請(qǐng)求的代碼,切記详囤,這段代碼要在回調(diào)函數(shù)之后-->
<script src="http://172.22.22.120/new/ajax_jsonp.php?callback=cb"></script>
<!-- 服務(wù)端 -->
<?php
$cb = htmlspecialchars($_GET['callback']); // 注意了财骨,這里要做好過濾,防止xss攻擊
echo $cb,'(',json_encode(array('website'=>'hcoding')),')'; // 返回客戶端的數(shù)據(jù)為:cb({"name":"hcoding"}) 這是一段js代碼
?>
AJax實(shí)現(xiàn)JSONP
客戶端代碼:
<script>
// 客戶端使用getJSON方法請(qǐng)求另一臺(tái)機(jī)子上的腳本
// 瀏覽器會(huì)生成一個(gè)隨機(jī)的callback參數(shù)
$.getJSON("http://172.22.22.120/new/ajax_jsonp.php?callback=?",function(json){
alert(json.website);
});
// $.getJSON簡單易用,但就是不能指定回調(diào)函數(shù)隆箩。
</script>
//或者
<script>
$.ajax({
type : "GET",
url : "http://172.22.22.120/new/ajax_jsonp.php",
dataType : "jsonp", // 數(shù)據(jù)格式指定為jsonp
jsonp: "callback", // 服務(wù)點(diǎn)通過這個(gè)鍵值獲取回調(diào)方法
jsonpCallback:"cb", // 指定回調(diào)方法
success : function(json){
},
});
// 回調(diào)方法
function cb(data){
alert(data.website);
}
</script>
服務(wù)端PHP腳本代碼:
<?php
$cb = htmlspecialchars($_GET['callback']); // 注意了滑肉,這里要做好過濾,防止xss攻擊
echo $cb,'(',json_encode(array('website'=>'hcoding')),')'; // 返回客戶端的數(shù)據(jù),這是一段js代碼
?>
CORS (IE8+) 推薦使用
跨源資源共享標(biāo)準(zhǔn)( cross-origin sharing standard ) 使得以下場景可以使用跨站 HTTP 請(qǐng)求:
- 如上所述摘仅,使用 XMLHttpRequest 發(fā)起跨站 HTTP 請(qǐng)求靶庙。
- Web 字體 (CSS 中通過 @font-face 使用跨站字體資源), 因此,網(wǎng)站就可以發(fā)布 TrueType 字體資源娃属,并只允許已授權(quán)網(wǎng)站進(jìn)行跨站調(diào)用六荒。
- WebGL 貼圖
- 使用 drawImage API 在 canvas 上畫圖
相關(guān)Header
服務(wù)端設(shè)置Header: (ResponseHeader)
-
Access-Control-Allow-Origin *
服務(wù)器設(shè)置允許訪問的源的網(wǎng)址 -
*
可以為網(wǎng)站網(wǎng)址 如http://api.a.com
-
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
允許訪問服務(wù)器的頭信息 -
Access-Control-Max-Age: <delta-seconds>
允許預(yù)請(qǐng)求參數(shù)緩存的毫秒數(shù),在此期間不用發(fā)出另一條預(yù)請(qǐng)求 Access-Control-Allow-Credentials: true | false
Access-Control-Allow-Methods: <method>[, <method>]*
-
Access-Control-Allow-Headers: X-PINGOTHER
這樣在實(shí)際的請(qǐng)求里請(qǐng)求頭信息里就可以有這么一條:X-PINGOTHER: pingpong
可以有多個(gè)自定義HTTP請(qǐng)求頭,用逗號(hào)分隔. Access-Control-Allow-Headers: <field-name>[, <field-name>]*
HTTP 請(qǐng)求頭(RequestHeader)
注意這些請(qǐng)求頭信息都是在請(qǐng)求服務(wù)器的時(shí)候已經(jīng)為你設(shè)置好的,當(dāng)開發(fā)者使用跨域的XMLHttpRequest的時(shí)候,不需要手動(dòng)的設(shè)置這些頭信息.
-
Origin: <origin>
注意,不僅僅是跨域請(qǐng)求,普通請(qǐng)求也會(huì)帶有ORIGIN頭信息. Access-Control-Request-Method: <method>
Access-Control-Request-Headers
Access-Control-Request-Headers: <field-name>[, <field-name>]*
請(qǐng)求方式
- 簡單請(qǐng)求
- 只使用 GET, HEAD 或者 POST 請(qǐng)求方法。如果使用 POST 向服務(wù)器端傳送數(shù)據(jù)矾端,則數(shù)據(jù)類型(Content-Type)只能是 application/x-www-form-urlencoded, multipart/form-data 或 text/plain中的一種掏击。
- 不會(huì)使用自定義請(qǐng)求頭(類似于 X-Modified 這種)。
- 預(yù)請(qǐng)求
- 請(qǐng)求以 GET, HEAD 或者 POST 以外的方法發(fā)起請(qǐng)求秩铆⊙馔ぃ或者,使用 POST殴玛,但請(qǐng)求數(shù)據(jù)為 application/x-www-form-urlencoded, multipart/form-data 或者 text/plain 以外的數(shù)據(jù)類型捅膘。比如說,用 POST 發(fā)送數(shù)據(jù)類型為 application/xml 或者 text/xml 的 XML 數(shù)據(jù)的請(qǐng)求滚粟。
- 使用自定義請(qǐng)求頭(比如添加諸如 X-PINGOTHER)
- 附帶憑證信息的請(qǐng)求
- XMLHttpRequest和訪問控制功能寻仗,最有趣的特性就是,發(fā)送憑證請(qǐng)求(HTTP Cookies和驗(yàn)證信息)的功能凡壤。一般而言署尤,對(duì)于跨站請(qǐng)求,瀏覽器是不會(huì)發(fā)送憑證信息的亚侠。但如果將XMLHttpRequest的一個(gè)特殊標(biāo)志位設(shè)置為true曹体,瀏覽器就將允許該請(qǐng)求的發(fā)送。
安全問題:不要依賴CORS當(dāng)中的權(quán)限制度硝烂,應(yīng)當(dāng)使用更多其它的措施來保障箕别,比如OAuth2
iframe 隱藏表單,進(jìn)行POST提交(非現(xiàn)代瀏覽器)
- 建立一個(gè)iframe,iframe內(nèi)的JS創(chuàng)建一個(gè)form表單钢坦,并可以將接收到的參數(shù)放入表單中POST提交
- 將iframe頁面插入到頁面中究孕。
window.name
有三個(gè)頁面:
- a.com/app.html:應(yīng)用頁面啥酱。
- a.com/proxy.html:代理文件爹凹,一般是一個(gè)沒有任何內(nèi)容的html文件,需要和應(yīng)用頁面在同一域下镶殷。
- b.com/data.html:應(yīng)用頁面需要獲取數(shù)據(jù)的頁面禾酱,可稱為數(shù)據(jù)頁面。
實(shí)現(xiàn)起來基本步驟如下:
- 在應(yīng)用頁面(a.com/app.html)中創(chuàng)建一個(gè)iframe,把其src指向數(shù)據(jù)頁面(b.com/data.html)颤陶。
數(shù)據(jù)頁面會(huì)把數(shù)據(jù)附加到這個(gè)iframe的window.name上颗管,data.html代碼如下:
<script type="text/javascript">
window.name = 'I was there!'; // 這里是要傳輸?shù)臄?shù)據(jù),大小一般為2M滓走,IE和firefox下可以大至32M左右
// 數(shù)據(jù)格式可以自定義垦江,如json、字符串
</script>
- 在應(yīng)用頁面(a.com/app.html)中監(jiān)聽iframe的onload事件搅方,在此事件中設(shè)置這個(gè)iframe的src指向本地域的代理文件(代理文件和應(yīng)用頁面在同一域下比吭,所以可以相互通信)。app.html部分代碼如下:
<script type="text/javascript">
var state = 0,
iframe = document.createElement('iframe'),
loadfn = function() {
if (state === 1) {
var data = iframe.contentWindow.name; // 讀取數(shù)據(jù)
alert(data); //彈出'I was there!'
} else if (state === 0) {
state = 1;
iframe.contentWindow.location = "http://a.com/proxy.html"; // 設(shè)置的代理文件
}
};
iframe.src = 'http://b.com/data.html';
if (iframe.attachEvent) {
iframe.attachEvent('onload', loadfn);
} else {
iframe.onload = loadfn;
}
document.body.appendChild(iframe);
</script>
- 獲取數(shù)據(jù)以后銷毀這個(gè)iframe姨涡,釋放內(nèi)存衩藤;這也保證了安全(不被其他域frame js訪問)。
<script type="text/javascript">
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
</script>
總結(jié)起來即:iframe的src屬性由外域轉(zhuǎn)向本地域涛漂,跨域數(shù)據(jù)即由iframe的window.name從外域傳遞到本地域赏表。這個(gè)就巧妙地繞過了瀏覽器的跨域訪問限制,但同時(shí)它又是安全操作匈仗。
HTML5 postMessage(IE8+)
otherWindow.postMessage(message, targetOrigin);
- otherWindow: 對(duì)接收信息頁面的window的引用瓢剿。可以是頁面中iframe的contentWindow屬性悠轩;window.open的返回值跋选;通過name或下標(biāo)從window.frames取到的值。
- message: 所要發(fā)送的數(shù)據(jù)哗蜈,string類型前标。
- targetOrigin: 用于限制otherWindow,“*”表示不作限制
a.com/index.html中的代碼:
<iframe id="ifr" src="b.com/index.html"></iframe>
<script type="text/javascript">
window.onload = function() {
var ifr = document.getElementById('ifr');
var targetOrigin = 'http://b.com'; // 若寫成'http://b.com/c/proxy.html'效果一樣
// 若寫成'http://c.com'就不會(huì)執(zhí)行postMessage了
ifr.contentWindow.postMessage('I was there!', targetOrigin);
};
</script>
b.com/index.html中的代碼:
<script type="text/javascript">
window.addEventListener('message', function(event){
// 通過origin屬性判斷消息來源地址
if (event.origin == 'http://a.com') {
alert(event.data); // 彈出"I was there!"
alert(event.source); // 對(duì)a.com距潘、index.html中window對(duì)象的引用
// 但由于同源策略炼列,這里event.source不可以訪問window對(duì)象
}
}, false);
</script>