解决CORS跨域资源共享问题

2022年9月13日 · 2 years ago

因为浏览器的同源安全策略,我们在访问一个网站的时候,只能请求同一个域名,同一种协议下的JS文件。 上图如果我们开启了CORS检查,在打开 domain-a.com 页面时请求 domain-a.com 域名的所有资源都很正常,但是请求 domain-b.com 的东西就会被拒绝。 所以当我们需要跨域请求时就很麻烦了。以前我们使用 JSONP 来解决跨域问题,大致是在 DOM 中加入一个 <script> Tag,由它去请求跨域资源,请求结束后会调用 callback 参数指定的函数,类似这样:

let s = document.createElement("script");
s.src = "jsonp_demo_db.php?callback=myDisplayFunction";
document.body.appendChild(s);

// 函数myDisplayFunction会被调用

后来(2009年)有了Cross-Origin Resource Sharing(CORS, 跨域资源共享)之后就不再需要 JSONP 了。 CORS的工作原理很简单,被请求的服务器在Response Header里告诉客户端(这里是浏览器)是否允许被跨域请求,以及允许的域名是哪些,如果不符合条件就拒绝: 比如上图,服务器返回 Access-Control-Allow-Origin: * 意味着允许任意域名请求该资源。这样即使从 domain-a.com 的页面请求 domain-b.com 的资源也可以正常放过。

1. Nginx 修改支持跨域

如果 domain-a.com 在自己手里那修改 http repsonse header 是小事,比如修改 nginx 配置文件:

location ~* \.(eot|ttf|woff|woff2)$ {
    add_header Access-Control-Allow-Origin *;
}

2. CDN 修改支持跨域

我上一次遇到 CORS 错误是在 domain-a.com 请求托管在 domain-cdn.com 的资源的时候,修改配置稍微有点麻烦。 以腾讯云 CDN 为例,这里有两层服务。一个是对象存储(Cloud Object Storage),即文件的实际存储功能;另一个是内容分发网络(Content Delivery Network,CDN),即用户实际接入的CDN节点。 以前腾讯云的对象存储支持默认域名访问,官方也提供了跨域访问 CORS 设置。有些 Google CORS 错误的结果会告诉你到这里设置,但其实这并不是用户实际接入的网络,所以配置也没什么用(以前可以单独使用默认cos域名访问对象存储内容,现在不支持了,都需要自己接入自定义域名)。 所以要解决用户端的CORS错误,我们需要修改自定义CDN域名的配置。 在内容分发网络CDN → 域名管理 → 高级配置 → HTTP响应头配置,这个地方新增规则,可以修改CDN服务器的HTTP response。

3. 有哪些Response Headers可以设置?

  • Access-Control-Allow-Origin 允许跨域访问的域名, * 表示任意域名。
  • Access-Control-Allow-Methods 允许的HTTP Method,比如 GET/POST。
  • Access-Control-Allow-Credentials 如果客户端发上来的Request credential设置为include,则只有当我们把Access-Control-Allow-Credentials设置为true时,浏览器才会把response暴露给前端JS。
  • Access-Control-Expose-Headers 告诉浏览器哪些response headers可以暴露给前端JS。
  • Access-Control-Max-Age 一个CORS preflight request的有效期。浏览器会自动发起 CORS 预检请求(preflight requests)给服务器,检查是否支持跨域访问。比如在客户端发起一个真正的 DELETE 请求之前,先preflight一下,如果服务器说允许,浏览器再发起真正的请求。
  • Access-Control-Allow-Methods 允许的跨越请求Methods,*表示都允许。比如:
    Access-Control-Allow-Methods: POST, GET, OPTIONS
    Access-Control-Allow-Methods: *
    
  • Access-Control-Allow-Headers 可以支持自定义response header。有几个安全的响应头无需列出,其他的可以自行设置,比如:
    Access-Control-Allow-Headers: X-Custom-Header, Upgrade-Insecure-Requests
    

4. 为什么 tag不设置type="module"就不会有CORS报错?

tag 加了 type="module" 就意味着这是一个module script,相对的,不带这个属性则为classic script。我们可以写个简单的 Demo 测试一下:

    <!doctype html>
    <html lang="en">

    <head>
        <meta charset="UTF-8">
    </head>

    <body>
        <a href="#" id="test-link">Test CORS</a>
        <script type="module" crossorigin>
            function test() {
                    var url = 'https://some-domain.com/some.js';
                var xhr = new XMLHttpRequest();
                xhr.open('HEAD', url);
                xhr.onload = function () {
                    var headers = xhr.getAllResponseHeaders().replace(/\r\n/g, '\n');
                    alert('request success, CORS allow.\n' +
                        'url: ' + url + '\n' +
                        'status: ' + xhr.status + '\n' +
                        'headers:\n' + headers);
                };
                xhr.onerror = function () {
                    alert('request error, maybe CORS error.');
                };
                xhr.send();
            }

            const link = document.getElementById("test-link")
            link.addEventListener('click', function() {
                test()
            })
        </script>
    </body>

    </html>

如果 'https://some-domain.com/some.js' 不支持CORS response headers,浏览器就会报错。 原因是浏览器支持 module scirpts 时规定了必须使用 CORS 协议作为跨域资源请求,可以参考这里

Unlike classic scripts, module scripts require the use of the CORS protocol for cross-origin fetching.

所以同样的代码,如果不使用 <script type="module"></script>,就不会触发 CORS 报错。

参考资料