目录
参考视频:https://www.imooc.com/video/16575所完成的笔记。
跨域问题是什么?
由于现在比较流行前后端分离开发,前端浏览资源和后端访问接口资源的请求URL不同,比如说端口号不同,不同访问到后端的资源,就产生了跨域。
测试环境搭建
采用SSM+Ajax,软件工具使用的是IDEA+HBuilderX,测试用的是火狐浏览器。
前端的代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>跨域</title>
<script src="js/jquery.min.js"></script>
</head>
<body>
<button id="btn">发送请求</button>
<p id="msg"></p>
</body>
<script>
$("#btn").click(function(){
$.ajax({
url: 'http://localhost:8585/test1',
type: 'GET',
success: function(res) {
console.log(JSON.parse(res))
$("#msg").text(JSON.parse(res).msg)
}
})
})
</script>
</html>
后端控制器的代码:
@Controller
public class TestController {
@RequestMapping("/test1")
@ResponseBody
public String test1() throws JsonProcessingException {
ObjectMapper om = new ObjectMapper();
String result = om.writeValueAsString(new Result("请求/test1成功!"));
System.out.println("请求/test1成功!");
return result;
}
}
IDEA直接运行后端浏览器地址是:
HBuilderX浏览网页采用的是内置浏览器,所以两个地址的端口号不同:
产生跨域的原因?
浏览器的控制台报如下错误,就表示产生了跨域:
产生跨域问题有如下三个原因:
-
浏览器限制
-
跨域
-
发送的请求是XMLHttprequest的请求
同时满足以上三个条件就可能会产生跨域。
浏览器限制
意思就是说跨域请求成功了,服务器端获取到了前台的请求,而浏览器端也获取到了后台响应回来的数据,但浏览器不予以展示,不把信息展示出来。
进行测试,点击“发送请求”按钮,可以看到:
IDEA控制台
浏览器控制台
可以清楚看到,响应成功,并且得到了响应的数据,但就是浏览器不显示在页面上。
跨域
就是两个请求地址的域名、端口号不同就可能产生跨域。
这里的端口号不同:一个是8848,一个是8585,所以会产生跨域
如果把前端的index.html页面放入到IDEA的web项目中,同源,就不会报跨域错误。
重启IDEA项目后,发送请求,就不会产生跨域问题。
发送的请求是XMLHttpRequest(XHR)的请求
如果发送的不是XMLHttpRequest请求,那么即使地址不同,也不会产生跨域:
在index.html中添加一个超链接
点击超链接,那么也会响应成功,而且不会跨域
解决思路
1. 让浏览器不做限制,指定参数,让浏览器不做校验,但该方法不太合理,它需要每个人都去做改动。
2. 不要发出XHR请求,这样就算是跨域,浏览器也不会报错,解决方案是JSONP,通过动态创建一个script,通过script发出请求
3. 在跨域的角度:一种是被调用方修改代码,加上字段,告诉浏览器,支持跨域,支持调用方调用。第二种是调用方使用代理,在a域名里面的的请求地址使用代理指定到b域名。第一种是支持跨域,第二种是隐藏跨域。
全面解决跨域
浏览器禁止检查
即通过设置浏览器参数来让浏览器不检查跨域,因为跨域检查是前端浏览器实施的,现在就可以通过禁止检查来达到跨域请求的目的。
这个使用关注较少,不过可以参考这篇博客:https://blog.csdn.net/nju_zjy/article/details/108870385
JSONP解决跨域
JSONP是什么?
使用JSONP服务器后台要改动吗?
JSONP原理
jsonp发出的类型是script而不是xhr,返回的是js类型,而不是json,会自动添加callback参数,是一个约定的参数名
先搭建下测试环境,写些测试代码,在前端的index.html页面中添加如下代码:
注意:前端的ajax请求中其实改变的就是添加了一个属性dataType,它的值指定为"jsonp",表示发送JSONP请求。
后端TestController.java中也添加一个测试方法
然后在浏览器中点击”发送JSONP请求“按钮,发现控制台报错了
也可以看到请求是发送成功的,也获得了响应,但同样没有显示在浏览器页面上,同时还报错了,这是为什么呢?
因为返回的是一个JSON格式的字符串,但jsonp是动态创建一个script标签,所以把结果当成了一个javascript来解析,所以报错了。
因此,也明白了,使用JSONP需要修改后台代码。
修改后台代码如下:
其中添加了个参数callback,这其实前端JSONP请求的约定的参数名,其中callback的值就是回调函数的函数名。
不需要关心它名字是什么,只要获取到callback的值就可以了,SSM可以直接在方法参数中获取到,
一般如果使用的是Servlet,那么就可以通过requset.getParamter("callback")获取值。
然后返回的值,是固定的格式,只需要修改result的结果就可以了,result就是一个JSON格式的字符串,不过现在被callback()包裹而已。
return callback + "(" + jsonStr + ")";
// 说明:
// callback是通过request.getParamter("callback")获取到的callback参数的值
// jsonStr就是你要返回给前端页面的数据,是一个JSON格式的字符串
然后如果使用的是Servlet,那么可以通过response.getWriter().print()方法来返回。
前端代码也要小改一下,因为返回的是一个JSON对象,而不是字符串,所以不需要使用JSON.parse()方法了
发送请求,成功响应
注意:可能在相关资料中有提到更高的springmvc版本中支持下面这种语法,我所使用的版本不支持,可以一试
JSONP有什么弊端?
- 服务器端需要改动代码支持(注:但如果调用的不是自己项目的接口,那么将无法改动服务器代码,这也是一个弊端)
- 只支持GET,不支持POST方式
- 发送的不是XHR请求,也就不能使用XHR请求的特有特性。
被调用方解决——支持跨域
- 服务器端实现
- NGINX配置
- apache配置
通过配置响应头来解决跨域
解决跨域可以通过配置响应头来解决跨域问题,下面使用了两种配置响应头的方法来实现。
在视频中是通过使用Filter来配置的响应头,下面说下代码
创建一个自定义类实现Filter接口,实现里面的doFilter()方法
public class MyFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 转换为HttpServletResponse类型
HttpServletResponse response=(HttpServletResponse)servletResponse;
// 设置响应头
response.addHeader("Access-Control-Allow-Origin","*");// "*"表示一个通配符,允许所有域跨域
// response.addHeader("Access-Control-Allow-Origin","http://localhost:8585/");// 允许指定域名进行跨域
response.addHeader("Access-Control-Allow-Methods","*");// "*"表示一个通配符,允许所有方法
// response.addHeader("Access-Control-Allow-Methods","GET");// 允许GET方法跨域
System.out.println("filter....");// 证明执行了过滤器方法
filterChain.doFilter(servletRequest,servletResponse);
}
public void destroy() {
}
}
然后在web.xml中进行配置:
也可以跨域成功。
不过由于使用的是SSM框架,也学了拦截器Interceptor,所以也可以使用拦截器来配置响应头
创建一个自定义类实现HandlerInterceptor接口,实现里面的preHandle方法
public class MyInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.addHeader("Access-Control-Allow-Origin","*");// "*"表示一个通配符,允许所有域跨域
// response.addHeader("Access-Control-Allow-Origin","http://localhost:8585/");// 允许指定域名进行跨域
response.addHeader("Access-Control-Allow-Methods","*");// "*"表示一个通配符,允许所有方法
// response.addHeader("Access-Control-Allow-Methods","GET");// 允许GET方法跨域
return true;
}
}
然后还需要在spring-mvc中注册这个拦截器
重新运行项目,点击“发送请求”按钮,这下也跨域成功了,而且多了两个响应头
那么这种解决方案能够面对所有情况吗?
视频中提出了简单请求与非简单请求的概念:简单请求是先请求,浏览器再判断是否是跨域;而非简单请求要先发送一个预检命令,检查通过之后才会真正的把跨域请求发出去。
非简单请求常见的是发送json格式的请求,下面写些测试代码来测试
后端在TestController.java中添加一个test3()方法,获取前端提交的POST数据:
前端通过ajax使用POST方式提交json格式的请求
点击“发送POST请求”按钮,跨域失败,报错
最后的结果如下,返回了两个请求,一个是OPTIONS,另一个是POST请求,其中的OPTIONS就是一个预检命令,成功了之后才会发送后面的跨域请求(通过谷歌浏览器查看)。
为什么会报这个错呢?原来在请求头中有这么一个,但响应头没有啊。
那么在响应头上添加这个"Access-Control-Allow-Headers“就可以了。
在拦截器或过滤器代码中添加如下代码:
response.addHeader("Access-Control-Allow-Headers","Content-Type");
// 配置后,前端提交json格式的POST请求,也能够跨域成功
重启项目,刷新浏览器,点击“发送POST请求”,跨域成功
而响应头如下,有了添加的三个:
浏览器是先执行请求还是先判断跨域?浏览器请求-->判断响应中是否有允许跨域-->发现不允许跨域,阻止跨域
说明:
- 当执行跨域请求时,浏览器会提示当前接口不被允许,这说明浏览器已发出了当前请求,但是它的的响应内容被拦截;如果在Response header中的Access-Control-Allow-Origin设置的允许访问源不包含当前源,则拒绝数据返回给当前源。
- 当浏览器要发送跨域请求时,如果请求是复杂请求,浏览器会先发送一个options预检命令即一个options请求,当该请求通过时才会再发送真正的请求。
- 该option请求会根据请求的信息去询问服务端支不支持该请求。比如发送的数据是json类型(通过content-type设置)的话,会携带一个请求头Access-Control-Request-Headers: content-type去询问支不支持该数据类型,如果支持,则请求就会通过,并发送真正的请求
非简单请求, 每次会发出两次请求, 这会影响性能. HTTP协议增加了个响应头, 可以让我们在服务端设置`Access-Control-Max-Age`来缓存预检请求, 比如说我们可以设置为3600m, 也就是一小时客户端只会在第一次的时候发送两个请求, 接下来一个小时内`OPTIONS`请求就被缓存起来了。
(这段话来源于:https://www.imooc.com/notepad/23e276)
又有一个问题:那么Access-Control-Allow-Origin:*能够面对所有情况吗?
很显然,不能,在面对带有cookie的情况下,就需要继续解决了,而Java的session是基于cookie的,所以带cookie的跨域也很重要。
下面写一个测试代码:
后端TestController.java中添加一个测试方法:
前端index.html写一个携带cookie的发送请求
点击“发送带Cookie请求”按钮,控制台报错:
意思是说不支持Access-Control-Allow-Origin的值为通配符“*”,必须是指定的地址。
即修改Access-Control-Allow-Origin的值为请求的URL,如下:
可以看到请求的Origin为:
所以将它写到Access-Control-Allow-Origin的值中去:
所以这个值来源于请求的Origin,可以去请求头中寻找。
注意:不要是http://127.0.0.1:8818/,注意看地址,最后多了一个"/",但如果多了这个字符,那么就会报错,所以最好是去请求头复制Origin的值。
现在再次点击“发送带Cookie"请求按钮,发现报另外一个错:
但事实上,我们的响应头没有这个值,所以将Access-Control-Allow-Credentials:true添加到响应头去。
重启项目,刷新浏览器,再次点击发送带cookie的请求
总结:使用带cookie的跨域,必须设置Access-Control-Allow-Origin的值为请求地址,即匹配,而不能使用通配符"*",同时还需要设置Access-Control-Allow-Credentials的值为true。
由于Access-Control-Allow-Origin的值已经设定为某一地址,那么又有一个域名要跨域怎么办?
所以可以动态获取Origin的值,然后设置到响应头,代码如下:
带自定义头的跨域
有时候需要自定义请求头,然后发送出去,所以,那么该如何实现自定义头的跨域呢?
下面先写测试代码:
后端TestController.java添加一个测试方法:
前端index.html添加一个发送请求的代码:
运行项目,刷新浏览器,发现控制台报错:
说不允许使用自定义头,携带不过去。
但请求头里是有自定义头的
那么修改响应头,让它允许即可,按照下图红框配置,添加到Access-Control-Allow-Headers中即可。
再次运行,刷新浏览器,跨域成功
但发现自定义请求头后端是写死的,这不行,要动态根据请求头获取。
被调用方解决:Nginx
要使用Nginx,需要安装Nginx,安装的教程请参考:https://blog.csdn.net/cnds123321/article/details/113308237
已经安装那么请继续往下看
找到HOSTS文件,配置域名,其一般位于C:\Windows\System32\drivers\etc目录下
在文件的最下面添加如下一行:
其中127.0.0.1表示本地的IP地址,而b.com是自定义的域名,可以随便写。
接着在Nginx的安装目录下添加一个vhost文件夹,名字也可以随便取的。
然后在nginx.conf中配置这个文件,目的是引入该vhost目录下的所有conf配置文件,其实也可以配置在nginx.conf中,但单独配置好,方便管理。
在vhost目录下创建一个b.com.conf配置文件,b.com就是在HOSTS中配置的域名,而conf是文件后缀名。
该b.com.conf配置文件的内容如下:
server {
listen 80;
server_name b.com;
location /{
proxy_pass http://localhost:8585/;
add_header Access-Control-Allow-Method *;
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;
}
}
}
这个内容不完全是可以复制的,下面说下
注意proxy_pass的值来自于下图的URL:
注意:需要重启nginx或者使用命令nginx.exe -s reload使配置文件生效
到这里Nginx的配置基本完成了,下面是创建前后端测试代码,来测试是否有效。
在后端的TestController.java中添加一个测试方法:
然后前端写一个请求代码:
注意:前端代码的请求URL变了,从原来的http://localhost:8585/test6变成了http://b.com/test6。而b.com就相当于localhost:8585,就是一个代理的域名,就可以通过该域名来跨域。
而其他请求如果也要正常使用那么也需要将域名改成如此,比如:
注意:由于我们之前使用的是Filter过滤器或者SpringMVC的拦截器,所以要注释掉它们,才能使用正常的Nginx代理。
然后点击“使用Nginx代理请求“按钮,发送成功
修改的POST请求也成功了
到此为止,使用Nginx解决跨域也完成了。
被调用方解决跨域——Apache解决方案
要使用apache解决跨域,首先要先安装有apache软件,可以参考:https://blog.csdn.net/cnds123321/article/details/113346020
有了apache软件后。
关闭之前使用过的Nginx,通过nginx.exe -s stop命令来停止它。
然后来进行apache的配置,先进行虚拟主机的配置,打开conf目录下的httpd.conf文件
打开LoadModule vhost_alias_module modules/mod_vhost_alias.so模块
找到配置文件的位置:Include conf/extra/httpd-vhosts.conf
保存配置文件httpd.conf,然后在extra目录下找到刚才取消注释的配置文件httpd-vhosts.conf
打开它,里面是虚拟主机结点
然后添加如下一个新的结点
<VirtualHost *:80>
ServerName b.com
ErrorLog "logs/b.com-error.log"
CustomLog "logs/b.log" common
ProxyPass / http://localhost:8585/
</VirtualHost>
说明如下:
保存httpd-vhosts.conf文件。
由于使用到了proxy_module,所以在httpd.conf中也要打开该模块,即取消掉LoadModule proxy_module modules/mod_proxy.so的注释
还要把LoadModule proxy_http_module modules/mod_proxy_http.so模块也打开,即取消掉注释
到bin目录下双击httpd.exe或者使用httpd -k start命令来启动,到bin目录下执行命令
如果报下面这种错误,那么是端口被占用了,关闭占用该端口的进程即可
然后配置响应头,需要在httpd-vhosts.conf文件中配置
<VirtualHost *:80>
ServerName b.com
ErrorLog "logs/b.com-error.log"
CustomLog "logs/b.log" common
ProxyPass / http://localhost:8585/
#把请求头的origin值返回到Access-Control-Allow-Origin字段
Header always set Access-Control-Allow-Origin "expr=%{req:origin}"
#把请求头的Access-Control-Request-Headers值返回到Access-Control-Allow-Headers字段
Header always set Access-Control-Allow-Headers "expr=%{req:Access-Control-Request-Headers}"
Header always set Access-Control-Allow-Methods "*"
Header always set Access-Control-Allow-Credentials "true"
Header always set Access-Control-Max-Age "3600"
#处理预检命令OPTIONS,直接返回204
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ "/" [R=204,L]
</VirtualHost>
文件说明:
该配置中用到了LoadModule headers_module modules/mod_headers.so,所以取消注释
同样用到了LoadModule rewrite_module modules/mod_rewrite.so模块,所以取消注释
保存两个配置文件,然后重启apache
接下来就是为前后端的跨域创建测试代码了。
后端的TestController.java添加一个测试方法
前端的index.html添加一个请求
点击“使用Apache代理请求”按钮,发送请求,跨域成功
被调用方解决跨域——Spring框架
如果后端使用了spring框架,那么就不需要这些配置了,只需要在后端添加一个@CrossOrigin注解,就能解决跨域问题。
下面写一个测试代码来测试
后端TestController.java添加一个测试方法
前端发送一个跨域请求
点击按钮,发送请求,跨域成功
使用spring框架解决跨域真的特别简单,只需要使用一个@CrossOrigin注解就可以了。
该注解还可以放在类上。
一些简单配置,更复杂全面的配置也都可以通过@CrossOrigin来完成。
调用方解决——隐藏跨域(Nginx解决)
当你无法修改服务器端代码时,那么可能就需要通过调用方(即前端)来解决跨域问题。
使用反向代理来完成。
使用Nginx来完成反向代理。
现在HOSTS文件中最后一行,添加一个"a.com",前面的代码是在上面的配置中添加的。
然后在vhost目录下创建一个a.com.conf配置文件
该配置文件的内容如下:
server{
listen 80;
server_name a.com;
location /{
# 将所有的请求转发到该地址
proxy_pass http://127.0.0.1:8848/;
}
location /ajaxserver{
proxy_pass http://localhost:8585/;
}
}
该文件的简要说明:
这就是前端地址:
这是后端的地址:
配置后,需要使nginx的配置文件重新生效,可以重启nginx或者执行下面的命令
配置完成后,接着是添加测试代码:
后端的TestController.java继续添加一个测试方法:
前端写一个测试请求,注意:这个测试请求并不会生效的
运行项目,刷新浏览器,点击“调用方解决跨域之使用Nginx隐藏跨域“按钮,发现跨域失败,报错
因为不是这样来的,要按照下面的方式来才可以:新打开一个浏览器页面,输入"a.com"
接着输入"ajaxserver",现在地址是:http://a.com/ajaxserver/,然后查看:
因此http://a.com/ajaxserver/就相当于后台的http://localhost:8585/
可以查看后台的index.jsp页面,其他页面,也可以按照这个路径来:
是一样的,所以接下来可以输入"/test9",请求成功
就没有跨域问题,就是隐藏了跨域,查看响应头也没有添加什么
到此,使用Nginx反向代理解决跨域成功。
调用方解决——隐藏跨域(Apache解决)
使用apache来实现反向代理,在httpd-vhosts.conf文件中新增加一个虚拟主机结点。
<VirtualHost *:80>
ServerName a.com
ErrorLog "logs/a.com-error.log"
CustomLog "logs/a.log" common
ProxyPass /ajaxserverapache http://localhost:8585/
ProxyPass / http://127.0.0.1:8848/
</VirtualHost>
前端地址就是所使用的浏览页面的URL,这里使用的是HBuilderX的内置浏览器,所以请求URL是这个
后端地址如果是使用的IDEA+tomcat配置的话,就是如下:
配置完成后,就是写前后端的测试代码。
后端TestController.java添加一个测试方法
前端写代码发送跨域请求
点击“调用方解决跨域之使用Apache隐藏跨域”按钮,发现控制台报错
同上面的Nginx反向代理一样,都不是这样操作的。
打开刚才红框中圈出的地址:http://a.com/ajaxserverapache/test10,然后在浏览器地址栏打开
跨域成功
这里a.com/ajaxserverapache同localhost:8585是等同的,同样可以通过http://a.com/ajaxserverapache/访问后端的其他资源。
例如:访问index.jsp页面
好了,使用apache反向代理解决跨域也完成了。
所有关于跨域的知识学习完成,由衷感谢该老师让我学习了跨域的知识。
源码
本项目所使用的源码地址:GitHub的Demo