$ 什么是CORS
CORS:Cross-Origin-Resource-Sharing:跨站点资源共享,是一种由服务端主导的,通过配置Access-Control-Allow-Origin
等内容来解决跨域的方式,可以理解为服务器端允许配置表站点网站来请求数据。
当然解决跨域的办法还有别的办法,如:
- JSONP:通过动态生成
<Script>
标签,且在其src
中嵌入真正的请求地址(仅支持get请求),在请求参数中定义好与服务端约定的回调函数名称这种方式来实现跨域。见后续 - Nginx反向代理:目前最主流的解决跨域办法之一。在Nginx代理服务器上,在
server
块的localtion /
中配置proxy_pass
,实现请求的转发来实现跨域。见后续
$ 是谁在引起跨域?
与老一辈的网页不同的是,为了提高开发效率、缩短开发周期和提高开发质量,现在大多数项目都采用的前后端分离的形式,这给跨域问题创造了天然的条件。
跨域是浏览器的安全检测行为,本与服务器端无关。从浏览器发起一个请求开始,到服务器接收和响应请求这个过程中,真正跨域作用的地方在于浏览器接收到服务器端响应的数据后,通过检查是否满足跨域的条件要求而决定是否对数据进行拦截丢弃的过程。是不是感觉浏览器在“多管闲事”呢?但其实这种设计是出于对服务器端数据安全而做的考虑。
$ 发生跨域的三个必要条件
也许你不假思索的就能回答出 不同协议
,不同域名
,不同端口
。没有问题,但并不准确,我更倾向于把这三个叫 跨域的三要素,那什么是跨域形成的必要条件呢?
- 浏览器限制: 即浏览器对跨域行为进行检测和阻止
- 触发跨域的三要素之一: 即 协议,域名和端口三个条件满足其一
-
发起的是xhr请求: 即发起的是
XMLHttpRequest
类型的请求。
其实xhr请求才是设计者们设计跨域的最关键的条件因素。并且只有同时满足三个条件才能触发跨域问题。
$ 为什么 JSONP 可以解决跨域问题
都知道jsonp能解决跨域问题,其实不太准确,因为它是绕过浏览器的安全限制策略。那他为什么能绕过浏览器的安全限制呢?
JSONP(json with padding
)方案原理就是通过动态创建script
标签,利用标签内src
属性发送同步请求,并利用回调函数的方式实现异步数据的回调从而完成与后台交互的功能。当然除了jsonp方案使用script
标签发送请求的办法外,还能通过img
标签的src
属性也同样能发送请求。
可以通过浏览器控制台Network选项查看发现,JSONP发出去的请求类型是script
,img
标签src
属性发出去的请求类型是JSON
,他们都不是 xhr
, 因为没有形成跨域的第三个条件,因此不会触发浏览器跨域检查策略。这就是为什么JSONP 方案能处理解决跨域问题的原因。
$ 发生跨域了怎么办?
前端开发者们最关心的还是如何处理跨域问题。当形成跨域条件后,我们又该如何处理?
1. 对于浏览器限制的问题
我们可以在启动浏览器的时候添加启动参数,告知浏览器不需要检查安全。实现办法是使用命令行启动如谷歌浏览器:
open -a "Google Chrome" --args --disable-web-security --user-data-dir
但这种办法只能实现在测试环节而且并不理想,这是客户端行为,针对于一个实际应用,要广大用户去做这件事情简直无稽之谈。
2. JSONP处理方式
上述已经提到为什么JSONP为什么能实现跨域,具体又该如何操作呢?JSONP是一种非官方的协议,虽说非官网但也是一种协议,他需要前后端共同遵守一个约定。假设前端有如下代码用来发送一个请求,为了方便我们使用jQuery来编写如下测试用例,我们标记dataType
为jsonp
来标识这是一个jsonp请求,jQuery会帮助我们事先动态创建script标签并设置为异步请求和发送请求等功能,并将script标签插入到html的头部上
// javascript
$.ajax({
url: base + "/get1",
dataType: "jsonp",
jsonp: 'callback', // 默认jsonp协议的约定就是callback作为回调函数,一般不修改
cache: false, // 默认为false,结果不能被缓存,它会在请求上添加随机数参数
success: function(json) {
result = json
}
})
因为需要客户端和服务器端共同遵守约定,因此服务器端也需要添加相应的代码来约定接受处理JOSNP请求,否则服务器端依旧会返回JSON对象结果从而导致浏览器接受到响应后解析到的数据与响应头格式不一致而抛出解析错误。换句话说就是服务端不知道该请求是jsonp请求。如下案例
// java
@ControllerAdvice
public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice {
public JsonpAdvice() {
super("callback") // 约定 如果请求的参数中含有callback参数,就认为是jsonp的请求
}
}
这样,服务器就能正确的识别jsonp请求,并返回js代码类型格式而不是json数据格式,从而实现执行所谓的回调函数。
虽然JSONP能处理跨域问题,但其实它存在很多弊端,主要如下:
- 需要服务器端修改代码支持: 服务器需要遵守约定导致服务器端也需要编写代码支持
- 只支持GET请求: 即使设置了请求类型为POST也无效,只支持GET
- 发送的不是XHR请求: 正因为这点才支持了跨域,然而也丢失了xhr强大的功能
因为jsonp并不能满足开发者们的开发需求,也不方便使用,因此现实中并不常用到。说句白话,学习jsonp跨域有时候确实能帮我们解决跨域问题,更多的还是用在处理面试上~
3. 解决请求式跨域【重点】
为了更好的理解这个知识点,我们先回顾一下一个普通项目的交互关系
客户端有各种各样的请求发送给中间服务器,中间服务器在接收到请求之后如果判断如果是静态资源(img,js插件等)则直接返回(绿线),如果是交互资源则转发至应用服务器上(蓝线)。
请求式跨域是最为常有也最为有效的跨域解决办法,因为前后端分离的开发模式,使得客户端和服务器端通常都在不同服务器上,这种模式解决跨域主要有两种思路
-
被调用方解决:调用方在浏览器直接将请求发送至被调用方,被调用方处理完成后,在请求响应中添加基于http协议关于跨域请求的一些规定,就是在http响应头中添加
Access-Control-Allow-Origin
等一些配置允许跨域访问。 - 调用方解决:这是基于隐藏跨域的解决办法。调用方通过一个代理服务器转发请求到被调用方的中间服务器,浏览器看到请求都是来自同一个域,就不会报跨域问题了
这两种方案虽然具有相同的效果,但思路是完全不一样的。第一种是基于解决跨域的思路,修改的是被调用方的HTTP服务器,我们在浏览器中能看到有调用方的url,也有被调用方的url;而第二种是基于隐藏跨域的思路,修改的是调用方的HTTP服务器,在浏览器中也就只能看到调用方的url。
1.被调用方解决
被调用方解决支持跨域办法: 最终的目的是在响应头增加字段
(1)在应用服务器端实现[重点] (2)在Ngnix上配置 (3) 在Apache上配置(4)Spring框架解决
本文只讲第一种 ,后三种有兴趣的同学可以搜索一下如何配置,一般都由服务端小伙伴完成。
浏览器在执行跨域请求时,如果遇到是简单请求,则先执行后判断;如果是非简单请求,则先使用OPTION发起一个预检请求【preflight request】,从而获知服务器是否允许该跨域访问,如果允许,就在此发起带真实数据的请求,否则不发起。这就实现了对被调用方的数据安全保护,也是跨域问题设计所在的目的之一
【常见简单请求】主要有一下几种:1. GET / 2. HEAD / 3. POST且它的Content-Type
为text/plain
或multipart/form-data
或application/x-www-form-urlencoded
中的一种
【常见非简单请求】1. PUT / 2. DELETE / 3. OPTIONS /4. 发送json格式的ajax请求[常为post] / 5. 带自定义Header信息的ajax请求 / 6. CONNECT / 7. TRACE / 8. PATCH 等
浏览器实现跨域判断的办法是: 当浏览器发现发起的是一个跨域的请求时,它会向请求头里增加一个
Origin
字段,当请求被响应时,浏览器会检查响应头里有没有设置允许跨域的信息,如果没有,它就会报错。
同理,如果给请求增加头信息如contentType: application/json;charset=utf-8
,那么contentType
也是会被加入到请求头里作为跨域检查信息的。
因此,在应用服务器端的响应头需要添加允许跨域的设置,即如下:
// java
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse) response;
// 允许跨域的域名,设置*表示允许除带Cookies信息的所有域名
res.addHeader("Access-Control-Allow-Origin", "http://localhost:8081");
// 允许跨域的方法,可设置*表示所有。GET/POST/OPTIONS等
res.addHeader("Access-Control-Allow-Methods", "GET");
// 假如给post请求头设置了contentType字段,则需要添加以下信息
res.addHeader("Access-Control-Allow-Headers", "Content-Type");
// 设置预检命令的缓存时效。单位是"秒"
// 如果没有失效,则不会再次发起OPTION预检请求
res.addHeader("Access-Control-Max-Age", "3600");
// 还可以有其他配置...
chain.doFilter(request, response);
}
这时,我们就可以在响应头Response Headers里观察到如下信息,这时跨域就被成功允许了。
以上方案已基本能实现在被调用方添加响应头字段来实现跨域的办法,但还有一种情况无法处理,那就是请求中带有Cookie的情况
带有Cookie的请求还需要注意一下两点才能实现跨域
-
Access-Control-Allow-Origin
的值不能为'*'而是必须是全匹配,因此需要添上具体的域名 - 打开允许Cookie的设置,即
Access-Control-Allow-Credentials: true
但是这又带出了另一个问题,就是只能支持一个域名的跨域,怎么办?其实该变量可以通过调用方的请求头信息获取,解决办法如下:
// java
HttpServletRequest req = (HttpServletRequset) request;
String origin = req.getHeader('Origin');
if (!org.springframework.util.StringUtils.isEmpty(origin)) {
// 带cookie的时候origin必须是全匹配,不能使用 *
res.addHeader("Access-Control-Allow-Orign", origin);
}
对于需要增加请求头信息解决方案与此类似
2. 调用方解决跨域:反向代理
当被调用方无法帮助解决处理跨域问题时,调用方也可以自己解决处理。其实现的办法就是利用反向代理
正向代理:利用代理客户端去请求服务器,从而隐藏了真实的客户端,服务器并不知道客户端是谁,这种代理方式称作正向代理,其代理的对象是客户端
反向代理: 反向代理隐藏了真正的服务端。举个例子,我们只知道敲下www.baidu.com
时就能访问百度搜索页面,然而背后成千上万的服务器到底是哪一台正在为我们服务我们并不知道,这种隐藏了服务器端的代理方式称作反向代理,其代理的是服务器端。软件层面上常用ngnix来做反向代理服务器,他的性能很好,用来做负载均衡。
为了实现反向代理,我们需要在ngnix中配置一个代理域名,或者称为一个网址a.com
,就像百度成千上万的服务器使用用一个代理网址www.baidu.com
一样。ngnix的配置信息如下
server {
listen 80;
server_name: a.com;
// 真正服务器的地址
location / {
proxy_pass http://localhost:8081;
}
// 代理ajax请求的url
location /ajaxserver {
proxy_pass http://localhost:8081/test/;
}
}
$ 总结
跨域是由浏览器安全限制造成
解决跨域的办法有三种,一是jsonp绕过浏览器安全检测策略,二是从被调用方配置支持跨域的请求头信息,三是从调用方利用反向代理,在ngnix或apache中配置代理域名隐藏跨域。