跨域资源共享(Cross-Origin Resource Sharing)是一种机制,它使用额外的 HTTP 头部告诉浏览器可以让一个web应用进行跨域资源请求。
请求类型
简单请求
若一个请求同时满足下述所有条件,则该请求可视为“简单请求”(注:灰色字体内容了解即可):
-
使用的方法为
GET
HEAD
POST
-
手动设置的头部字段只能是(注意:也可以设置 Forbidden header name 中的头部字段,如
Connection
、Accept-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
。
当一个请求不是“简单请求”时,即应该先发送预检请求,比如:
- 这个请求的请求方式不是
GET
、HEAD
、POST
- 或者,这个请求设置了自定义的头部字段,比如
X-xxx
- 或者这个请求的
Content-Type
值不是application/x-www-form-urlencoded
、multipart/form-data
、text/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
表示哪个域可以访问该资源。使用 Origin
和 Access-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 参数的值指定了允许访问该资源的外域 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-Headers
。Access-Control-Expose-Headers
让服务器把允许浏览器访问的头部字段放入白名单,比如:
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
这样浏览器就能够访问到 X-My-Custom-Header
和 X-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等等。