作为一个前端开发工程师,每天都要对接端口,如果你对跨域知识不了解,不知道什么是 CORS,那么我敢肯定你会经常遇到一些下面图片中的报错,对于如何解决却摸不着头脑,本文就从 CORS 入手介绍一下「跨域」的前世今生,让你面对跨域问题时再也不用发愁。
本篇文章主要分为两个部分:
- 跨域的原理
- CORS 如何调试
为什么会有跨域问题出现?
根本原因:浏览器通过同源策略来防止恶意网站窃取数据
这里我们需要了解这些前提:
什么是同源策略
同源是指:协议、域名、端口都相同
非同源的资源之间不能相互通信
同源政策的目的
是为了保证用户信息的安全,防止恶意的网站窃取数据。
同源策略限制了什么
如果非同源,共有三种行为受到限制
- Cookie、LocalStorage 和 IndexDB 无法读取
- DOM 无法获得
- AJAX 请求不能发送
矛盾冲突:不同源之间的合理通信也会受到限制
虽然这些限制是必要的,但是有时很不方便,合理的用途也受到影响。
解决方案:CORS、代理 和其他几种解决方案
下面详细介绍CORS和代理两种方式(最通用,最贴合我们的业务场景)
方案一:CORS
现在项目研发一般都是前后端分离的模式,前后端代码在上线的时候也是分开部署的,这个时候就肯定会涉及到跨域问题,CORS 是一种简洁,简单,标准的跨域请求方式,要发出 CORS 请求,首先我们要先了解这些前置知识:
前置知识
什么是 CORS
CORS是一种W3C标准,它允许服务器通过一些自定义的头部来限制哪些源可以访问它自身的资源。
CORS还规定对于一些可能对服务器数据产生副作用的http请求,需要在正式请求被发出之前,先发送一个options的预检请求,获知服务器是否允许该跨源请求。
注意:跨域并非是浏览器限制了发起请求,也可能是跨域请求其实可以发出去,但是返回结果被浏览器拦截了
非简单请求正式发送前,浏览器会帮忙发出一个预检请求
上面说了,CORS还规定对于一些可能对服务器数据产生副作用的http请求,需要在正式请求被发出之前,先发送一个options的预检请求,获知服务器是否允许该跨源请求。
预请求 发生在下列情况中:
- 使用GET或POST以外的方法;
- 利用POST发送application/x-www-form-urlencoded, multipart/form-data, or text/plain之外的Content-Type;例如,post body的Content-type为application/xml
- 发送自定义的头信息,如x-pagination-count
操作方式
其实,实现 CORS 通信的关键在于 服务器。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
那么服务端应该怎么做呢?
服务器需要在响应头部中配置一些与跨源有关的字段
Access-Control-Allow-Origin
origin 参数的值指定了允许访问该资源的外域 URI。
对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符*,表示允许来自所有域的请求
Access-Control-Expose-Headers
在跨域访问时,XMLHttpRequest对象的getResponseHeader()方法只能拿到一些最基本的响应头,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果要访问其他头,则需要服务器设置本响应头
例如前后端协作时,分页信息要储存在header头部,需要设置 x-pagination-count
Access-Control-Allow-Headers
指明了实际请求中允许携带的首部字段
Access-Control-Allow-Credentials
指定了当浏览器的credentials设置为true时是否允许浏览器读取response的内容。
当用在对preflight预检测请求的响应中时,它指定了实际的请求是否可以使用credentials。
请注意:简单 GET 请求不会被预检;如果对此类请求的响应中不包含该字段,这个响应将被忽略掉,并且浏览器也不会将相应内容返回给网页。
Access-Control-Max-Age
指定了preflight请求的结果能够被缓存多久(多少秒)
Access-Control-Allow-Methods
指明了实际请求所允许使用的 HTTP 方法
Access-Control-Allow-Headers 和 Access-Control-Expose-Headers 的区别
access-control-allow-headers:实际request请求中可以带上哪些头部
access-control-expose-headers:指的是浏览器发出 the actual request 得到 response, 浏览器可以使用/读取哪些 response 中的 headers
注意: cookie 始终遵循同源策略
需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为*,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。
方案二:代理
这种方案可以用于解决开发环境的跨域问题,如果你的系统上线部署时是前后端同源部署,生产环境不会出现跨域问题,那就可以用这种方案来解决开发调试的问题。
这种方式是成本最小(不需要服务端的参与,前端可以自己解决)的一种解决跨域的方案。
综上,这个跨域方案可以基于以下两个条件来使用:
- 生产环境,前后端同源部署
- 后端还没有配置CORS,你就可以自己撸起袖子上,不用block在别人那边
原理
代理的原理是开发时客户端这边启动一个服务端来拦截客户端发出去的请求,该服务端代为发送请求至真实的服务端,这样通信就是发生在服务端和服务端之间,也就不会出现所谓的跨域问题。
操作方式
其他方案
在 CORS 规范出来之前,还有一些其他的跨域解决方案(不推荐使用)
- document.domain + iframe
- 动态创建script
- location.hash + iframe
- window.name + iframe
- postMessage
- JSONP
- web sockets
其实这些解决方案在CORS这个标准出现之后就比较少有人用了,我也没有真正使用过这几个方案,这里不过多讨论,感兴趣的可以参考 这篇文章 看下
CORS 的调试
分析headers
大部分的 CORS 错误都可以归因与请求头和响应头的不匹配。
所以,CORS 出现错误,分析的第一个动作就是分析 headers,大部分的 CORS 错误都可以归因与请求头和响应头的不匹配,例如:
请求头中定义了 Access-Control-Request-Method: POST 以及 Access-Control-Request-Headers: authorization,content-type, 但是服务端没有与之匹配的响应头,所以报错。
辅助工具
浏览器的开发者工具
需要关注 console 和 network 两个面板,这个是我们最常用的方法,以后可以收集一些常见报错放上来,现在不多赘述。
抓包工具
简单请求(没有预检请求的那种)浏览器是不会给你展示具体的请求信息(响应头和请求头),这个时候你要怎么查看头部信息来定位问题呢?可以使用网络抓包工具,常用的有 Wireshark或Fiddler。
(使用教程网上很多,可以自行搜索查看)
curl
要用curl来模拟跨域请求(其实关键是模拟options请求),我们需要先来观察一下options请求的特征:
- 具有OPTIONS的HTTP方法
- Origin标头
- Access-Control-Request-Method标头。
所以要使用curl模拟预检请求,我们需要模仿这三个特征。
(同样的道理也适用于postman,反正关键是你要设定正确的请求头才能模拟出 options请求)
curl --verbose -H "Origin: http://localhost:9090" -H "Access-Control-Request-Method: POST" -X OPTIONS http://xxx.xxx.xxx:8080/a/b
下图是curl的全部内容:
反正这里的请求结果里,如果响应的状态码不是200,那你就大胆地去和后端 battle 吧(当然前提是你已经正确设置了请求头,模拟出来的确实是options请求)
总结
无论使用什么辅助工具,其实总结起来就是三步:
- 关注请求和响应头
- 比较headers以查看是否存在不匹配的
- 解决问题
- 更新客户端的 request headers 以发送正确的请求
- 更新服务器的 response headers 以允许客户端来请求