跨域资源共享——CORS

7,750 阅读6分钟

跨域资源共享(Cross-Origin Resource Sharing)是一种机制,它使用额外的 HTTP 头部告诉浏览器可以让一个web应用进行跨域资源请求。

请求类型

简单请求

若一个请求同时满足下述所有条件,则该请求可视为“简单请求”(注:灰色字体内容了解即可)

  • 使用的方法为
    • GET
    • HEAD
    • POST
  • 手动设置的头部字段只能是(注意:也可以设置 Forbidden header name 中的头部字段,如 ConnectionAccept-Encoding等,但是设置无效)
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type(值的范围还要符合下面的要求)
    • `DPR`
    • `Downlink`
    • `Save-Data`
    • `Viewpoer-Width`
    • `Width`
  • Content-Type 的值只能为
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • No event listeners are registered on any `XMLHttpRequestUpload` object used in the request; these are accessed using the XMLHttpRequest.upload property.
  • No ReadableStream object is used in the request.

预检请求

CORS 预检请求发生在实际请求之前,用于检查服务器是否支持 CORS,以判断实际请求发送是否安全。预检请求使用的方式是 OPTIONS

当一个请求不是“简单请求”时,即应该先发送预检请求,比如:

  • 这个请求的请求方式不是GETHEADPOST
  • 或者,这个请求设置了自定义的头部字段,比如 X-xxx
  • 或者这个请求的 Content-Type 值不是 application/x-www-form-urlencodedmultipart/form-datatext/plain,等等

跨域请求过程

跨域请求,CORS要求服务端设置一些头部字段,最重要的一个就是 Access-Control-Allow-Origin。下面以案例进行说明,前端使用 axios 进行 http 传输,后端以 koa 作为服务端框架,并使用CORS中间件 koa2-cors

简单跨域请求

// Client http://localhost:8080
simpleRequest() {
  axios({
    method: 'GET',
    url: 'http://localhost:3000/api/simple'
  }).then(data => {
    console.log(data);
  });
}
// Server http://localhost:3000
app.use(cors());
router.get('/api/simple', ctx => {
  ctx.body = { result: 'simple request success' };
});

HTTP 报文:

HTTP 请求头部有个 Origin 字段,表示请求来自哪里。HTTP 响应头部中的 Access-Control-Allow-Origin 表示哪个域可以访问该资源。使用 OriginAccess-Control-Allow-Origin 就完成了最简单的访问控制。

预检请求&正式请求

// Client http://localhost:8080
mainRequest() {
  axios({
    method: 'POST',
    url: 'http://localhost:3000/api/mainRequest',
    headers: { 'X-test': 'CORS' } // 增加一个自定义的头部字段,触发预检请求
  }).then(data => {
    console.log(data);
  });
}
// Server http://localhost:3000
app.use(cors());
router.post('/api/mainRequest', ctx => {
  ctx.body = { result: 'main request success' };
});

预检请求的报文:

请求首部字段 Access-Control-Request-Method 告知服务器,实际请求将使用 POST 方法。 请求首部字段 Access-Control-Request-Headers 告知服务器,实际请求将携带一个自定义请求首部字段:x-test。服务器据此决定,该实际请求是否被允许。

响应首部字段 Access-Control-Allow-Methods 表明服务器允许客户端使用哪些方法发起请求。 响应首部字段 Access-Control-Allow-Headers 表明服务器允许请求中携带字段 x-test。

实际请求的报文:

实际请求中发送了 X-test 头部字段,响应状态码 200 OK。

可以看到,预检请求中 Client 和 Server 使用了更多的头部字段来完成访问控制。那么,CORS 相关的请求头部字段和响应头部字段共有哪些呢?

头部字段

HTTP 请求头部字段

  • Origin
    Origin 头部字段表示预检请求或实际请求的源站。
  • Access-Control-Request-Method
    Access-Control-Request-Method 头部字段用于预检请求。其作用是,将实际请求所使用的 HTTP 方法告诉服务器。
  • Access-Control-Request-Headers
    Access-Control-Request-Headers 头部字段用于预检请求。其作用是,将实际请求所携带的首部字段告诉服务器。

注意,以上请求头部字段无须手动设置,当使用 XMLHttpRequest 对象发起跨域请求时,它们已经被设置就绪。

HTTP 响应头部字段

  • Access-Control-Allow-Origin
    其语法如下:

    Access-Control-Allow-Origin: <origin> | *  
    

    origin 参数的值指定了允许访问该资源的外域 URI。如果该字段的值为通配符 *,则表示允许来自所有域的请求。
    注意,如果服务端指定了具体的域名而非 *,那么响应头部中的 Vary 字段的值必须包含 Origin。这将告诉客户端:服务器对不同的源站返回不同的内容。

  • Access-Control-Allow-Methods
    Access-Control-Allow-Methods 头部字段用于预检请求的响应。其指明了实际请求所允许使用的 HTTP 方法。

  • Access-Control-Allow-Headers
    Access-Control-Allow-Headers 头部字段用于预检请求的响应。其指明了实际请求中允许携带的首部字段。

  • Access-Control-Expose-Headers
    跨域请求中,浏览器默认情况下通过API只能获取到以下响应头部字段:

    • Cache-Control
    • Content-Language
    • Content-Type
    • Expires
    • Last-Modified
    • Pragma

    如果想要访问其他响应头部信息,则需要在服务器端设置 Access-Control-Allow-HeadersAccess-Control-Expose-Headers 让服务器把允许浏览器访问的头部字段放入白名单,比如:

    Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
    

    这样浏览器就能够访问到 X-My-Custom-HeaderX-Another-Custom-Header 响应头部了。

  • Access-Control-Max-Age
    Access-Control-Max-Age 字段指定了预检请求的结果能够被缓存多久,单位是 ,比如:

    Access-Control-Max-Age: 5
    

    表示在第一次预检请求发出后,5s 内再访问该接口时会直接发送实际请求,而不需要先发预检请求。过了 5s 后,会再要求先发送预检请求,以此类推。

    app.use(
      cors({
        maxAge: 5
      })
    );
    

    服务端设置了 5s 缓存,实际请求如下:

    注意,如果设置缓存后,发现每次还是会发送 OPTIONS 请求,请检查你是不是勾选了“禁止缓存”。

  • Access-Control-Allow-Credentials
    XMLHttpRequest.withCredentials (或者 Request.credentials)表示跨域请求中,user agent 是否应该发送 cookies、authorization headers 或者 TLS client certificates 等凭据。 Access-Control-Allow-Credentials 的作用就是:当 credentials 为 “真” 时(XHR和Fetch设置方式不一样),Access-Control-Allow-Credentials 告诉浏览器是否把响应内容暴露给前端 JS 代码。比如:

    // Client http://localhost:8080
    simpleRequest() {
      axios({
        method: 'GET',
        url: 'http://localhost:3000/api/simple',
        withCredentials: true // 增加了withCredentials 选项
      }).then(data => {
        console.log(data);
      });
    }  
    
    // Server http://localhost:3000
    app.use(
      cors({
        maxAge: 5,
        // credentials: true
      })
    );
    

    此时,服务端未设置 credentials: true,发起请求能看到客户端报错:

    如果服务端设置了 credentials: true 则客户端就不会报错了。

    预检请求的时候,Access-Control-Allow-Credentials 响应头部字段表示实际请求中是否可以使用 credentials。

关于 CORS 响应头部字段的运用,建议看一下 koa2-cors 中间件的源码。代码只有几十行,特别清晰易懂。


CORS 相关内容如上,了解之后能更好地帮助我们解决日常联调中出现的问题,比如:出现跨域了服务端怎么设置,axios.post 方法发送一个对象时为什么会出现 OPTIONS 请求,代理服务器怎么才能转发cookies等等。