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 同源政策 ! 一切安全的基础


<<:  Day30 管线命令I

>>:  Day 28. 测试HTTP Status Code

用React刻自己的投资Dashboard Day7 - CORS与Proxy Server

tags: 2021铁人赛 React 上一篇在串接API的时候有遇到一个前端蛮常见的问题,跨来源资...

【IntelliJ IDEA 入门指南】Java 开发者的神兵利器

天下武功 唯快不破 目录 前言 IntelliJ 特点 Android 与 Python 下载与安装...

如何制作一个精美的网站

什麽是好的网站设计? 使用者使用网站时是否容易操作及有良好的动线,避免过多不必要的元素,让使用者快速...

MITRE Engenuity ATT&CK Evaluations 测试报告

才刚提到趋势科技去年在 MITRE Engenuity 的 ATT&CK Evaluatio...

系统开发生命周期(SDLC)

SDLC定义了工程系统时的阶段和过程。由於系统的多样性,它通常不提供特定的设计原则。 系统开发生命周...