CORS学习分享


http://localhost:93/' from origin 'http://localhost:91' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

在前端用Ajax(XMLHttpRequest或支持fetch)请求服务端API的时候,应该都有碰到过这个这个错误,那为什么会会有这个错误呢?

其实是浏览器因为安全性的考量,有一个东西叫做同源策略

http://example.com/app1/index.html http://example.com/app1/index.html 同源 协议、主机和端口号都一样 2 http://Example.com:80 http://example.com 同源 服务器默认通过端口 80 传递 HTTP 内容 3 http://example.com/app1 https://example.com/app2 不同源 协议不一样 4 http://example.com http://www.example.com 不同源 主机不一样 5 http://example.com http://example.com:8080 不同源 端口不一样

第4点是大家要特别注意的一点,domain 跟subdomain 之间也是不同源的,所以 example.comwww.example.com 不同源。有很多人常常会把这个跟cookie 搞混,因为 example.comwww.example.com 是可以共用cookie 的。在这边特别强调,cookie 比对的规则叫做:Domain Matching ,它是看domain 而不是看我们这边所定义的origin,千万不要搞混了。

从以上范例可以得知,其实要达成同源满困难的,如果只看网址的话,基本上要长得一模一样,只有path 跟后面的部分可以不一样。

CORS

已经知道了同源策略,与之相对的,如果你想实现在不同源之间进行HTTP传输数据的话,你应该怎么做?这规范就叫做CORS。

Run Chrome browser without CORS,其他浏览器的话就要自己查一下相关资料了。

把安全机制关掉以后,就可以顺利拿到response,浏览器也会跳一个提示出来:

安全提示

问题是解决了,但为什么我说这是治标不治本呢?因为只有在你电脑上没问题而已,在其他人的电脑上面还是有问题。而且你关掉的不只是CORS,你连其他安全机制也一起关掉了。总之呢,只是跟大家介绍有这个解法,但不推荐使用。

http://localhost:91,跟 http://localhost:93 不同源):

     
xxxxxxxxxx
       
fetch('http://localhost:93').then(res => {
console.log('response', res);
return res.text();
}).then(body => {
console.log('body' , body)
})
   

你就会看到console 上面跳出显眼的红字:

前半段很熟悉,后半段可能就比较陌生一点。但没关系,我们看到了关键字:set the request's mode to 'no-cors',喔喔喔,难道说这样就可以不管CORS 吗?马上来试试看:

     
xxxxxxxxxx
       
fetch('http://localhost:93', {
mode: 'no-cors'
}).then(res => {
console.log('response', res);
return res.text();
}).then(body => {
console.log('body' , body)
})
   

添加mode: 'no-cors'之后重新执行,果真不会跳错误出来了!console 一片干净,只是印出来的值似乎怪怪的:

Response 的status 是0,body 的内容是空的,type 是一个叫做 opaque (不透明)的东西,看起来很奇怪。但如果我们切到Network 的那一个tab 去看,会发现其实后端是有回传response 的。所以设置这个mode 以后,并不会神奇地就让你可以突破限制拿到东西,正好相反,这个模式是在跟浏览器说:「我就是要发request 给一个没有cors header 的资源,我知道我拿不到response,所以你绝对不要给我response」。这就是一个自欺欺人的方法;

http://localhost:93/' from origin 'http://localhost:91' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

重点是这一句:No 'Access-Control-Allow-Origin' header is present on the requested resource

刚刚有提到说后端才是拥有权限的那一方,可以告诉浏览器:「我允许这个origin 跨来源存取我的资源」,而告诉浏览器的方法,就是在response 加上一个header。

这个header 的名称叫做Access-Control-Allow-Origin,内容就是你想要放行的origin,例如说:Access-Control-Allow-Origin: http://127.0.0.1:91,这样就是允许 http://127.0.0.1:91 的跨来源请求。

如果只想针对特定的origin 开放权限,只要传入要开放的origin 就行了。放*,就代表允许任何origin 的意思。如果想要针对多个origin,server 那边必须做一点额外处理。

这才是从根本去解决跨来源请求的问题的方式,如果你跟存取资源的服务端有合作关系的话,直接请他们设定这个header 就行了。但是时候,你可能会想从「跟你没有合作关系」的服务端获取数据,例如说你想去抓 google.com 的内容之类的,这些资源绝对不会给你 Access-Control-Allow-Origin 这个header。

这时候怎么办呢?

让我们欢迎proxy server 登场!

Content-Type头值须为application/x-www-form-urlencodedmultipart/form-datatext/plain其中之一;
  • HTTP请求中没有使用自定义的标头(如X-token等)。
  • Content-Type的值非application/x-www-form-urlencodedmultipart/form-datatext/plain 其中之一;

  • 带有自定义的 HTTP 标头(如X-token)的请求;

  • 执行修改服务器数据的方法时,例如PUTDELETEPATCH方法。

    现在我们写一个用JSON 的方式传数据到后端的代码:

    浏览器运行后,切到Network tab 去看request 的状况,发现除了原本预期的POST 以外,还多了一个方法为OPTIONS 的预检请求,浏览器会帮忙带上两个header:

    image-20220223205629438

  • 预检请求的作用就是查看服务器是否支持当前的跨域请求。预检请求的response 需要明确用 Access-Control-Allow-Headers 来表明:「我愿意接受这个header」,浏览器才会判断预检通过。

    而在这个案例中,content-type就属于自定义header,所以后端必须明确表示愿意接受这个header:

         
    xxxxxxxxxx
           
    res.header('Access-Control-Allow-Headers', 'Content-Type');
       

    如此一来,前端那边就可以顺利通过preflight request,只有在通过preflight 之后,真正的那个request 才会真正发出。

    CORS 完全手册