# CORS 字段详解

# 源 (Origin)

Web 内容的源由用于访问它的 URL 的方案(协议)、主机名(域名)和端口定义。只有当协议、主机和端口都匹配时,两个对象才具有相同的源。

# 跨源资源共享(CORS)

跨源资源共享(CORS,或通俗地译为跨域资源共享)是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其他源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。

# 简单请求

某些请求不会触发 CORS 预检请求。在废弃的 CORS 规范中称这样的请求为简单请求。

若请求满足所有下述条件,则该请求可视为简单请求

  • 请求方式为 GET HEAD POST
  • 请求标头在用户代理自动设置以及对 CCORS 安全的标头集合内。安全标头包括:accept accept-language content-language content-type range
  • content-type 需要是 text/plain multipart/form-data application/x-www-form-urlencoded
  • 如果请求是使用 XMLHttpRequest 对象发出的,不能进行任何监听
  • 请求中没有使用 ReadableStream 对象。

# 复杂请求

除了简单请求以外的请求都是复杂请求,从简单请求满足的条件来看,显然当我们遇到跨域时,基本都会产生复杂请求,因此我们需要学习如何处理复杂请求的跨域。

# 预检请求

与简单请求不同,复杂请求(“需预检的请求”)要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。

# Origin

Origin 标头字段表明预检请求或实际跨源请求的源站。

origin 参数的值为源站 URL。它不包含任何路径信息,只是服务器名称。

# Host

Host 请求标头指定了接收请求的服务器的主机名和端口号。

如果没有包含端口,则默认使用请求服务的端口(例如,HTTPS URL 默认为 443,HTTP URL 默认为 80)。

所有 HTTP/1.1 请求消息中都必须发送一个 Host 标头字段。

# Access-Control-Request-Headers

Access-Control-Request-Headers 请求标头用于浏览器在发起预检请求时告知服务器客户端在实际请求发送时可能附带的 HTTP 标头

# Access-Control-Request-Method

Access-Control-Request-Method 请求标头会由浏览器在发出预检请求时使用,通知服务器在实际请求中发出时将会采用哪种 HTTP 方法。

此标头是必需的,因为预检请求总是采用 OPTIONS 方法。

# 举例

Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type

标头字段 Access-Control-Request-Method 告知服务器,实际请求将使用 POST 方法。标头字段 Access-Control-Request-Headers 告知服务器,实际请求将携带两个自定义请求标头字段:X-PINGOTHER 与 Content-Type。服务器据此决定,该实际请求是否被允许。

# 服务端响应

# Access-Control-Allow-Origin

Access-Control-Allow-Origin 响应标头指定了该响应的资源是否被允许与给定的来源(origin)共享。

对于不包含凭据(Cookie 或 Authorization)的请求,服务器可以以“*”作为通配符。

对于包含凭据的请求,服务器必须指定一个来源。

 function configureOrigin(options, req) {
    var requestOrigin = req.headers.origin,
      headers = [],
      isAllowed;

    if (!options.origin || options.origin === '*') {
      // allow any origin
      headers.push([{
        key: 'Access-Control-Allow-Origin',
        value: '*'
      }]);
    } else if (isString(options.origin)) {
      // fixed origin
      headers.push([{
        key: 'Access-Control-Allow-Origin',
        value: options.origin
      }]);
      headers.push([{
        key: 'Vary',
        value: 'Origin'
      }]);
    } else {
      isAllowed = isOriginAllowed(requestOrigin, options.origin);
      // reflect origin
      headers.push([{
        key: 'Access-Control-Allow-Origin',
        value: isAllowed ? requestOrigin : false
      }]);
      headers.push([{
        key: 'Vary',
        value: 'Origin'
      }]);
    }

    return headers;
  }

# Vary

如果服务端指定了具体的单个源(作为允许列表的一部分,可能会根据请求的来源而动态改变)而非通配符“*”,那么响应标头中的 Vary 字段的值必须包含 Origin。

# Access-Control-Allow-Headers

Access-Control-Allow-Headers 响应标头中用于响应包含了 Access-Control-Request-Headers 的预检请求,以指示在实际请求中可以使用哪些 HTTP 标头。

 function configureAllowedHeaders(options, req) {
    var allowedHeaders = options.allowedHeaders || options.headers;
    var headers = [];

    if (!allowedHeaders) {
      allowedHeaders = req.headers['access-control-request-headers']; // .headers wasn't specified, so reflect the request headers
      headers.push([{
        key: 'Vary',
        value: 'Access-Control-Request-Headers'
      }]);
    } else if (allowedHeaders.join) {
      allowedHeaders = allowedHeaders.join(','); // .headers is an array, so turn it into a string
    }
    if (allowedHeaders && allowedHeaders.length) {
      headers.push([{
        key: 'Access-Control-Allow-Headers',
        value: allowedHeaders
      }]);
    }

    return headers;
  }

# Access-Control-Allow-Methods

Access-Control-Allow-Methods 响应标头指定了在响应预检请求时访问资源所允许的一个或多个方法。

function configureMethods(options) {
    var methods = options.methods;
    if (methods.join) {
      methods = options.methods.join(','); // .methods is an array, so turn it into a string
    }
    return {
      key: 'Access-Control-Allow-Methods',
      value: methods
    };
  }

# Access-Control-Allow-Credentials

CORS 预检请求不能包含凭据。预检请求的响应必须指定 Access-Control-Allow-Credentials: true 来表明可以携带凭据进行实际的请求。

在响应附带身份凭证的请求时,Access-Control-Allow-Origin Access-Control-Allow-Headers Access-Control-Allow-Methods 均不能设置为 “*”

 function configureCredentials(options) {
    if (options.credentials === true) {
      return {
        key: 'Access-Control-Allow-Credentials',
        value: 'true'
      };
    }
    return null;
  }

# Access-Control-Expose-Headers

响应标头 Access-Control-Expose-Headers 允许服务器指示那些响应标头可以暴露给浏览器中运行的脚本,以响应跨源请求。

在跨源访问时,XMLHttpRequest 对象的 getResponseHeader() 方法只能拿到一些最基本的响应头,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果要访问其他头,则需要服务器设置本响应头。

 function configureAllowedHeaders(options, req) {
    var allowedHeaders = options.allowedHeaders || options.headers;
    var headers = [];

    if (!allowedHeaders) {
      allowedHeaders = req.headers['access-control-request-headers']; // .headers wasn't specified, so reflect the request headers
      headers.push([{
        key: 'Vary',
        value: 'Access-Control-Request-Headers'
      }]);
    } else if (allowedHeaders.join) {
      allowedHeaders = allowedHeaders.join(','); // .headers is an array, so turn it into a string
    }
    if (allowedHeaders && allowedHeaders.length) {
      headers.push([{
        key: 'Access-Control-Allow-Headers',
        value: allowedHeaders
      }]);
    }

    return headers;
  }

# Access-Control-Max-Age

Access-Control-Max-Age 响应标头指示了预检请求(即包含在 Access-Control-Allow-Methods 和 Access-Control-Allow-Headers 标头中的信息)的结果能够被缓存多久。

 function configureMaxAge(options) {
    var maxAge = (typeof options.maxAge === 'number' || options.maxAge) && options.maxAge.toString()
    if (maxAge && maxAge.length) {
      return {
        key: 'Access-Control-Max-Age',
        value: maxAge
      };
    }
    return null;
  }

# 示例

  function cors(options, req, res, next) {
    var headers = [],
      method = req.method && req.method.toUpperCase && req.method.toUpperCase();

    if (method === 'OPTIONS') {
      // preflight
      headers.push(configureOrigin(options, req));
      headers.push(configureCredentials(options))
      headers.push(configureMethods(options))
      headers.push(configureAllowedHeaders(options, req));
      headers.push(configureMaxAge(options))
      headers.push(configureExposedHeaders(options))
      applyHeaders(headers, res);
    } else {
      // actual response
      headers.push(configureOrigin(options, req));
      headers.push(configureCredentials(options))
      headers.push(configureExposedHeaders(options))
      applyHeaders(headers, res);
      next();
    }
  }

# 参考资料

跨源资源共享(CORS) MDN (opens new window)

Node.js CORS middleware Github (opens new window)