解决CORS跨域资源共享问题

2022年9月13日 · 9 months 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 报错。

参考资料