經過這幾天學習AJAX,對於接API開始有點認識了,雖然有把一些例子順利寫出來跟大家分享,但是背後也曾經出了不少奇怪問題,例如以下經典問題:
fetch('https://www.facebook.com/')
.then( (response) => console.log(response))
.catch( (error) => console.log(error))
那時候馬上google查找答案,發現一堆術語,什麼同源政策、CORS、JSONP之類的,但當時真的沒有時間消化好這些知識(現在文章都是當天學當天寫當天發QQ),所以即使在鐵人賽最後一天,本來用來寫心得的,只好乖乖來繼續寫技術文了,完賽心得就留待明天再發囉~
回到重點,這篇文章會整理以下知識:
這裏推薦Huli老師關於AJAX的文章,以及卡斯伯老師使用dev tool檢查要求的文章,對新手的我非常有幫忙!
簡單講就是,自己網站的資源不能被別人存取或修改。
如果我從目前瀏覽器的網頁向跟自己「不同源」的網址發出請求和存取資料,就是被視作「跨來源存取」,一般情況下是不允許的,只有「同源」才會被允許。
原因:這是基於網絡安全的考量,避免有駭客惡意呼叫其他人的網絡服務。若沒有這個政策保護,別人就可以任意修改和存取你網頁裏的資源了。
同源政策有兩種:
這裏會集中講第一點,DOM同源政策。
什麼是DOM?在瀏覽器裏載入的所有圖片、文字、程式碼等等的資源,會變成一個個DOM元素。同源政策會禁止我去存取別人網站裏的DOM元素,即是別人網站裏的網絡資源。
那麼什麼同源(Same-Origin)?
要判斷是否同源,就看這兩個網址在以下的部分是否相同:
MDN的例子:
所以簡單講,不同domain就是不同源,http
和https
就是不同源,port
不同就是不同源。當我們接別人的API時,多數就是不同源的情況。
要注意一點:我的請求(request)的確是有發出去,我的瀏覽器之後也收到回應(response)。但多得瀏覽器的同源政策,它把回應擋下來了,不會把回拿到的回應掉給我的JavaScript去做另一些的處理。
但在某些情況下,即使兩個網站是「不同源」,也可以允許存取的。例如以下情況:
跨來源寫入:
例如允許:表單送出(form)、連結(link)、重新導向(redirect)
跨來源嵌入:
例如允許:嵌入圖片<img>
、影片<video>
、<iframe>
、放在<script>
裏的程式碼、CSS stylesheet <link rel="stylesheet" href="...">
等等。然而,雖然我的網頁可以顯示到這些資源,但我的JavaScript並不能讀取這些資源的內容。
fetch
和XMLhttprequest
都是會跟從同源政策,我們再次看這張圖:
裏面有一個關鍵: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(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會帶有一些關於我想發的請求的一些資訊,例如我將會使用的HTTP請求方法(GET、POST...)、Authorization
等等。
什麼時候會使瀏覽器發出Preflight request呢?當我發出的請求不是簡單請求時,就會觸發Preflight request,當Preflight request被通過,我本身的請求才會被發出。簡單請求有一堆定義,例如請求要是GET
、HEAD
或POST
、headers
其中一個,詳細請看這個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 同源政策 ! 一切安全的基礎