视频参考:ajax跨域完全讲解
本文精华版:【综合】ajax跨域问题
什么是跨域问题
简单来讲,当前台调用后台,如果接口不是一个域时,就会产生跨域问题。
由于目前前后端分离的技术架构,前台项目和后台项目都是独立开发进行的。当联调测试时,前台页面必然会大量调用后台的接口,此时如果接口不是同一个域,就会发生跨域问题。
测试环境搭建
【后台环境搭建】
使用node+express搭建一个后台系统
(1)新建一个文件夹NodeBack,输入 npm init 初始化项目,创建index.js作为整个后台的入口文件
(2)安装express:npm install express –save
(3)安装body-parse:npm install body-parse–save,用于解析post请求的参数
(4)编写入口文件 index.js
var express = require('express');
/**
* 解析post来的参数
app.use(bodyParser.json()); // for parsing application/json
app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
* */
var bodyParser = require('body-parser');
var app = express();
app.use(bodyParser.urlencoded({extended: true})); // for parsing application/x-www-form-urlencoded
app.get('/test', function (req, res) {
var name = req.query.name
var age = req.query.age
res.jsonp({
data:'name is ' + name + "| age is " + age
});
});
app.post('/login', function (req, res) {
var name = req.body.name
var password = req.body.password
if (password === "123") {
res.jsonp({
"name": name,
"password": password
});
} else {
res.send("error!");
}
});
var server = app.listen(3005, function () {
var host = server.address().address;
var port = server.address().port;
console.log('Example app listening at http://%s:%s', host, port);
});
【前台请求页面】
简单html版
简单起见,就使用Jquery来发送前台请求
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!--导入jquery-->
<script src="http://code.jquery.com/jquery-latest.js"></script>
</head>
<body>
<a href="#" onclick="getReq()">发送get请求</a>
<script>
function getReq(){
$.getJSON("http://localhost:3005/test?name=hugo&&age=18")
.then((result)=>{
console.log(result)
})
}
</script>
</body>
</html>
使用firefox,打开此网址,按F12,调出控制台,点击发出get请求
但是在控制台发现报错提示,这就是跨域问题
ajax跨域问题分析
发生跨域问题有三个原因:
【1】浏览器限制:很多同学认为跨域问题的是服务器后台不允许前台跨域访问,但一些情况下并不是这样,还有可能是浏览器多管闲事,处于安全考虑限制了跨域访问。
如何验证这种想法呢,我们可以在get请求中增加一句log
app.get('/test', function (req, res) {
var name = req.query.name
var age = req.query.age
res.jsonp({
data:'name is ' + name + "| age is " + age
});
console.log("receive get request")
});
再次发送请求:
发现响应都是没有任何问题的
【2】跨域,地址中,协议、域名、端口任意一个不一样,浏览器就认为是跨域
【3】XHR(XMLHttpRequest)请求。请求的type为XHR,非json、img等其他类型。
当上述三种情况同时满足时,跨域问题就产生了,
解决思路
【1】解决浏览器限制:通过指定浏览器的启动参数,解除限制,但是这种情况需要用户手动操作,所以交互不友好,所以不推荐使用
【2】转变请求的类型。上面说到,只有请求类型是xhr时,将xhr的类型转化为jsonp的格式
【3】跨域。
被调用(服务器)方支持跨域;
调用方做跨域支持(隐藏跨域):通过代理,从浏览器发送出的请求都是同源请求
全面解决跨域问题
【禁止浏览器检查跨域】
不做介绍,因为实用价值不大。
【jsonp解决跨域】
json for pending :利用js请求资源标签时,请求可以跨域,来解决跨域请求,它是一种变通的解决方案。
function jsonpReq(){
var result
$.ajax({
url:"http://localhost:3005/test?name=hugo&&age=18",
dataType:'jsonp',
success:function(json){
result=json
console.log(result)
}
})
}
在使用jsonp的时候,后台需要做改动(java后台),不然服务器返回的内容,浏览器会当做js代码来解析,会报语法错误。经测试,node编写的后台无需添加额外的内容。
我们对jsonp的原理进行分析,解释为什么java后台需要进行额外设置:
![]()
首先,看一下jsonp发送的报文类型是script,在之前分析跨域问题产生的条件之一就是请求报文的类型是xhr,那么jsonp的思路就是改变报文的请求类型,变为script。第二,普通的ajax请求返回的json对象,而jsonp返回的是js脚本
第三,jsonp发送的url后面接了一串额外的callback字符
![]()
那么从这里我们就能简单分析jsonp的原理是:jsonp发送的url请求自动添加了一个callback参数,当后台发现callback参数时,就认为是一个jsonp请求,响应头在返回数据时,数据格式就由json变成script,而script的内容就是一个函数调用。那么java后台的改动就能分析得出如下:
![]()
所以,如果我们把url请求中的callback改成callback2,那么后台就不会认为前台请求是一个jsonp请求。即:前台调用的“callback”和后台的“callback”实际上是一个接口上的协议,需要保持一致。
很明显,Jsonp是基于Jquery的一种解决方案,但是在react、vue这类本身的是要替代Jquery操作dom结构的框架,效果可想而知。
接下来,再来谈谈jsonp的利弊:
1、服务器端需要改动。当服务器是第三方提供时,这种做法就会出现局限性。
2、只支持GET请求,通过分析原理,我们知道jsonp是通过Jquery动态创建一个script来创建一个请求的,所以请求类型就被局限在了GET。
3、 发送的不是XHR请求,即XHR的诸多特性都无法使用,例如异步、事件机制都无法使用。
综上可见,我们在解决跨域问题时,最好方法还是解决请求是跨域的,而不是改变请求是XHR还是JSONP。
下面就继续介绍本文的重点:【让服务器支持跨域】、【前台请求隐藏跨域】
【从系统架构分析跨域】
最常见的javaee 架构
请求流程:
请求从浏览器发出,首先到apache或nginx(静态服务器) ,然后判断客户端请求是静态请求还是动态请求。简单讲,跟客户端数据有关系的就是动态数据(用户信息,消息列表等),而图片、js、css、html等就是静态请求。
如果是静态请求,静态服务器直接处理,然后把数据返回给客户端。如果是动态请求,则转发给后台的应用>服务器,处理完毕后,应用服务器把数据返回给静态服务,随后静态服务器发送给客户端。
中间的http服务器,一般有两个作用:
(1)处理静态请求
(2)转发动态请求和负载均衡。
那么在看跨域请求时的两种思路:
第一:直接从浏览器里发出请求到被调用方的http服务器。 这时需要在http服务器上做响应的修改,这些修改都是基于http请求关于跨域部分的协议。就是在返回头里增加一些字段,告诉浏览器被调用方允许客户端跨域访问。这样浏览器就不会报跨域的问题。
第二:隐藏跨域。请求不直接从浏览器发出,而是从中间http服务器转出。通过服务器的转发,浏览器不会发现所有的请求都是同一个域。即调用方会发现所有的请求都是从本地的http服务器发出(其实并不是,请求都是经过客户端http服务器的转发)
跨域解决方向-被调用方解决-Filter及spring解决方案
这是基于支持跨域的解决思路,是基于http协议关于跨域方面的一些规定,在响应头里增加指定的字段,告诉浏览器服务端支持跨域调用。 此种情况下,浏览器直接发送请求到服务方。
在据图解释此方案前,我们理清几个问题:
浏览器如何判断是否跨域?
答:通过对比普通请求和跨域请求浏览器里的请求头里的内容,可以发现跨域请求中增加了一个origin字段,字段值为当前域名的信息。也就是说,当浏览器发现请求是跨域时,就会在请求头添加origin的字段。当浏览器接收到响应头后,就会检查里面是否有允许跨域的字段,如果没有,就会报错。
浏览器是先执行?还是先判断?
答:浏览器发送跨域请求给后台,后台日志打印正常,浏览器响应状态码为200,但却报跨域问题,说明浏览器是先执行请求操作,然后在判断是否跨域。那么,是不是所有的请求都是先执行后判断呢?
并不是的,这就引出了简单请求和非简单请求的概念。
简单请求和非简单请求
每次浏览器再发送请求时,会做一次判断,如果是简单请求,那么就是先执行再判断。如果是非简单请求,那么浏览器会先发送一个预检头,检查通过后,浏览器才会发送真正的请求
跨域解决——服务器端实现【Spring 编写的后台】
基于注解,可以使用
@CrossOrigin
那么被注解类下的所有请求方法都是可以支持跨域的
跨域解决——服务器端实现【Spring boot 编写的后台】
这里我们需要了解http协议关于跨域方面所有的要求,针对不同的场景返回不同的头。只有知道了所有的响应头,我们才能在后面的http服务器上配置响应头。
首先需要注册一个filterRegistrationBean ,然后让所有的请求都经过这filter
接下来我们在CrosFilter里的doFilter方法中添加服务器响应头
对于非简单请求,我们在按照上述方法添加响应头后,可能会出现如下的错误:
意思是我们的返回头缺少“access-control-allow-headers”是缺少的。
所以我们需要在后台服务器中再增加一行代码:
res.addHeader("Access-Control-Allow-Headers","Content-type");
在上面我们分析了什么是简单请求和非简单请求,那么在非简单请求的情况下,浏览器会发送两次请求:
这样会影响请求的效率。http协议中增添了一个请求头,对预检头缓存。
res.addHeader("Access-Control-Max-Age","3600");
告诉浏览器缓存预检头,时间为3600s。那么当再次发送跨域请求时,就只会请求一次。
问题:*代表所有的Url请求,那么是不是在任何情况下都没有问题呢?
带cookie的跨域
随着项目模块越来越多,很多模块现在都是独立部署。模块之间的交流有时可能会通过cookie来完成。比如说门户和应用,分别部署在不同的机器或者web容器中,假如用户登陆之后会在浏览器客户端写入cookie(记录着用户上下文信息),应用想要获取门户下的cookie,这就产生了cookie跨域的问题。
当出现带cookie的跨域问题时,浏览器会报如下错误:
即*无法满足带cookie的情景,如果把header改为特定的地址
res.addHeader("Access-Control-Allow-origin","http://localhost:8081");
则可能会出现如下错误:
接下来我们继续设置
//enable cookie
res.addHeader("Access-Control-Allow-Credentials","true");
但随之而来的问题也比较明显,那就是我们显然不能把origin里的地址写死,所以我们需要在http请求中取出orign字段,然后设置在响应头当中。
这样就完成了带cookie的跨域调用。这里再总结一下:
【1】带cookie的跨域时,Access-Control-Allow-Origin 不能设置为*
【2】发送的cookie的被调用发(服务器)的cookie,而不是调用方(浏览器)的cookie
带自定义头的跨域
当我们直接在跨域请求中添加自定义请求头时(假设自定义请求头为x-header1和x-header2),可能会报如下错误:
这里的解决办法是将自定义的请求头添加到Access-Control_Allow-Headers:
res.addHeader("Access-Control_Allow-Headers","Content-type,x-header1,x-header2");
当然我们也可以动态地设置headers
跨域解决——服务器端实现【Node.js 编写的后台】
跨域解决方向-被调用方解决-Http服务器解决方案
【Nginx配置】
Nginx配置文件:
【apache配置】
相当于把之前在nginx上的配置,再在apache上配置一下,思路也是和nginx一样的。由于apache配置比较复杂,详细配置请参考其他文章。
跨域解决方向-调用方解决
这是基于隐藏跨域的解决思路。此种情况下,请求不会直接从浏览器发送到被调用方,而是从调用方的http服务器转发过去的(被调用方http服务器或者应用服务器接收)。使用了反向代理技术,在浏览器上是看不到任何跨域请求。
这里就和上述apache和nginx操作类型,需要修改他们的配置文件(反向代理的配置)。本文只做简单的配置解释,详细内容请小伙伴们自行百度。
Nginx反向代理
首先我们修改一下host,其效果是当你在浏览器访问 www.a.com和www.b.com时,url地址会映射到127.0.0.1上
接下来配置Nginx配置
然后客户端在进行网络请求时,需要变成nginx代理之后的地址,也就是
/ajaxserver
可以发现,此时调用的url就是本域的地址(浏览器看到的是相对地址),自然就不存在了跨域问题。
apache反向代理配置
思路:增加一个虚拟主机,然后让此虚拟主机,把我们的跨域请求作反向代理
总结
至此,本文简单分析了一下跨域问题产生的原因和常见的解决方法,如果有什么问题欢迎留言交流讨论~
笔者个人订阅号~欢迎小伙伴们关注