共计 4431 个字符,预计需要花费 12 分钟才能阅读完成。
跨域资源共享(Cross-Origin Resource Sharing)是一种机制,它使用额外的 HTTP 头部告诉浏览器可以让一个 web 应用进行跨域资源请求。
请求类型
简单请求
若一个请求同时满足下述所有条件,则该请求可视为“简单请求”<span style=”color:grey”>(注:灰色字体内容了解即可)</span>:
使用的方法为
GET
HEAD
POST
手动设置的头部字段只能是(注意:也可以设置 Forbidden header name 中的头部字段,如 Connection、Accept-Encoding 等,但是设置无效)
Accept
Accept-Language
Content-Language
Content-Type(值的范围还要符合下面的要求)
<div style=”color:grey”>DPR</div>
<div style=”color:grey”>Downlink</div>
<div style=”color:grey”>Save-Data</div>
<div style=”color:grey”>Viewpoer-Width</div>
<div style=”color:grey”>Width</div>
Content-Type 的值只能为
application/x-www-form-urlencoded
multipart/form-data
text/plain
<div style=”color:grey”>No event listeners are registered on any XMLHttpRequestUpload object used in the request; these are accessed using the XMLHttpRequest.upload property.</div>
<div style=”color:grey”> No ReadableStream object is used in the request.</div>
预检请求
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> | *
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 等等。