么是跨域?
簡單的來說,出于安全方面的考慮,頁面中的JavaScript無法訪問其他服務器上的數據,即“同源策略”。而跨域就是通過某些手段來繞過同源策略限制,實現不同服務器之間通信的效果。
什么是JSONP?
JSON(JavaScript Object Notation) 是一種輕量級的數據交換格式,JSONP 是 JSON with padding(填充式 JSON 或參數式 JSON)的簡寫。JSONP(JSON with Padding)則是JSON 的一種“使用模式”,通過這種模式可以實現數據的跨域獲取。JSONP 由兩部分組成:回調函數和數據。回調函數是當響應到來時應該在頁面中調用的函數。回調函數的名字一般是在請求中指定的。而數據就是傳入回調函數中的 JSON 數據。
JSONP跨域的基本原理
在同源策略下,在某個服務器下的頁面是無法獲取到該服務器以外的數據的,但img、iframe、script等標簽是個例外,這些標簽可以通過src屬性請求到其他服務器上的數據。利用script標簽的開放策略,我們可以實現跨域請求數據,當然,也需要服務端的配合。當我們正常地請求一個JSON數據的時候,服務端返回的是一串JSON類型的數據,而我們使用JSONP模式來請求數據的時候,服務端返回的是一段可執行的JavaScript代碼。
舉個例子,假如需要從服務器(http://www.a.com/user?id=123)獲取的數據如下:
那么,使用JSONP方式請求(http://www.a.com/user?id=123?callback=foo)的數據將會是如下:
當然,如果服務端考慮得更加充分,返回的數據可能如下:
這時候我們只要定義一個foo()函數,并動態地創建一個script標簽,使其的src屬性為http://www.a.com/user?id=123?callback=foo:
便可以使用foo函數來調用返回的數據了。
JSONP跨域原理探秘
我們知道,使用 XMLHTTPRequest 對象發送HTTP請求時,會遇到 同源策略 問題,域不同請求會被瀏覽器攔截。
那么是否有方法能繞過 XMLHTTPRequest 對象進行HTTP跨域請求呢?
換句話說,不使用 XMLHTTPRequest 對象是否可以發送跨域HTTP請求呢?
細心的你可能會發現,像諸如:
<script type="text/javascript" src="http://www.a.com/scripts/1.js"></script>
<img src="http://www.b.com/images/1.jpg" />
<link rel="stylesheet" />
這種標簽是不會遇到"跨域"問題的,嚴格意義上講,這不是跨域,跨域是指在腳本代碼中向非同源域發送HTTP請求,這只是跨站資源請求。
那么,我們是否可以利用跨站資源請求這一方式來實現跨域HTTP請求呢?
以<script></script>標簽為例進行探索,先看一段代碼:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>jsonp demo</title>
<!-- JavaScript片斷1 -->
<script type="text/javascript">
function handler(data) {
alert(data);
// our code here...
}
</script>
<!-- JavaScript片斷2 -->
<script type="text/javascript">
handler('success');
</script>
</head>
<body>
A JSONP demo.
</body>
</html>
這段代碼中,有2個JavaScript片斷,第1個片斷中定義了一個處理函數handler(),這個處理函數比較簡單,沒有對數據做任何處理,只是把它alert出來;第2個片斷調用了它,運行這個頁面瀏覽器會彈出"success"。
我們假設第2個JavaScript片斷存儲在別的地方,然后我們使用<script src="" />的方式把它引入進來,像這樣:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>jsonp demo</title>
<!-- JavaScript片斷1 -->
<script type="text/javascript">
function handler(data) {
alert(data);
// our code here...
}
</script>
<!-- JavaScript片斷2 -->
<script type="text/javascript" src="http://service.a.com/script/1.js"></script>
</head>
<body>
A JSONP demo.
</body>
</html>
service.a.com/script/1.js:
handler('success');
這種方法和把JavaScript代碼直接寫在頁面是等效的,但是,我們由此可以聯想到什么?
我們是否可以事先在本頁面定義處理程序,服務端返回JS腳本,腳本的內容就是對處理程序的回調,服務返回的數據通過參數的形式傳回:
handler('服務返回的數據');
然后通過動態向當前頁面head節點添加<script src="服務地址"></script>節點的方式來“偽造”HTTP請求?
于是,可以編寫這樣一個簡單的測試用例:
先寫服務端,非常簡單的一個服務,返回字符串"Hello World",一般處理程序Service.ashx:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace JSONPDemo.Service
{
/// <summary>
/// Service2 的摘要說明
/// </summary>
public class Service2 : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType="text/plain";
context.Response.Write("handler('Hello World');");
}
public bool IsReusable
{
get
{
return false;
}
}
}
}
再寫客戶端,一個簡單的靜態Web頁,index.html:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title></title>
<script type="text/javascript">
// 跨域發送HTTP請求,從服務端獲取字符串"Hello World"
function getHello() {
var script=document.createElement('script');
script.setAttribute('src', 'http://localhost:8546/Service.ashx');
document.querySelector("head").appendChild(script);
}
// 處理函數
function handler(data) {
alert(data);
// our code here...
}
</script>
</head>
<body>
<input type="button" value="發送跨域HTTP請求,獲取Hello World" onclick="getHello()" />
</body>
</html>
測試成功!
在這個測試例子中,我們使用一般處理程序編寫了一個簡單的返回Hello World的服務,然后使用動態創建<script></script>節點的方式實現了跨域HTTP請求。
由于<script>、<img>標簽資源請求是異步的,所以我們就實現了一個跨域的異步HTTP請求。
但是這么做是不夠的,一個頁面可能會有多個HTTP請求,而上面這個示例的處理程序只有一個——handler。
不同的請求應該由不同的處理程序來處理,對上面的代碼稍做修改,只需要給<script>標簽的src屬性中的URL添加一個參數來指定回調函數的名稱就可以了:
服務端:
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType="text/plain";
// 前端指定的回調函數名稱
var callbackFuncName=context.Request.QueryString["callback"];
var responseData="Hello World";
// 回調腳本,形如:handler('responseData');
var scriptContent=string.Format("{0}('{1}');", callbackFuncName, responseData);
context.Response.Write(scriptContent);
}
Web客戶端:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>jsonp demo</title>
<script type="text/javascript">
// 跨域發送HTTP請求,從服務端獲取字符串"Hello World"
function getHello() {
var script=document.createElement('script');
script.setAttribute('src', 'http://localhost:8546/Service.ashx?callback=handler');//callback指定回調函數名稱
document.querySelector("head").appendChild(script);
}
// 處理函數
function handler(data) {
alert(data);
// our code here...
}
</script>
</head>
<body>
<input type="button" value="發送跨域HTTP請求,獲取Hello World" onclick="getHello()" />
</body>
</html>
使用jQuery的JSONP跨域
jQuery的ajax方法對JSONP式跨域進行了封裝,如果使用jQuery進行JSONP原理式的跨域HTTP請求,將會變得非常簡單:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>jQuery jsonp demo</title>
<script src="jquery-1.11.0.min.js"></script>
<script type="text/javascript">
$.ajax({
type: "get",
async: false,
url: "http://localhost:8546/Service.ashx",
dataType: "jsonp",
success: function (data) {
alert(data);
},
error: function () {
alert('fail');
}
});
</script>
</head>
<body>
使用jQuery一切將會變得非常簡單。
</body>
</html>
只需要將dataType設置為"jsonp"就可以進行跨域請求了,一切就像發送非跨域請求那樣簡單。
jQuery為我們封裝好了回調函數,一般情況下不需要我們單獨去寫,如果你不想在success中處理,想單獨寫處理函數,那么可以通過設置這2個參數來實現:
必須要強調的是:
1.JSONP雖然看起來很像一般的ajax請求,但其原理不同,JSONP是對文章第一小節原理的封裝,是通過<script>標簽的動態加載來實現的跨域請求,而一般的ajax請求是通過XMLHttpRequest對象進行;
2.JSONP不是一種標準協議,其安全性和穩定性都不如 W3C 推薦的 CORS;
3.JSONP不支持POST請求,即使把請求類型設置為post,其本質上仍然是一個get請求。
、使用跨文檔消息傳遞(Cross-document Messaging)
可以在不同窗口或iframe之間安全地傳遞消息,即使這些窗口或iframe來自不同的域。以下是使用window.postMessage()方法進行跨域消息傳遞的基本示例:
假設有兩個頁面:page1.html和page2.html,它們分別位于不同的域。
在page1.html中,我們想要向page2.html發送消息:
<!-- page1.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Page 1</title>
</head>
<body>
<button id="sendMessageBtn">Send Message to Page 2</button>
<script>
const sendMessageBtn=document.getElementById('sendMessageBtn');
// 監聽按鈕點擊事件
sendMessageBtn.addEventListener('click', function() {
// 獲取目標窗口的引用
const targetWindow=window.parent.frames['page2-frame'];
// 向目標窗口發送消息
targetWindow.postMessage('Hello from Page 1!', 'http://www.example.com/page2.html');
});
</script>
</body>
</html>
在page2.html中,我們接收來自page1.html的消息:
<!-- page2.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Page 2</title>
</head>
<body>
<iframe src="http://www.example.com/page1.html" name="page1-frame" id="page1-frame"></iframe>
<script>
// 監聽來自其他窗口的消息
window.addEventListener('message', function(event) {
// 判斷消息來源是否是預期的域
if (event.origin==='http://www.example.com') {
// 處理收到的消息
console.log('Received message from Page 1:', event.data);
}
});
</script>
</body>
</html>
page1.html包含一個按鈕,當點擊按鈕時,會向page2.html發送消息。page2.html中通過監聽window對象的message事件來接收來自page1.html的消息,并且只有當消息的來源是預期的域時才會處理該消息。
需要注意的是,跨文檔消息傳遞僅在現代瀏覽器中得到支持,且需要發送消息的窗口或iframe引用以及目標窗口的域。以前的IE瀏覽器啥的是不支持的。這種方式較為簡單,方便初學者進行測試。
二、還有一種辦法較為復雜,可以利用代理服務器,通過在同一域下設置一個代理服務器,將跨域請求發送到該代理服務器上,再由代理服務器轉發請求到目標服務器。這種方法需要在服務器端實現代理,但可以繞過瀏覽器的跨域限制。
另外還有兩種較為簡單的方式也做一下介紹,下面這2種方式是常用的方式了,較為簡單,
三、JSONP(JSON with Padding):JSONP是一種利用<script>標簽的GET請求實現跨域數據傳輸的技術。它允許從其他域加載數據,但只支持GET請求,且需要目標服務器支持返回JavaScript回調函數。JSONP的缺點是安全性較低,僅能進行GET請求。
四、CORS(Cross-Origin Resource Sharing):CORS是一種現代的跨域資源共享機制,它允許服務器端設置HTTP頭部,以允許在不同域之間的安全數據傳輸。通過在服務器端配置,可以允許跨域請求發送和接收數據。
五、WebSocket:得利于HTML5技術的發展,現在主流瀏覽器的支持,WebSocket是HTML5提供的一種在單個TCP連接上進行全雙工通訊的協議,它可以與任意域的服務器進行通訊,但需要服務器端支持WebSocket協議。
在客戶端,我們可以使用JavaScript來創建WebSocket連接:
// 客戶端代碼
const socket=new WebSocket('ws://example.com:8080'); // 替換為實際的WebSocket服務器地址
// 當WebSocket連接成功建立時觸發
socket.onopen=function(event) {
console.log('WebSocket連接已建立');
// 向服務器發送數據
socket.send('Hello from client!');
};
// 當接收到來自服務器的消息時觸發
socket.onmessage=function(event) {
console.log('Received message from server:', event.data);
};
// 當發生錯誤時觸發
socket.onerror=function(error) {
console.error('WebSocket發生錯誤:', error);
};
// 當WebSocket連接關閉時觸發
socket.onclose=function(event) {
console.log('WebSocket連接已關閉');
};
在服務器端,您需要使用相應的語言和框架來創建WebSocket服務器。以下是一個簡單的Node.js示例:
家好,我是前端西瓜哥。今天講 JSONP。
JSONP,是 JSON with Padding 的縮寫,字面上的意思就是 “填充 JSON”。JSONP 是解決跨域請求的一種方案,我們先了解下跨域請求是什么。
瀏覽器在跨域發送 Ajax/fetch 請求時,會觸發瀏覽器的同源策略,導致請求失敗。
只要協議、域名、端口有一個不同,那瀏覽器就會認為是跨域。比如你在 a.com 下通過 Ajax 請求 b.com/api/book,默認情況下就被瀏覽器攔截,導致無法獲得返回數據。
具體跨域的知識點,可以看我的這篇文章《為什么瀏覽器不能跨域發送 ajax 請求?》
既然 Ajax 不能發送跨域請求,那我們不用 Ajax,改用 script 標簽。
HTML 下的 script 標簽會指向一個腳本地址,這個地址允許跨域,瀏覽器會加載這個腳本然后執行。
除了 script 標簽, link、img 等標簽的請求也是允許跨域的。
下面以通過用戶 id 獲取用戶信息為例。
在 script 標簽的 src 上,我們指定好需要服務器進行填充的回調函數名 setUser,并帶上用戶 id。
<script src="http://b.com:4000/user?id=2&callback=setUser"></script>
上面這種直接這樣寫到 HTML 里不太靈活,我們改寫成下面這樣。
let user = null;
function setUser(user) {
// 保存用戶信息
window.user = user;
// 輸出到頁面上,看看效果。
document.body.append(
JSON.stringify(user);
);
}
function getUserById(id) {
const script = document.createElement('script');
script.src = `http://b.com:4000/user?id=${id}&callback=setUser`;
document.body.appendChild(script);
}
document.querySelector('button').onclick = function() {
getUserById(2);
}
然后是服務端的處理,這里我用了 Nodejs 的 Express 框架。
const app = express();
// ...
const map = {
1: { name: 'fe_watermelon' },
2: { name: '前端西瓜哥' }
};
// 例子:/user?id=2&callback=setUser
app.get('/user', (req, res, next) => {
const { id, callback } = req.query;
res.send(`${callback}(${JSON.stringify(map[id])})`);
});
// ...
服務端從 url 的請求字段中提取出 id ,找到對應的用戶信息(通常為 JSON 的形式),配合要填充的回調函數 setUser,組裝成字符串 setUser({"name":"前端西瓜哥"})
這個內容會作為腳本內容返回給前端,前端運行這個腳本后,就會執行全局作用域下的 setUser 函數,這個函數還會拿到用戶信息,將其保存下來。
JSONP 是一種解決跨域的方案,但一般比較少用到。
因為 JSONP 并不是標準,也不安全,服務端代碼沒寫好會有代碼注入的風險,且無法防范跨站請求偽造(CSRF)攻擊。
我們也注意到要兜住返回的數據,必須定義一個全局的函數,在如今主流的模塊化(變量隔離在各個模塊中不暴露到全局)寫法下有點格格不入。
我是前端西瓜哥,喜歡寫技術文章,歡迎關注我。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。