以下將以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了,詳情可參考官方文件。

  1. 產生一個jsonp.js
    主要的核心都在這,這裡在做的事情就是產生一個script tag放到你的HTML DOM內。而這邊會放入的是一個callback function,主要原因剛剛也有提到了,因為JSON並非一個程式,因此需要用function去包裝它。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    export 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;
    };
  2. 在Redux的action中,GET資料
    以下舉例兩種GET資料的方式

    1. 以redux-api-middleware為例
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      import 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'
      ]
      }
      };
      };

2.2. 以fetch的方式為例
2.2.1. GET function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import jsonp from './jsonp';
const link = 'https://<your wordpress site>/wp-json/wp/v2/posts?per_page=3';
export const initWordpress = () => {
return dispatch => {
fetch( jsonp(link, response => callback(JSON.parse(response.data))), {
method: 'GET',
headers: {
'Content-Type': 'application/javascript'
}
})
.then((receivedData) => {
return dispatch(setWordpress(receivedData));
}
.catch((error) => {...})
}

2.2.2. action type function

1
2
3
4
5
6
export const setWordpress = (receivedData) => {
return {
type: actionTypes.SET_WORDPRESS,
options: receivedData
}
}
  1. 在Redux的reducer中,放入要執行的動作

    1
    2
    3
    4
    5
    export default function (state = initState, action) {
    switch (action.type) {
    case 'LOAD_WORDPRESS_DATA': {...}
    return {...};
    }
  2. 測試你的結果
    這樣就完成了?沒錯!就是這麼簡單。
    還不太了解原理的話,可以在你的程式中加入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
2
3
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.
或是
Refused to execute script from 'https://...' because its MIME type ('application/json') is not executable, and strict MIME type checking is enabled.

答:其實上面已經寫得很清楚了,主要是因為JSONP他是JavaScript,並不是JSON,這很重要!所以他才會給你警告。你只需要加上你的script的type =’javascript’即可。
你的JSONP程式碼

1
2
3
4
5
6
...
let script = document.createElement('script');
script.src = ...;
script.type = 'javascript';
document.body.appendChild(script);
...

或是你可以嘗試新增header的Content-Type

  1. fetch
    1
    2
    3
    4
    5
    6
    fetch( ... ), {
    method: 'GET',
    headers: {
    'Content-Type': 'application/javascript'
    }
    })
  2. RSAA
    1
    2
    3
    4
    5
    6
    7
    8
    [RSAA]: {
    endpoint: ...,
    method: 'GET',
    headers: { 'Content-Type': 'application/javascript' },
    types: [
    ...
    ]
    }

後記

其實會寫這篇文章也是因為之前找資料的時候發現資源不好找,大部分都是很舊的資料和jQuery的使用範例,所以在這邊寫下來,希望能幫助到需要的人。