跨域资源共享 (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 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. 下面以案例进行说明, 前端使用 https://github.com/axios/axios 进行 http 传输, 后端以 https://github.com/koajs/koa 作为服务端框架, 并使用 CORS 中间件 https://github.com/zadzbw/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 响应头部字段的运用, 建议看一下 https://github.com/zadzbw/koa2-cors 中间件的源码. 代码只有几十行, 特别清晰易懂.
CORS 相关内容如上, 了解之后能更好地帮助我们解决日常联调中出现的问题, 比如: 出现跨域了服务端怎么设置, axios.post 方法发送一个对象时为什么会出现 OPTIONS 请求, 代理服务器怎么才能转发 cookies 等等.
来源: https://juejin.im/post/5bfa5f8e6fb9a049a7117b67