iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 30
4
Modern Web

JavaScript基本功修煉系列 第 30

JavaScript基本功修練:Day30 - AJAX常遇上的同源政策問題與解決方法

經過這幾天學習AJAX,對於接API開始有點認識了,雖然有把一些例子順利寫出來跟大家分享,但是背後也曾經出了不少奇怪問題,例如以下經典問題:

fetch('https://www.facebook.com/')
    .then( (response) => console.log(response))
    .catch( (error) => console.log(error))

那時候馬上google查找答案,發現一堆術語,什麼同源政策、CORS、JSONP之類的,但當時真的沒有時間消化好這些知識(現在文章都是當天學當天寫當天發QQ),所以即使在鐵人賽最後一天,本來用來寫心得的,只好乖乖來繼續寫技術文了,完賽心得就留待明天再發囉~

回到重點,這篇文章會整理以下知識:

  • 同源政策
  • CORS
  • JSONP
  • Preflight Request

這裏推薦Huli老師關於AJAX的文章,以及卡斯伯老師使用dev tool檢查要求的文章,對新手的我非常有幫忙!

同源政策(Same Origin Policy)

簡單講就是,自己網站的資源不能被別人存取或修改

如果我從目前瀏覽器的網頁向跟自己「不同源」的網址發出請求和存取資料,就是被視作「跨來源存取」,一般情況下是不允許的,只有「同源」才會被允許。

原因:這是基於網絡安全的考量,避免有駭客惡意呼叫其他人的網絡服務。若沒有這個政策保護,別人就可以任意修改和存取你網頁裏的資源了。

同源政策有兩種:

  • DOM 同源政策
  • Cookie 同源政策

這裏會集中講第一點,DOM同源政策。

DOM 同源政策

什麼是DOM?在瀏覽器裏載入的所有圖片、文字、程式碼等等的資源,會變成一個個DOM元素。同源政策會禁止我去存取別人網站裏的DOM元素,即是別人網站裏的網絡資源。

那麼什麼同源(Same-Origin)?
要判斷是否同源,就看這兩個網址在以下的部分是否相同:

  • scheme (通訊協定,http, https是不一樣的!)
  • domain
  • port (埠號,如有指定)

MDN的例子:

所以簡單講,不同domain就是不同源,httphttps就是不同源,port不同就是不同源。當我們接別人的API時,多數就是不同源的情況

要注意一點:我的請求(request)的確是有發出去,我的瀏覽器之後也收到回應(response)。但多得瀏覽器的同源政策,它把回應擋下來了,不會把回拿到的回應掉給我的JavaScript去做另一些的處理。

同源政策並非完全禁止跨來源存取

但在某些情況下,即使兩個網站是「不同源」,也可以允許存取的。例如以下情況:

  1. 跨來源寫入(Cross-origin writes)
  2. 跨來源嵌入(Cross-origin embedding)

跨來源寫入:
例如允許:表單送出(form)、連結(link)、重新導向(redirect)

跨來源嵌入:
例如允許:嵌入圖片<img>、影片<video><iframe>、放在<script>裏的程式碼、CSS stylesheet <link rel="stylesheet" href="...">等等。然而,雖然我的網頁可以顯示到這些資源,但我的JavaScript並不能讀取這些資源的內容。

CORS 跨來源資源共享 (Cross-Origin Resource Sharing)

fetchXMLhttprequest都是會跟從同源政策,我們再次看這張圖:

裏面有一個關鍵:No 'Access-Control-Allow-Origin' header is present on the request resource.

Access-Control-Allow-Origin的設定決定了我這邊是否能順利存取資源。如果我想發出跨來源請求的話,對方的伺服器必須在回應表頭(response header)裏加上Access-Control-Allow-Origin,並在Access-Control-Allow-Origin的設定裏,新增我的Origin(即是我的網址),或者設定為萬用字符*,代表所有Origin都接受,這是在公共API裏常見的設定。

例如我的網址是https://amazing.site

Access-Control-Allow-Origin: https://amazing.site
//或者
Access-Control-Allow-Origin: *

只要伺服器設定好Access-Control-Allow-Origin(加入我的網址或*號),當我發出請求,以及伺服器那邊回傳回應後,瀏覽器就會檢查回應表頭,看看裏面的Access-Control-Allow-Origin是否有我的網址或者有*,如果有的話就會允許通過,成功存取資料。

例如我去接randam user這個公共API,我會成功收到資料。這時候打開dev tool去查,access-control-allow-origin的確是設定為*

如果要測試對方伺器服是否有設定好Access-Control-Allow-Origin,我們可以用test-cors.org這個平台去查。

JSONP

除了做以上的設定,我們也可以透過JSONP(JSON with Padding)這個方法來解決。剛才提及過<script>tag是不受同源政策限制的,我們可以用它來解決問題。

JSONP的做法就是,在一個<script>tag裏的放入伺服器端提供的網址,之後在另一個<script>tag裏宣告一個函式,函式名字是由伺服器端提供,也可以在伺服器端所提供的網址裏找到,例如它提供了https://...callback=abc這個網址,那麼該函式的名字就是abc

這裏用randomuser的API來做範例:

<script>
    function randomuserdata(response){
        console.log(response);
    }
</script>
<script src="https://randomuser.me/api/?gender=female&nat=us&callback=randomuserdata"></script>

下面那行URL會回傳randomuserdata函式,並在回傳randomuserdata函式時帶入那筆我本來想抓的資料,整個過程可以想像成以下這樣:

<script>
    function randomuserdata(response){
        console.log(response);
    }
</script>
<script>randomuserdata({那些你想抓的資料})</script>

注意,這兩個<script>有次序之分,要先寫那個宣告randomuserdata函式的<script>,之後才寫負責回傳randomuserdata函式的那個<script>,不然是報錯。

雖然JSONP解決了跨來源問題,但是JSONP只適用於GET請求,無法做到POST,所以首選還是上面提及的CORS的方法。

Preflight Request

最後來談談Preflight Request。

Preflight request並不是我本身想要發出的請求。Preflight request是我(瀏覽器端)發出請求前的一個「預檢請求」,這個預檢請求是負責查問伺服器,問它是否批准我們發出請求給它。

Preflight request會帶有一些關於我想發的請求的一些資訊,例如我將會使用的HTTP請求方法(GET、POST...)、Authorization等等。

什麼時候會使瀏覽器發出Preflight request呢?當我發出的請求不是簡單請求時,就會觸發Preflight request,當Preflight request被通過,我本身的請求才會被發出。簡單請求有一堆定義,例如請求要是GETHEADPOSTheaders其中一個,詳細請看這個MDN

例如,如果我提出DELETE請求,那就一定會觸發preflight request。這很合理,因為如果沒有preflight request,不管對方伺服器有沒有把我的網址加入'Access-Control-Allow-Origin',我仍然可以發出DELETE請求把對方伺服器裏的資料刪除。即使因為同源政策瀏覽器會擋下response,這也沒關係,因為我的DELETE請求一定會被對方伺服器接收的,這就是為什麼我們需要preflight request,否則別人真的可以隨便修改自己的東西。

重用昨天的六角學院練習用的API為例,我想先找出所有商品:

const uuid = xxxxxx;
const token = xxxxxx;
const url = `https://course-ec-api.hexschool.io/api/${uuid}/admin/ec/products`;

let headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Authorization": `Bearer ${token}`,
}
fetch(url,{
    method: "GET",
    headers: headers
})
    .then((response) => {
        return response.json();
    })
    .then( (response) => {
        console.log(response);
    })
    .catch( (error) => console.log(error))

這裏顯示我的後台有2件T-shirt商品,各有不同ID。之後我想刪除第一個商品,於是我跟從六角學院刪除後台商品的API,發出DELETE請求:

const uuid = xxxxxx;
const token = xxxxxx;
const id = 'RfmTRZT3QpNZrOvZrPFyZyyYooeCHpW67WngnZ3ZPjQF6IhfFYyiJnFBuVo3coaP'
const url = `https://course-ec-api.hexschool.io/api/${uuid}/admin/ec/product/${id}`;

let headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Authorization": `Bearer ${token}`,
}

fetch(url,{
    method: "DELETE",
    headers: headers
})
    .then(response => response.json())
    .then(json => console.log(json))
    .catch( (error) => console.log(error))

成功刪除:

這時候看看network,會發現有送出OPTION請求,即是Preflight request,之後也有DELETE請求:

如果Preflight request沒有通過,那麼我的DELETE請求就不會發出去了。

總結

呼~ 終於打完最後一篇技術文了,自己對於AJAX這個題目真的很不熟,也沒有足夠接API的經驗,所以好多內容都是邊學邊寫的(擦汗),希望透過整理網上找到的內容來消化知識。雖然鐵人賽到這裏完結了,但明天我還會發一篇完賽心得,畢竟努力了30天,也需要好好反思一下自己除了技術以外,還學到什麼東西~ 感謝你的閱讀,明天再見!

参考資料

CORS, preflighted requests & OPTIONS method
輕鬆理解 Ajax 與跨來源請求
Same Origin Policy 同源政策 ! 一切安全的基礎


上一篇
JavaScript基本功修練:Day29 - axios基本語法與練習(GET、POST請求)
下一篇
JavaScript基本功修練:Day31 - 完賽了,然後呢?
系列文
JavaScript基本功修煉31

尚未有邦友留言

立即登入留言