Ajax跨域问题详解

1.什么是Ajax跨域问题

客户端Client通过Ajax方式向服务器Server发送Ajax请求,想要得到响应数据,但是由于客户端和服务器不在同一个域(协议,域名或端口不一致),浏览器出于安全方面的考虑,会在Ajax请求的时候作校验,校验不通过时浏览器会在控制台会抛出一个类似于SEC7120: [CORS] 原点“http://localhost:8080”未在“http://localhost:8081/ajaxserver/hello”的 cross-origin资源的 Access-Control-Allow-Origin response header 中找到“http://localhost:8080”的跨域安全问题

2.为什么会产生Ajax跨域问题

*浏览器限制:通俗一点讲就是浏览器多管闲事,当发现客户端和服务器不在同一个域中时会对Ajax请求做校验,校验不通过就会产生跨域安全问题,并非服务器不允许客户端访问(以下示例可以验证)。

*跨域:当客户端和服务器的协议,域名,端口有一样不一致时,浏览器就会认为是跨域。

*XMLHttpRequest请求(Ajax请求):如果发出的不是XMLHttpRequest请求,浏览器也不会报跨域问题(以下示例可以验证)。

3.Ajax跨域问题示例(基于SpringBoot)

创建服务器端:

[java] view plain copy

  1. import org.springframework.web.bind.annotation.RequestMapping;
  2. import org.springframework.web.bind.annotation.RestController;
  3. @RestController
  4. @RequestMapping("/ajaxserver")
  5. public class AjaxServerController {
  6. @RequestMapping("hello")
  7. public String getString(){
  8. System.out.println("************");
  9. return "Hello world!";
  10. }
  11. }

添加配置:

[html] view plain copy

  1. #配置端口号
  2. server.port=8081
  3. #热部署生效
  4. spring.devtools.restart.enabled=true

验证服务器端(浏览器访问http://localhost:8081/ajaxserver/hello):

Ajax跨域问题详解

创建客户端:

[java] view plain copy

  1. import org.springframework.stereotype.Controller;
  2. import org.springframework.web.bind.annotation.RequestMapping;
  3. @Controller
  4. @RequestMapping("/ajaxclient")
  5. public class AjaxClientController {
  6. @RequestMapping("/index")
  7. public String getIndex(){
  8. return "index";
  9. }
  10. }

创建静态页面index.html

[html] view plain copy

  1. <!DOCTYPE html>
  2. <html xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Ajax跨域请求</title>
  6. <!-- <script type="text/javascript" src="js/jquery-3.2.1.min.js"></script> -->
  7. <!-- 引入静态资源文件-->
  8. <script th:src="@{/static/js/jquery-3.2.1.min.js}" type="text/javascript"></script>
  9. </head>
  10. <body>
  11. <a href="#" onclick="getRequest();">Ajax跨域请求</a>
  12. <script type="text/javascript">
  13. function getRequest(){
  14. console.log("getRequest");
  15. $.ajax({
  16. url:"http://localhost:8081/ajaxserver/hello",
  17. dataType:"JSON",
  18. type:"POST",
  19. async:true,
  20. success:function(data){
  21. console.log(data);
  22. }
  23. });
  24. }
  25. </script>
  26. </body>
  27. </html>

添加配置:

[html] view plain copy

  1. ############################################################
  2. #
  3. # thymeleaf相关配置
  4. #
  5. ############################################################
  6. spring.thymeleaf.prefix=classpath:/templates/
  7. spring.thymeleaf.suffix=.html
  8. spring.thymeleaf.mode=HTML5
  9. spring.thymeleaf.encoding=UTF-8
  10. spring.thymeleaf.content-type=text/html
  11. #加载静态资源文件
  12. spring.mvc.static-path-pattern=/static/**

引入依赖:

[html] view plain copy

  1. <!-- 引入thymeleaf依赖 -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  5. </dependency>

启动客户端,访问http://localhost:8080/ajaxclient/index

Ajax跨域问题详解

点击Ajax跨域问题链接,可以看到如下信息:

Ajax跨域问题详解

浏览器控制台抛出如下错误信息:SEC7120: [CORS] 原点“http://localhost:8080”未在“http://localhost:8081/ajaxserver/hello”的 cross-origin 资源的 Access-Control-Allow-Origin response header 中找到“http://localhost:8080”。

此时,清空编辑器控制台信息,来验证跨域问题并非服务器不允许客户端访问。再一次请求服务器,可以看到,编辑器控制台打印信息如下:

Ajax跨域问题详解

此时,F12进入浏览器调试模式查看网络,服务器没有响应数据

Ajax跨域问题详解

在index.html中添加如下链接来验证如果发出的不是XMLHttpRequest请求,浏览器也不会报跨域问题

[html] view plain copy

  1. <a href="http://localhost:8081/ajaxserver/hello">非Ajax请求</a>

访问http://localhost:8080/ajaxclient/index可以看到如下信息:

Ajax跨域问题详解

点击非Ajax请求链接,可以看到如下信息:

Ajax跨域问题详解

控制台没有抛出Ajax跨域安全问题,并且服务器返回了响应数据

4.Ajax跨域问题解决思路

Ajax跨域问题详解

Ajax跨域问题产生的原因是浏览器限制,Ajax请求以及跨域,当三者同时满足时才会产生Ajax跨域问题。基于这种情况,解决思路如下:

1>不让浏览器做跨域校验:可以通过一些参数设置禁止浏览器做限制,但是这需要客户端都要做改动,因此不推荐。

2>发出不是XMLHttpRequest请求:JSONP可以动态创建一个script来发出跨域请求,但是浏览器不认为这是一个XMLHttpRequest请求。

3>支持跨域/隐藏跨域:当被调用方可以做一些修改时,被调用方可以设置参数来支持跨域(例如A域名调用B域名时,在返回的数据里面加入一些字段允许A域名调用);当被调用方不可以做一些修改时(例如需要请求www.baidu.com响应数据,而百度并非你的合作公司),此时需要调用方来做修改,通过一个代理,从浏览器发出的都是A域名的请求,在代理里面把指定的URL转到B域名里,此时浏览器认为就是同一个域名,就不会产生跨域问题。

5.Ajax跨域问题解决方法

基于Ajax跨域问题解决思路,整理了如下解决办法:

1>不让浏览器做跨域校验

可以通过命令行的方式进行设置,具体操作参考:https://www.cnblogs.com/zhongxia/p/5416024.html

2>JSONP的方式

JSONP(JSON with Padding)是JSON的一种“使用模式”,可用于解决主流浏览器的跨域数据访问的问题。通俗地说,JSONP是非官方协议,是一种约定,它约定了如果请求的参数里包含了指定的参数(默认是callback)就是一个JSONP请求,服务器发现该请求是JSONP请求时就会把响应数据由原来的JSON对象改为JS代码(JS代码是函数调用的形式,函数名是callback参数的值,函数参数是原来要返回的JSON对象)。

将index.html中的Ajax请求的dataType改为JSONP,并修改服务器代码,添加如下类:

[java] view plain copy

  1. import org.springframework.web.bind.annotation.ControllerAdvice;
  2. import org.springframework.web.servlet.mvc.method.annotation.AbstractJsonpResponseBodyAdvice;
  3. @SuppressWarnings("deprecation")
  4. @ControllerAdvice
  5. public class JSONPAdvice extends AbstractJsonpResponseBodyAdvice{
  6. public JSONPAdvice(){
  7. super("callback");
  8. }
  9. }

然后重新访问客户端,可以看到:

Ajax跨域问题详解

错误信息变为SCRIPT1004: SCRIPT1004: Expected ';'查看服务器响应数据如下:

Ajax跨域问题详解

服务器已经正常响应,但是浏览器控制台依然报错,这是为什么呢?于是百度了这个错误,得到如下结果:http://www.codes51.com/itwd/2116389.html

Ajax跨域问题详解

由此得知,接口不支持JSONP。JSONP弊端:(1)服务器需要改动代码支持(2)SJSONP只支持GET请求(可以验证)将Ajax请求添加参数type:"POST",然后重新访问客户端可以看到如下信息(请求的方法依然是GET):

Ajax跨域问题详解

(3)发送的不是XMLHttpRequest请求:这既是JSONP能解决跨域问题的原因,也是其弊端。因为XMLHttpRequest请求有很多新特性(例如异步,各种事件),而在JSONP中无法使用。

3>支持跨域/隐藏跨域

(1)常见的J2EE架构图

Ajax跨域问题详解

客户端Client向服务器发送请求,先经过Apache/Nginx(http服务器),当Apache/Nginx发现请求是静态请求(js文件,css文件,图片等)时,会直接将资源文件返回给客户端Client而不会再转发到Tomcat;当

[java] view plain copy

  1. import java.io.IOException;
  2. import javax.servlet.Filter;
  3. import javax.servlet.FilterChain;
  4. import javax.servlet.FilterConfig;
  5. import javax.servlet.ServletException;
  6. import javax.servlet.ServletRequest;
  7. import javax.servlet.ServletResponse;
  8. import javax.servlet.annotation.WebFilter;
  9. import javax.servlet.http.HttpServletResponse;
  10. @WebFilter(urlPatterns="/*")
  11. public class CrossFilter implements Filter {
  12. @Override
  13. public void init(FilterConfig filterConfig) throws ServletException {
  14. // TODO Auto-generated method stub
  15. }
  16. @Override
  17. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
  18. throws IOException, ServletException {
  19. System.out.println("CrossFilter");
  20. HttpServletResponse res=(HttpServletResponse) response;
  21. //res.addHeader("Access-Control-Allow-Origin", "*");
  22. res.addHeader("Access-Control-Allow-Origin", "http://localhost:8080");
  23. //res.addHeader("Access-Control-Allow-Methods", "*");
  24. res.addHeader("Access-Control-Allow-Methods", "POST");
  25. chain.doFilter(request, response);
  26. }
  27. @Override
  28. public void destroy() {
  29. // TODO Auto-generated method stub
  30. }
  31. }

此过滤器的作用是将响应头添加两个字段Access-Control-Allow-Origin和Access-Control-Allow-Methods,前者表示支持跨域的域,后者表示支持跨域的方法。

修改控制层代码(注意:控制层要返回JSON格式的数据,这是因为在Ajax请求时要求响应数据必须时JSON格式, 所以在返回数据时都需要转为JSON格式)

JSONResult为LZ自己封装的工具类,源码地址https://github.com/Jasper2s/Study_Imooc/tree/master/SpringBoot/src/main/java/com/springboot/until

[java] view plain copy

  1. import org.springframework.web.bind.annotation.RequestMapping;
  2. import org.springframework.web.bind.annotation.RestController;
  3. import com.ajaxserver.utils.JSONResult;
  4. @RestController
  5. @RequestMapping("/ajaxserver")
  6. public class AjaxServerController {
  7. /**
  8. * 注意:由于在Ajax请求时要求响应数据必须时JSON格式
  9. * 所以在返回数据时都需要转为JSON格式
  10. * @return
  11. */
  12. @RequestMapping("hello")
  13. public JSONResult getString(){
  14. System.out.println("AjaxServerController-->getString");
  15. return JSONResult.ok("Hello world!");
  16. }
  17. }<span style="background-color:transparent;color:rgb(79,79,79);float:none;font-size:16px;font-style:normal;font-variant:normal;font-weight:400;letter-spacing:normal;line-height:26px;text-align:justify;text-decoration:none;text-indent:0px;text-transform:none;white-space:normal;word-spacing:0px;">
  18. </span>

然后,在项目的启动程序AjaxServcerApplication上要添加@ServletComponentScan来扫描组件(否则,过滤器不会起作用)

最后,进入浏览器访问http://localhost:8080/ajaxclient/index,查看控制台,可以看到:

Ajax跨域问题详解

控制台的响应表头多了两个字段Access-Control-Allow-Origin和Access-Control-Allow-Methods,此时我们查看数据是否打印出来了:

Ajax跨域问题详解

由此说明,此次Ajax跨域访问成功响应,并将数据成功返回给客户端!

b.Apache配置

可参考https://www.imooc.com/video/16592

c.Nginx配置

可参考https://www.imooc.com/video/16591/0

d.Spring框架解决方案

可参考https://www.imooc.com/video/16593

(3)调用方解决跨域:Apache/Nginx(http服务器)将客户端Client所有的请求转发到服务器,此时浏览器发现所有的请求都是同一个域就不会产生跨域问题。a.Nginx配置可参考https://www.imooc.com/video/16594/0b.Apache配置可参考https://www.imooc.com/video/16595

6.请求分类

Ajax跨域问题详解

(1)简单请求:浏览器对于简单请求往往先执行后判断,例如:浏览器控制台报跨域安全问题时,请求依然有响应数据,这说明浏览器将该请求视为简单请求,先执行然后再判断是否存在跨域安全问题。

(2)非简单请求:浏览器对于非简单请求往往先判断后执行,举例说明:

发送JSON格式Ajax请求示例如下:

修改index.html(添加如下代码)

[html] view plain copy

  1. <a href="#" onclick="getRequest2();">非简单请求</a>
  2. function getRequest2(){
  3. console.log("getRequest2");
  4. $.ajax({
  5. url:"http://localhost:8081/ajaxserver/getuser",
  6. dataType:"JSON",
  7. type:"POST",
  8. data:{"name":"Jasper","age":20,"sex":"man"},//请求的数据为JSON对象
  9. //cache:true,//表示请求可以被缓存
  10. async:true,
  11. success:function(data_response){
  12. console.log(data_response);
  13. },
  14. error:function(){
  15. alert("error");
  16. }
  17. });
  18. }

Controller层添加如下代码:

[java] view plain copy

  1. @RequestMapping("getuser")
  2. public JSONResult getUser(UserVO userVO){
  3. System.out.println("AjaxServerController-->getUser");
  4. System.out.println(userVO.getName());
  5. return JSONResult.ok(userVO);
  6. }

添加UserVO类:

[java] view plain copy

  1. public class UserVO {
  2. private String name;
  3. private Integer age;
  4. private String sex;
  5. public String getName() {
  6. return name;
  7. }
  8. public void setName(String name) {
  9. this.name = name;
  10. }
  11. public Integer getAge() {
  12. return age;
  13. }
  14. public void setAge(Integer age) {
  15. this.age = age;
  16. }
  17. public String getSex() {
  18. return sex;
  19. }
  20. public void setSex(String sex) {
  21. this.sex = sex;
  22. }
  23. }

浏览器访问http://localhost:8080/ajaxclient/index,点击非简单请求链接可以看到如下信息:

Ajax跨域问题详解

可以看到浏览器发起了两次getUser请求,第一次getUser请求是预检命令,当预检命令通过后,再发第二次请求然后服务器进行响应。而简单请求只有一次请求,如下图:

Ajax跨域问题详解

那么问题又出现了,如果每次发送非简单请求,浏览器都要发起两次请求岂不很影响速度?如何只让浏览器在第一次发起的非简单请求中请求两次,之后只请求一次呢(因为第一次预检命令通过之后,就没有必要在之后的每次请求都发送一次预检命令)?可以通过设置缓存!Win10浏览器已经自动将预检命令加入到缓存(从上图可知预检命令执行时间为0秒,且来自缓存)。当然,也可以通过res.addHeader("Access-Control-Max-Age", "3600");告诉浏览器在一个小时内可以缓存设置的头部信息。

至此,Ajax跨域请求相关问题已全部介绍完毕,欢迎大家指出不足之处!

相关推荐