Ajax跨域详解

目录

跨域问题是什么?

测试环境搭建

产生跨域的原因?

浏览器限制

跨域

发送的请求是XMLHttpRequest(XHR)的请求

解决思路

全面解决跨域

浏览器禁止检查

JSONP解决跨域

被调用方解决——支持跨域

通过配置响应头来解决跨域

被调用方解决:Nginx

被调用方解决跨域——Apache解决方案

被调用方解决跨域——Spring框架

调用方解决——隐藏跨域(Nginx解决)

调用方解决——隐藏跨域(Apache解决)

源码


参考视频: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浏览网页采用的是内置浏览器,所以两个地址的端口号不同:

产生跨域的原因?

浏览器的控制台报如下错误,就表示产生了跨域:

产生跨域问题有如下三个原因:

  1. 浏览器限制

  2. 跨域

  3. 发送的请求是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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值