# CORS

# 原理简析

# 什么是 CORS

CORS 是一个基于 HTTP 头的 W3C 标准机制 (opens new window),全称是“跨域资源共享”(Cross-origin resource sharing)。该机制通过额外的 HTTP 响应头来告诉浏览器一个页面是否允许访问来自不同源服务器上的资源,克服了 AJAX 只能使用同源的限制,从而实现跨域访问资源。

更详细的关于 CORS 的介绍,可以参考这篇MDN文档 (opens new window)

# 同源策略

同源策略是一种用于隔离潜在恶意文件、减少被攻击的重要安全机制,其限制了一个源的页面或者它加载的脚本如何能于另一个源的资源进行交互。举个例子,在一个页面中,通过 AJAX 向一个不同源的URL发送请求,浏览器检测到该请求不同源(也就跨域),则阻止请求,并在控制台输出错误。

那什么是同源?用一句话说,具有相同的协议、IP/域名、端口就可以认为是同源,只要其中一项不同,则不是同源。下表给出了一些示例:

URLs 结果 原因
http://www.company.com/
https://www.company.com/
不同源 协议不同
http://www.company.com/
http://www.company.net/
不同源 域名不同
http://www.company.com/
http://blob.company.com/
不同源 域名不同
http://www.company.com/
http://blob.company.com/
不同源 域名不同
http://www.company.com/
http://www.company.com:8080/
不同源 端口不同(http:// 默认端口是 80)
http://www.company.com/
http://www.company.com/blob.html
同源 只有路径不同
http://www.company.com/blog/index.html
http://www.company.com/blob.html
同源 只有路径不同
http://192.168.0.1/
http://192.168.1.1
不同源 IP不同

# 两种 CORS 请求

浏览器将 CORS 请求分成两类:简单请求和非简单请求。 只要一个请求同时满足以下两大条件,就属于简单请求:

(1) 请求方法是这三种方法之一: HEAD, GET, POST (2) HTTP 的头部信息不超出以下几种字段: - Accept - Accept-Language - Content-Language - Last-Event-ID - Content-Type 为 application/x-www-form-urlencoded, multipart/form-data, text/plain 三者之一

浏览器对这两种请求的处理是不一样的。

# 简单请求

对于简单请求,浏览器直接发出 CORS 请求,并在请求头之中增加一个 Origin 字段:

simple-request

Origin 字段说明了本次请求来自哪个源(协议、域名、端口), 服务器端可以根据这个值来设置 Access-Control-Allow-Origin 字段,告诉浏览器是否响应这次请求。

# 非简单请求

非简单请求和简单请求不同,在浏览器检测到一个非简单请求时,会先自动发起一次 OPTIONS 请求,也就是预检(Preflight)请求,预检请求的作用就是询问服务器端是否允许跨域,这么做通常是为了避免额外的计算逻辑。当浏览器接收到预检请求的响应内容后,浏览器会判断响应头中的 Access-Control-Allow-Origin, Access-Control-Allow-Methods 等相关字段来决定是否继续发送第二次请求。

# 几个典型的跨域错误

  1. 响应头没有包含 Access-Control-Allow-Origin

    Access to fetch at 'http://127.0.0.1:8967/1.pdf' from origin 'http://127.0.0.1:7001' has been blocked by CORS policy:
    No 'Access-Control-Allow-Origin' header is present on the requested resource. 
    If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
    
  2. Access-Control-Allow-Origin 格式错误:

    Access to fetch at '<AJAX请求目标源: 比如: http://192.168.0.1:8080>' from origin 'http://127.0.0.1:7001' has been blocked by CORS policy:
    The 'Access-Control-Allow-Origin' header contains the invalid value '<Access-Control-Allow-Origin 响应头内容>'.
    Have the server send the header with a valid value, or, if an opaque response serves your needs, 
    set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
    

    invalid access origin

  3. Access-Control-Allow-Origin 不匹配

    Access to fetch at 'http://127.0.0.1:8967/1.pdf' from origin 'http://127.0.0.1:7001' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header has a value '<Access-Control-Allow-Origin 响应头内容, 比如:http://127.0.0.1:9999>' that is not equal to the supplied origin. Have the server send the header with a valid value, or, if an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
    
  4. Access-Control-Allow-Headers 未设置

    Access to fetch at 'http://127.0.0.1:8967/1.pdf' from origin 'http://127.0.0.1:7001' has been blocked by CORS policy: Request header field range is not allowed by Access-Control-Allow-Headers in preflight response
    

    missing-allow-headers

# CORS 解决方案

解决 CORS 的方法有两种,一种是使用代理服务器避免出现跨域, 另一种则是配置 CORS 规则

# 使用代理服务器

代理服务器是一个位于客户端和目标服务器之间的中间服务器,我们可以通过代理服务器来请求第三方URL资源,由于请求是代理服务器发起的,因此浏览器不会出现CORS问题。我们可以自己搭建一个代理服务器,也可以使用第三方提供的代理服务器。这里介绍 nginx 和 nodejs 配置代理服务器的方法,这些方法都是实现同一个功能,就是将 /prefix/*的请求代理到第三方服务服务器 http://third_party.file.server 上, 同时去掉原始路径中的 /prefix 前缀。如 http://location:3000/prefix/path/to/some.pdf 请求代理后的 URL 为 http://third_party.file.server/path/to/some.pdf

# Nginx 配置代理服务器

Nginx 是一个高性能的 Web 服务器,也可以作为代理服务器使用,下面是 Nginx 中配置代理服务器的方法:

打开 nginx 配置文件,(通常位于/etc/nginx/nginx.conf, 具体情况视服务器环境决定), 找到对应的 server 块,并添加一下代码:

location ~* ^/prefix/(.*) {
    proxy_pass http://third_party.file.server/$1$is_args$args;
    proxy_redirect off;
}

上述代码指示 Nginx 在接收到路径中以 /prefix 开头的请求时,先拼凑出正确的文件URL路径,再把请求代理到第三方文件服务器上。

# Nodejs 配置代理服务器

以 Express, Koa, NestJS 为例

  1. Express

    Express 可以搭配 http-proxy-middleware 这个第三方中间件实现代理,配合 express 的路由功能,将以 prefix 开头的请求代理到第三方文件服务器上:

    const express = require('express');
    const { createProxyMiddleware } = require('http-proxy-middleware');
    const app = express();
    
    app.use('/prefix', createProxyMiddleware({
        target: 'http://third_party.file.server',
        changeOrigin: true,
        pathRewrite: {
            ['^/prefix']: ''
        }
    }));
    

    这段代码在接收到路径以 /prefix 开头的请求时,会帮我们将路径中的 /prefix/ 替换掉, 并将请求转发到 target URL 去。更多用法可以参考 https://www.npmjs.com/package/http-proxy-middleware (opens new window)

  2. Koa

    Koa 需要搭配 koa-proxy 这个第三方中间实现代理:

    const Koa = require('koa');
    const proxy = require('koa-proxy');
    const app = new Koa();
    
    app.use(
        proxy('/prefix', {
            host: 'http://third_party.file.server',
            match: /^\/prefix\//,
            map: function(path) {
                return path.replace('/prefix', ''); // 替换掉路径中的 /prefix 前缀
            }
        })
    )
    

    更多用法可以参考 https://www.npmjs.com/package/koa-proxy (opens new window)

  3. NestJS 和 express 类似,也可以搭配 http-proxy-middleware 这个第三方库实现代理:

    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { createProxyMiddleware } from 'http-proxy-middleware';
    
    async function bootstrap() {
        const app = await NestFactory.create(AppModule);
    
        // Proxy endpoints
        app.use('/prefix', createProxyMiddleware({
            target: 'http://third_party.file.server',
            changeOrigin: true,
            pathRewrite: {
                [`^/prefix`]: '',
            }
        }));
        await app.listen(3000);
    }
    bootstrap();
    

    这段代码最终效果和 Express 的示例相同。

# 配置 CORS

WebSDK 中最经常能够遇到跨域问题的接口就是 PDFViewer.openPDFByHttpRangeRequest , 为了提高PDF文档打开速度和减少文件服务器带宽,这个接口会在发送 PDF 文件请求时向文件服务器发送带有 Range 请求头的请求,并且在接收到响应后,需要根据Content-Range响应头计算文件总大小。所以在配置 CORS 时,应至少包含以下三项:

Access-Control-Allow-Headers: Range;
Access-Control-Allow-Origin: *; // 为了安全考虑,建议和请求头中的 Referer 的值相同
Access-Control-Expose-Headers: Content-Range; // 只有将加入这里的响应头 key,才能被 JS 获取到响应头的值。

下面将列举出几个不同场景下的配置方法。

# Web 服务器配置 CORS

  1. Nginx 配置 CORS

    在 nginx.conf(一般目录是 /etc/nginx/nginx.conf) 文件中添加下面的节点:

    server {
        listen 8967;
        server_name 127.0.0.1;
        charset utf8;
        location / {
            root "/path/to/files/directory/";
            if ($request_method = OPTIONS) {
                add_header 'Access-Control-Allow-Headers' 'Range';
                add_header 'Access-Control-Allow-Origin' '*';
                add_header 'Access-Control-Expose-Headers' 'Content-Range';
                return 204;
            }
            add_header 'Access-Control-Allow-Headers' 'Range';
            add_header 'Access-Control-Allow-Origin' '*';
            add_header 'Access-Control-Expose-Headers' 'Content-Range';
        }
    }
    

    上面的配置允许了所有站点都可以跨域访问资源,这是一种非常不安全但省事的做法,实际的应用场景应添加限制,比如根据访问的源设置是否允许跨域,做法如下:

    1. 添加 $cors 变量,根据 $http_origin 判断是否为合法的源, 下面的代码将允许所有 foxit.com 的子域名跨域访问资源:

      map $http_origin $cors {
          '~*^https?://.*.foxit.com$' 'true';
      }
      
    2. 添加 $allow_origin 变量,如果 $cors 值为 'true', 则表示该请求为跨域访问,则响应 Access-Control-Allow-Origin

      map $cors $allow_origin {
          'true' $http_origin;
      }
      
    3. 如此,也可以想上一步那样,在跨域访问时再指定 Access-Control-Allow-Headers 响应头

      map $cors $allow_headers {
          'true' 'Range';
      }
      
    4. 最后我们整合所有配置

      map $http_origin $cors {
          '~*^https?://.+.foxit.com$' 'true';
      }
      map $cors $allow_origin {
          'true' $http_origin;
      }
      map $cors $allow_headers {
          'true' 'Range';
      }
      map $cors $allow_expose_headers {
          'true' 'Content-Range'
      }
      server {
          listen 8967;
          server_name 127.0.0.1;
          charset utf8;
          location / {
              root "/path/to/files/directory/";
              if ($request_method = OPTIONS) {
                  add_header 'Access-Control-Allow-Headers' $allow_headers;
                  add_header 'Access-Control-Allow-Origin' $allow_origin;
                  add_header 'Access-Control-Expose-Headers' $allow_expose_headers;
                  return 204;
              }
              add_header 'Access-Control-Allow-Headers' $allow_headers;
              add_header 'Access-Control-Allow-Origin' $allow_origin;
              add_header 'Access-Control-Expose-Headers' $allow_expose_headers;
          }
      }
      

    为了确保修改是正确的,我们建议先运行 nginx -t 检查配置变化是否有误,如果无误,再运行 nginx -s reload 重新加载 nginx 服务.

  2. Tomcat 配置 CORS 下面是一个精简的 CORS 配置例子, 当然,你也可以参考 Tomcat 官方文档: http://tomcat.apache.org/tomcat-7.0-doc/config/filter.html#CORS_Filter (opens new window)

    <filter>
        <filter-name>CorsFilter</filter-name>
        <filter-class>org.apache.catalina.filters.CorsFilter</filter-class>
    </filter>
    <init-param>
        <param-name>cors.allowed.origins</param-name>
        <param-value>https://*.foxit.org</param-value>
    </init-param>
    <init-param>
        <param-name>cors.allowed.headers</param-name>
        <param-value>Range</param-value>
    </init-param>
    <init-param>
        <param-name>cors.exposed.headers</param-name>
        <param-value>Content-Range</param-value>
    </init-param>
    
    <filter-mapping>
        <filter-name>CorsFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    
  3. Apache 配置 CORS 在 Apache 中, 我们可以通过添加以下规则到服务器配置(通常位于 httpd.conf 或 apache.conf)的<Directory>,<Location>, <Files><VirtualHost>部分中来启用 CORS,:

    Header set Access-Control-Allow-Origin '*';
    Header set Access-Control-Allow-Headers 'Range';
    Header set Access-Control-Expose-Headers 'Content-Range';
    

    也可以在 .htaccess 文件中添加下面的代码:

    <IfModule mod_headers.c>
        Header set Access-Control-Allow-Origin '*';
        Header set Access-Control-Allow-Headers 'Range';
        Header set Access-Control-Expose-Headers 'Content-Range';
    </IfModule>
    

    为了确保修改是正确的,强烈建议你使用 apachectl -t 来检查配置变化是否有误,检查无误之后在运行 sudo service apache2.confapachectl -k graceful 来重新加载 Apache 服务。 注意:你也可以使用 add 代替 set 命令,但是 add 可能会导致头信息被多次添加,所以最安全的做法就是用 set.

  4. IIS 配置 CORS IIS6 和 IIS7 版本配置方式有所不同,在配置前请先确认当前使用的版本。

    1. IIS6 打开 IIS, 选择你需要配置的站点,右键进入属性对话框,选择 'HTTP 头'标签,点击 添加 按钮,然后分别添加这些响应头: Access-Control-Allow-Headers: 'Range';Access-Control-Allow-Origin: *;Access-Control-Expose-Headers: Content-Range;
    2. IIS7 将下面的配置合并或添加到你的站点配置(也就是 web.config 配置文件,如果没有的话可以新建一个)中:
    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
    <system.webServer>
    <httpProtocol>
        <customHeaders>
        <add name="Access-Control-Allow-Origin" value="*" />
        <add name="Access-Control-Allow-Headers" value="Range" />
        <add name="Access-Control-Expose-Headers" value="Content-Range" />
        </customHeaders>
    </httpProtocol>
    </system.webServer>
    </configuration>
    

# 云存储资源配置 CORS

市场上有很多云存储和CDN服务,大部分通过查阅官方文档都可以找到响应的配置方式, 这里只列举几个例子:

  1. Alibaba Cloud: https://www.alibabacloud.com/help/en/object-storage-service/latest/configure-cors (opens new window)
  2. Tencent Cloud: https://www.tencentcloud.com/document/product/436/13318 (opens new window)
  3. Google Cloud: https://cloud.google.com/storage/docs/using-cors (opens new window)
  4. Azure Storage: https://learn.microsoft.com/en-us/rest/api/storageservices/cross-origin-resource-sharing--cors--support-for-the-azure-storage-services (opens new window)
  5. AWS S3: https://docs.aws.amazon.com/AmazonS3/latest/userguide/cors.html (opens new window)

# 在服务端框架中配置 CORS

  1. Nodejs 相关框架
    1. Express: 请参考 express cors middleware (opens new window)
    2. Koa: 请参考 @koa/cors (opens new window)
    3. NestJS: 请参考 CORS|NestJS (opens new window)
  2. SpringBoot (java) 框架请参考: Enabling Cross Origin Requests for a RESTful Web Service (opens new window)
  3. Django (python) 请参考 django-cors-headers (opens new window)
  4. Laravel (php) 请参考 laravel-cors (opens new window)