发生跨域的三个必要条件
大白话解释:所谓跨域其实就是浏览器对某些请求进行拦截,不允许访问,所以浏览器就是一个中间桥梁一样,它说让你通过就让你通过。它说不让你通过那就不让你通过。
发生跨域的三个必要条件:
也许你不假思索的就能回答出 不同协议
,不同域名
,不同端口
。没有问题,但并不准确,我更倾向于把这三个叫 跨域的三要素,那什么是跨域形成的必要条件呢?
- 浏览器限制: 即浏览器对跨域行为进行检测和阻止
- 触发跨域的三要素之一: 即 协议,域名和端口三个条件满足其一
- 发起的是xhr请求: 即发起的是
XMLHttpRequest
类型的请求。
解决请求式跨域【重点】
为了更好的理解这个知识点,我们先回顾一下一个普通项目的交互关系
客户端有各种各样的请求发送给中间服务器,中间服务器在接收到请求之后如果判断如果是静态资源(img,js插件等)则直接返回(绿线),如果是交互资源(例如访问@RequestMapping里的方法)则转发至应用服务器上(蓝线)。
请求式跨域是最为常有也最为有效的跨域解决办法,因为前后端分离的开发模式,使得客户端和服务器端通常都在不同服务器上,这种模式解决跨域主要有两种思路
- 被调用方解决:调用方在浏览器直接将请求发送至被调用方,被调用方处理完成后,在请求响应中添加基于http协议关于跨域请求的一些规定,就是在http响应头中添加
Access-Control-Allow-Origin
等一些配置允许跨域访问。
// 允许跨域的域名,设置*表示允许除带Cookies信息的所有域名
response.addHeader("Access-Control-Allow-Origin", "*");
- 调用方解决:这是基于隐藏跨域的解决办法。调用方通过一个代理服务器转发请求到被调用方的中间服务器(解释:在调用方有一个nginx做代理,然后转换成和“被调用方”一样的域名端口即可,然后通过浏览器去访问“被调用方”),浏览器看到请求都是来自同一个域,就不会报跨域问题了
这两种方案虽然具有相同的效果,但思路是完全不一样的。第一种是基于解决跨域的思路,修改的是被调用方的HTTP服务器(解释:因为是在被调用方的servlet代码里面写response.addHeader("Access-Control-Allow-Origin", "*");
),我们在浏览器中能看到有调用方的url,也有被调用方的url;而第二种是基于隐藏跨域的思路,修改的是调用方的HTTP服务器(解释:调用方的请求url是经过调用方的nginx做反向代理转换转换的,目的就是为了和‘’被调用方”的域名端口一致),在浏览器中也就只能看到调用方的url。
接下来,咱们再看一副架构图吧!
讲解:首先看到“调用方”和“被调用方”都是可以加nginx服务器的。
如果是在“调用方”解决跨域问题,由图可知,是在调用方加上nginx,然后让nginx做反向代理,把域名和端口代理成和“被调用方”的一致。
被调用方解决
如果是在“被调用方”解决跨域问题,先扩展一下,被调用方解决支持跨域办法如下:
(1)在应用服务器端实现[重点] (2)在Ngnix上配置 (3) 在Apache上配置(4)Spring框架解决
虽然有4种方案,但是这4种方案的本质都是一样的, 最终的目的是在响应头增加字段
浏览器在执行跨域请求时,如果遇到是简单请求,则先执行后判断;如果是非简单请求,则先使用OPTION向服务器发起一个预检请求【preflight request】,从而获知服务器是否允许该跨域访问,如果允许,就在此发起带真实数据的请求,否则不发起。这就实现了对被调用方的数据安全保护,也是跨域问题设计所在的目的之一
【常见简单请求】主要有一下几种:1. GET 2. HEAD 3. POST
且它的Content-Type
(解释: Content-Type是指http/https发送信息至服务器时的内容编码类型)为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
也是会被加入到请求头里作为跨域检查信息的。
我们可以发现上图里面的Origin和host的端口不一致,原因是咱们的项目是前后端分离,所以需要跨域请求,目标网站是8081端口,所以咱们通过nginx做了一层反向代理,把咱们本地的8080端口代理成8081端口,然后再去访问目标网站
因此,在应用服务器端的响应头需要添加允许跨域的设置,即如下:
// 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);
}
对于需要增加请求头信息解决方案与此类似
调用方解决跨域:反向代理
当被调用方无法帮助解决处理跨域问题时,调用方也可以自己解决处理。其实现的办法就是利用反向代理
正向代理:利用代理客户端去请求服务器,从而隐藏了真实的客户端,服务器并不知道客户端是谁,这种代理方式称作正向代理,其代理的对象是客户端
反向代理: 反向代理隐藏了真正的服务端。举个例子,我们只知道敲下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/;
}
}
讲解:
listen 80;
server_name: a.com;
的作用是说,凡是我们的请求域名是a.com,端口是80的,都会被nginx做代理
location / {
proxy_pass http://localhost:8081;
}
// 代理ajax请求的url
location /ajaxserver {
proxy_pass http://localhost:8081/test/;
}
的意思,我举个例子就明白了,http://a.com:80/
就会跳转
location / {
proxy_pass http://localhost:8081;
}
如果是请求http://a.com:80/ajaxserver
就会跳转到
location /ajaxserver {
proxy_pass http://localhost:8081/test/;
}
总结
跨域是由浏览器安全限制造成
解决跨域的办法有三种,一是jsonp绕过浏览器安全检测策略,二是从被调用方配置支持跨域的请求头信息,三是从调用方利用反向代理,在ngnix或apache中配置代理域名隐藏跨域。