感谢慕课网上的晓风轻老师的详细讲解。写这篇文章的目的是为了自己能做一个学习笔记,加深自己的印象以及方便以后能随时翻阅,也希望能给大家的ajax跨域学习做一个参考和浏览。本人也只是一个IT萌新,会有很多知识不足的地方,如果文中有什么不足或者不对的地方,欢迎留言指出,十分感谢。嘻嘻(#.#)
文章会从问题出现、原因、分析以及Demo搭建实战解决入手,可以根据自己的需要浏览对应的模块。
一、原因
为什么会发生Ajax跨域问题?
1 浏览器限制
浏览器对ajax的限制。 浏览器处于安全的考虑,当浏览器发现你的请求时跨域请求时,会做一些校验,如果校验不通过,则发生跨域调用失败的问题。
服务器中不会有限制,现实处理中,服务器中可能已经将请求正确处理了,前台请求也报200,但页面中会报跨域错误问题。
2 跨域
发出的请求不是本域的就会报这个错误。只要协议 / 域名 / 端口中任意一个不同,浏览器便认为是跨域。
3 XHR(XMLHttpRequest)请求
浏览器会对XHR请求进行校验,对非XHR请求并不会进行校验,所以就算非XHR请求跨域了,也并不会引发错误。
非XHR的请求有很多种。其中,最简单的非XHR请求可使用标签调用请求:
以上任何一条都有可能引起跨域问题。
二、环境搭建
搭建两个简易的项目模拟实现调用方与被调用方,实现现实业务场景中的跨域问题。
前端(调用方)与后台(被调用方)分别为两个项目,端口分别为8092(调用方)和8091(被调用方)。
1 前端
前端我使用了jasmine测试框架,简单使用可按照如下编辑
1.官网下载源码:https://github.com/jasmine/jasmine/releases
2.将下载好的源码的lib目录解压到项目中并进行引入:
3.编写测试页面JS代码
<script>
//统一设置超时时间
jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000;
var baseUrl = "http://localhost:8091/cross";
//测试模块
describe("跨域demo", function() {
//测试方法
it("get请求", function(done) {
var result;
$.getJSON(baseUrl + "/get1").then(function(jsonObj) {
result = jsonObj;
}
);
//因为是异步请求,所以需要设置超时时间
setTimeout(function() {
expect(result).toEqual({
"data": "get1 ok"
});
//校验完成,通知jasmine框架
done();
}, 100);
});
it("jsonp请求", function(done) {
var result;
$.ajax({
url: baseUrl + "/get1",
dataType: "jsonp",
success:function(json) {
result = json;
console.log(json);
}
});
setTimeout(function() {
expect(result).toEqual({
"data": "get1 ok"
});
done();
}, 100);
});
});
</script>
2 后台
后台使用了Springboot,实际搭建和配置不再细说,以下贴上后台controller代码:
三、解决思路
1 浏览器
1.1 让浏览器不去做这个限制
浏览器使用命令行参数启动。
找到浏览器执行目录的路径,并在启动时加入如下参数,在该模式下访问即可不进行跨域检查。
但这个改动需要每一个客户端都需要改动,所以并不推荐。
2 发送非XHR类型的请求 -> JSONP
JSONP(JSON For Pending)JSON的补充使用方式,本质上是利用动态生成Script标签请求资源可以跨域解决。ajax请求类型会因此改变成script类型,所以需要后台也要将返回类型改为script类型,可使用@ControllerAdvise进行统一处理。
实战方案
2.1 编写前端代码
2.2 增加后台代码处理结果
该代码会进行处理(下方原理会详解)。
2.3 运行结果
原理
- JSONP发送的请求时script类型而非XHR请求,故浏览器不会对其进行跨域检查
- 普通返回类型为JSON,而JSONP返回类型则为Javascript
- URL请求中的callback参数,在后台Advise切面中约定(JSONP规范的约定)了接收到该参数时会判定该请求为JSONP请求,便会对其进行相应的处理,将结果返回到以该参数值作为函数名,结果为参数的JS函数返回到前端。
- 动态生成script标签,使用完成后会自动销毁(可通过对JQuery打断点查看具体实现)
- callback后的参数【_=***】,是为了防止浏览器缓存,可在ajax请求中加入cache:true允许缓存。
缺点
- 只支持get方法
- 后台也需要进行相应的改动,如果被调用方是第三方代码不可修改,此方法就不可行
- 缺少XHR新特性如异步、事件等
- JSONP方式在spingboot2.0上不推荐使用,在springboot2.0以上该方法是不可使用的
3 跨域问题的解决
最常见的J2EE架构
3.1 修改被调用方代码,让其支持跨域
原理
被调用方解决跨域:在浏览器中直接请求,需要对被调用方进行修改,基于HTTP协议关于跨域请求的规定,在被调用方返回头中加入几个字段,告诉浏览器允许被跨域调用。
①服务器实现。需要了解HTTP协议关于跨域方面的所有请求,针对不同的场景,返回不同的头。只有知道了所有的响应头,才可进行往下的服务器配置
②Ngnix配置
③Apache配置
实战方案
Spring有非常简洁方便的方法,想快速寻求解决方法的朋友可直接看3.1.3的解决方法!
3.1.1 Filter解决方案
修改服务调用端,通过添加返回头完成跨域响应。
简单请求(后有详细介绍)浏览器对请求是先执行,后判断。如果是跨域请求,浏览器会在请求头中新增一个origin的字段,用于保存当前域名信息,当请求返回时,会查看返回头中有没有匹配的跨域信息,如果没有,则报错。通过Filter在【服务器端】返回头中增加参数允许跨域。
请求头说明:
请求头 | 说明 |
---|---|
Access-Control-Allow-Origin | 允许这个域跨域,允许*通配符,但带cookie时必须全匹配,并加上Access-Control-Allow-Credentials=true的请求头 |
Access-Control-Allow-Methods | 制定允许的方法(HEAD / POST / GET / PUT / DELETE / * ) |
Access-Control-Allow-Header | 预检命令中出现这个头,假如值为content-type,就会检查服务器返回头中是否存在相关信息,如果不通过则跨域失败 |
Access-Control-Max-Age | 在指定时间内缓存预检命令结果 |
Access-Control-Allow-Credentials | 默认为空,当代cookie进行跨域时需设置为true |
简单请求和非简单请求
不是所有的请求都是先执行后判断是否跨域。浏览器进行跨域请求的时候,会判断请求是否是简单请求。
简单请求: 先执行后判断。
常见的简单请求:
方法 | 请求Header中 |
---|---|
GET / HEAD / POST | 无自定义头(自定义头在下方有详解) 或 Content-Type = text-plain 、multipart/form-data、application/x-www-from-urlencoded |
非简单请求: 先发送预检命令,检查通过后,才会真正发送请求。
常见的非简单请求:
- PUT / DELETE 方法的AJAX请求
- 发送JSON格式的AJAX请求
- 带自定义头的AJAX请求
以下ajax为非简单请求(发送JSON格式的数据)
以上实例需要加Access-Control-Allow-Header:content-type 才可通过跨域请求。
带cookie跨域访问
xhrFields: {withCreentials:true}—表示发送ajax请求时会带上cookie
Access-Control-Allow-Origin:带cookie时,该请求头需要设置调用方的域
Access-Control-Allow-Credentials:带cookie跨域时必须为true
cookie加在被调用方(服务器端)。即从本域cookie请求到被调用方。(只能调用到本域的cookie)
为了让代码能更加灵活,在filter中获取requestHeader中的origin字段(跨域时新增字段,为当前请求域地址),并设置到Access-Control-Allow-Origin中,即可实现动态设置跨域地址。
带自定义头的跨域
自定义头的两种方式:
后台接收方式:
过滤器处理方式:
通过动态获取request中的header加入response的头中,即可通过浏览器预检。
3.1.2 Nginx解决方案
原理:
服务器端使用Nginx代理,统一处理域名请求,nginx接收到请求时进行转发,即可完成跨域。
Nginx主要功能:
①处理静态文件
②负载均衡
虚拟主机的概念:
多个域名指向同一个服务器,服务器根据不同的域名指向不同的服务器,看似有多台主机,实际上只有一台主机在做这个负载均衡。
实战步骤:
①配置hosts文件,实现本地域名映射
②下载Nginx,并在config文件夹中新建vhost文件夹。并新建一个b.com.conf(hosts配置的映射域名)。
③在b.com.conf中按照nginx语法(详细请看nginx语法介绍)加入如下代码:
server{
listen 80; #监听端口
server_name b.com; #监听地址
location /{
proxy_pass http://localhost:8091/; #请求转向的服务器列表
add_header Access-Control-Allow-Methods *; #添加请求头语法
add_header Access-Control-Max-Age 3600;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Headers
$http_access_control_request_headers;
if ($request_method = OPTIONS){
return 200;
}
}
}
④在nginx/config目录下找到nginx.conf文件,并在最后加上该代码:
⑤在nginx目录下的cmd命令输入nginx.exe -t可测试配置是否正确
⑥在nginx目录下的cmd命令输入start nginx.exe即可启动nginx服务
⑦在被调用方加入cookie,修改前端代码中的调用地址为b.com。测试用例即可全部成功(测试之前记得先把被调用方的过滤器Bean给注释掉后重启)。
3.1.3 Spring框架解决方案
在类或方法中加入 @CrossOrigin注解即可。
3.2 调用方修改代码
隐藏跨域的使用:请求从HTTP服务器中转出,通过服务器进行转发后,浏览器会发现所有的请求都是同一个域,就不会进行跨域检查了。
3.2.1 Nginx解决方案
通过nginx的反向代理机制进行配置,使接口访问的时候让浏览器看起来只是访问本域的接口,从而实现跨域问题。
反向代理:
①配置hosts文件(a.com为请求方域名)
②nginx/vhost配置中添加c.com.conf配置文件,并加上以下配置。
server{
listen 80;
server_name a.com; # 调用方配置域名
location / {
proxy_pass http://localhost:8092/; # 调用方地址
}
location /ajaxserver{ # 代理,把要调用的服务器地址代理为/ajaxserver
proxy_pass http://localhost:8091/cross/; # 被调用方地址
}
}
③请求地址修改为/ajaxserver。
④重新加载nginx配置文件,并启动nginx。
⑤重启服务后在浏览器中进行页面访问http://a.com/page/cross.html,会发现请求除了cookie请求外都能成功。cookie在a.com域名下手动添加数据即可通过。
在network中查看请求可发现,请求地址均为a.com发出,通过相对地址进行请求,nginx通过相对地址进行反向代理,即可解决跨域问题。
END
后记:一看就会,一学就废。不动手之前什么事情都是容易的啊。。。以后还是要多动动手才行,否则实战上遇到的问题只会更多。