我以前知道的可能是半成品 CORS

2020/3/22 基础知识跨域HTTP

# 什么是 CORS

跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的 Web 应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。出于安全原因,浏览器限制从脚本内发起的跨源 HTTP 请求。 例如,XMLHttpRequest 和 Fetch API 遵循同源策略。 这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源,除非响应报文包含了正确 CORS 响应头。

跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。

# 使用 CORS

# 服务器配置

直接设置响应头

app.use(async (ctx, next) => {
  ctx.set("Access-Control-Allow-Origin", ctx.headers.origin);
  ctx.set("Access-Control-Allow-Credentials", true);
  ctx.set("Access-Control-Request-Method", "PUT,POST,GET,DELETE,OPTIONS");
  ctx.set(
      "Access-Control-Allow-Headers",
      "Origin, X-Requested-With, Content-Type, Accept, cc"
  );
  if (ctx.method === "OPTIONS") {
      ctx.status = 204;
      return;
  }
  await next();
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

使用 cors 插件

const cors = require("koa-cors");

app.use(cors());
1
2
3

# 页面请求

// jquery
$.ajax({
  url: url,
  type: "POST",
  dataType: "json",
  contentType: "application/json",
  data: {},
  crossDomain: true,   // 支持跨域
  xhrFields: {
      withCredentials: true   // 支持cookie必须要带上这个
  }
});

// 原生 xml 的设置方式
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

// axios 设置方式
axios.defaults.withCredentials = true;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

看到很多文档都说 jquery 要支持跨域需要设置 crossDomain 为 true,

# 具体过程

规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

CORS 请求失败会产生错误,但是为了安全,在 JavaScript 代码层面是无法获知到底具体是哪里出了问题。你只能查看浏览器的控制台以得知具体是哪里出现了错误。

# 简单请求

某些请求不会触发 CORS 预检请求。本文称这样的请求为“简单请求”,请注意,该术语并不属于 Fetch (其中定义了 CORS)规范。需要同时满足以下条件:

  • 请求方法为 GET、POST、HEAD 之一。
  • header 中只能有 Fetch 规范定义的 CORS 安全的首部字段集合 (opens new window)
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type (需要注意额外的限制)
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • Content-Type 的值仅限于 text/plain、multipart/form-data、application/x-www-form-urlencoded 之一
  • 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。
  • 请求中没有使用 ReadableStream 对象。

通过看请求的请求头和响应头可以比较清楚的了解请求的过程。

// Request Headers

GET /info HTTP/1.1
Accept: application/json, text/plain, */*
Referer: http://localhost:9004/demo.html
Origin: http://localhost:9004
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.113 Safari/537.36


// Response Header

HTTP/1.1 200
Access-Control-Allow-Origin: *
X-XSS-Protection: 1; mode=block
Content-Security-Policy: object-src 'self'
Cache-Control: no-cache
Pragma: no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Type: application/json;charset=UTF-8
Date: Fri, 17 Apr 2020 09:39:47 GMT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

请求的 Request 和普通的请求是一致的,Response 要比普通请求多出来一个 Access-Control-Allow-Origin: *,对于跨域请求,这个响应头为*或者是 Request 中 Origin 的值,表示这个跨域请求是服务器所允许的,浏览器就会正确响应,如果响应头不正确则浏览器会报跨域的错误。

# 预请求

当请求不满足简单请求的任意一点要求即可认为是非简单请求,需要发送预请求进行访问控制。对于一个预请求需要有几项动作:

  • 基于 Allow-Origin 进行过滤排查
  • 预请求的响应头要包含必要的 Access-Control-Allow-Methods,Access-Control-Allow-Headers,如果需要 cookie 等凭据的话,还要包括 Access-Control-Allow-Credentials 头信息。

# 携带认证信息的请求

默认情况下,发送跨域请求是不会携带 cookie 等认证信息。如果需要携带认证信息,首先需要在发送请求的地方加上 withCredentials: true,这时候发送的请求就会带上认证信息。如果是简单请求就可以直接返回,如果是非简单请求就需要在预请求中设置 Access-Control-Allow-Credentials: true 告诉浏览器这个请求的认证是有效的。

# 常用响应头部字段

  • Access-Control-Allow-Origin:指定了允许访问该资源的外域 URI
  • Access-Control-Expose-Headers:让服务器把允许浏览器访问的头放入白名单
  • Access-Control-Max-Age:头指定了 preflight 请求的结果能够被缓存多久
  • Access-Control-Allow-Credentials:头指定了当浏览器的 credentials 设置为 true 时是否允许浏览器读取 response 的内容。
  • Access-Control-Allow-Methods:首部字段用于预检请求的响应。其指明了实际请求所允许使用的 HTTP 方法。
  • Access-Control-Allow-Headers:部字段用于预检请求的响应。其指明了实际请求中允许携带的首部字段。

# 请求首部字段

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

# Fetch Metadata Request Headers

# 其他

在新版的 chrome 中,如果你发送了复杂请求,你却看不到 options 请求。可以在这里设置 chrome://flags/#out-of-blink-cors 设置成 disable ,重启浏览器。对于非简单请求就能看到 options 请求了。

Fetch Metadata 请求头提案 (opens new window)