React搭配JSONP解決跨網域存取問題
以下將以React抓取wordpress的資料為例子,讓你知道如何解決跨網域存取遇到的問題。
前言
在開發一些大型專案時,會運用同網域限制(Same origin policy)來保護資料傳輸的安全性問題。最常見的手法就是,在你的前端開一個Node.js API 專門與後端進行溝通,同時又能確保IP不被洩露的風險存在。
但是有時候專案上,會需要外部網域存取來完成你的需求。
例如:有一個專案要從WordPress的REST API拿資料,在自己的local測試是可行的,但是一旦Deploy到Lab,在Travis CI查看時,發現產生了不可預期的錯誤,抓下來的JSON是undefined,這就是同網域限制的問題。
同源策略
以下假設,現在有server1.abc.com 和 server2.abc.com兩個網域
理論上來說,server1和server2是無法進行溝通的。但是在HTML中,script tag是一個例外。因此可以利用script tag這個例外來達成雙方的溝通。
例如:從server1動態產生JSON傳給server2。這個模式稱為JSON-Padding。
認識JSONP
做法主要是在HTML上,形成一個JavaScript的tag,並且透過callback的方式形成跨網域的溝通。
運作原理
假設有一位同學使用這個網域來抓取自己的個人資料:server1.abc.com/profile?id=123
伺服器就會回傳一串JSON給使用者
1 | {id:123, user:'yy', favor:'apple'} |
這個時候,你可以想像成Script的src attribute被設置成回傳的URL。
JSON並非是一個程式,為了讓Browser可以在script tag運行,因此回傳的URL必須要是可執行的Script。
例如:
1 | <script src="http://server1.abc.com/profile?id=123&jsonp=parseResponse"></script> |
而在JSONP的模式中,他是一個由函數包裝起來的JSON。
因此瀏覽器必須為每一個JSONP加一個新的script tag元素到HTML DOM裡。瀏覽器執行時,會抓取src裡的URL,並執行回傳的 JavaScript。
於是JSONP也被稱為「讓使用者以script注入繞開同源策略」的方法。
安全問題
其實知道原理後,大概就能猜出會有什麼樣的問題產生了。如果來源的伺服器傳來的Script帶有任何威脅性的程式碼,可能會發生一些Script的攻擊。
如何使用
這邊以React抓取WordPress的資料為例,以下就不提如何使用WordPress REST API了,詳情可參考官方文件。
產生一個jsonp.js
主要的核心都在這,這裡在做的事情就是產生一個script tag放到你的HTML DOM內。而這邊會放入的是一個callback function,主要原因剛剛也有提到了,因為JSON並非一個程式,因此需要用function去包裝它。1
2
3
4
5
6
7
8
9
10
11
12
13export default (url, callback) => {
let callbackName = '_callback_' + Math.round(99999 * Math.random());
window[callbackName] = (data) => {
delete window[callbackName];
document.body.removeChild(script);
callback(data);
};
let script = document.createElement('script');
script.src = url + (url.indexOf('?') >= 0 ? '&' : '?') + 'callback=' + callbackName;
script.type = 'javascript';
document.body.appendChild(script);
return script.src;
};在Redux的action中,GET資料
以下舉例兩種GET資料的方式- 以redux-api-middleware為例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import jsonp from './jsonp';
const link = 'https://<your wordpress site>/wp-json/wp/v2/posts?per_page=3';
export const initWordpress = () => {
return {
[RSAA]: {
endpoint: jsonp(link, response => callback(JSON.parse(response.data))),
method: 'GET',
headers: { 'Content-Type': 'application/javascript' },
types: [
'REQUEST',
'LOAD_WORDPRESS_DATA',
'FAILURE'
]
}
};
};
- 以redux-api-middleware為例
2.2. 以fetch的方式為例
2.2.1. GET function
1 | import jsonp from './jsonp'; |
2.2.2. action type function
1 | export const setWordpress = (receivedData) => { |
在Redux的reducer中,放入要執行的動作
1
2
3
4
5export default function (state = initState, action) {
switch (action.type) {
case 'LOAD_WORDPRESS_DATA': {...}
return {...};
}測試你的結果
這樣就完成了?沒錯!就是這麼簡單。
還不太了解原理的話,可以在你的程式中加入console.log顯示你的script,你會發現他已經包裝成一個Script的tag了。1
<script src="<script src="https://<your wordpress site>/wp-json/wp/v2/posts?per_page=3&callback=_callback_72381"></script>
問與答
問:為什麼瀏覽器會有Cross-Origin Read Blocking的問題(如下)?
1 | Cross-Origin Read Blocking (CORB) blocked cross-origin response 'https://...' with MIME type application/json. See https://www.chromestatus.com/feature/5629709824032768 for more details. |
答:其實上面已經寫得很清楚了,主要是因為JSONP他是JavaScript,並不是JSON,這很重要!所以他才會給你警告。你只需要加上你的script的type =’javascript’即可。
你的JSONP程式碼
1 | ... |
或是你可以嘗試新增header的Content-Type
- fetch
1
2
3
4
5
6fetch( ... ), {
method: 'GET',
headers: {
'Content-Type': 'application/javascript'
}
}) - RSAA
1
2
3
4
5
6
7
8[RSAA]: {
endpoint: ...,
method: 'GET',
headers: { 'Content-Type': 'application/javascript' },
types: [
...
]
}
後記
其實會寫這篇文章也是因為之前找資料的時候發現資源不好找,大部分都是很舊的資料和jQuery的使用範例,所以在這邊寫下來,希望能幫助到需要的人。