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)


创建服务器端:

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/ajaxserver")
public class AjaxServerController {
	
	@RequestMapping("hello")
	public String getString(){
		System.out.println("************");
		return "Hello world!";
	}

}

添加配置:

#配置端口号
server.port=8081
#热部署生效
spring.devtools.restart.enabled=true
验证服务器端(浏览器访问http://localhost:8081/ajaxserver/hello):

创建客户端:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/ajaxclient")
public class AjaxClientController {
	
	@RequestMapping("/index")
	public String getIndex(){
		return "index";
	}

}

创建静态页面index.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Ajax跨域请求</title>
<!-- <script type="text/javascript" src="js/jquery-3.2.1.min.js"></script> -->
 <!-- 引入静态资源文件-->
 <script th:src="@{/static/js/jquery-3.2.1.min.js}" type="text/javascript"></script>
</head>
<body>
<a href="#" onclick="getRequest();">Ajax跨域请求</a>
<script type="text/javascript">
	function getRequest(){
		console.log("getRequest");
		$.ajax({
			url:"http://localhost:8081/ajaxserver/hello",
			dataType:"JSON",
			type:"POST",
			async:true,
			success:function(data){
				console.log(data);
			}
		});
	}
</script>
</body>
</html>

添加配置:

############################################################
#
# thymeleaf相关配置
#
############################################################
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html

#加载静态资源文件
spring.mvc.static-path-pattern=/static/**

引入依赖:

<!-- 引入thymeleaf依赖 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>

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


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

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

此时,清空编辑器控制台信息,来验证跨域问题并非服务器不允许客户端访问。再一次请求服务器,可以看到,编辑器控制台打印信息如下:此时,F12进入浏览器调试模式查看网络,服务器没有响应数据在index.html中添加如下链接来验证如果发出的不是XMLHttpRequest请求,浏览器也不会报跨域问题

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

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

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

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

4.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,并修改服务器代码,添加如下类:

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.AbstractJsonpResponseBodyAdvice;

@SuppressWarnings("deprecation")
@ControllerAdvice
public class JSONPAdvice extends AbstractJsonpResponseBodyAdvice{

	public JSONPAdvice(){
		super("callback");
	}
}

然后重新访问客户端,可以看到:错误信息变为SCRIPT1004: SCRIPT1004: Expected ';'查看服务器响应数据如下:服务器已经正常响应,但是浏览器控制台依然报错,这是为什么呢?于是百度了这个错误,得到如下结果:http://www.codes51.com/itwd/2116389.html由此得知,接口不支持JSONP。JSONP弊端:(1)服务器需要改动代码支持(2)SJSONP只支持GET请求(可以验证)将Ajax请求添加参数type:"POST",然后重新访问客户端可以看到如下信息(请求的方法依然是GET):(3)发送的不是XMLHttpRequest请求:这既是JSONP能解决跨域问题的原因,也是其弊端。因为XMLHttpRequest请求有很多新特性(例如异步,各种事件),而在JSONP中无法使用。

3>支持跨域/隐藏跨域

(1)常见的J2EE架构图

客户端Client向服务器发送请求,先经过Apache/Nginx(http服务器),当Apache/Nginx发现请求是静态请求(js文件,css文件,图片等)时,会直接将资源文件返回给客户端Client而不会再转发到Tomcat;当
Apache/Nginx发现请求是动态请求(如Ajax请求)时,会将请求转发到Tomcat服务器,Tomcat服务器响应结果返回给Apache/Nginx,Apache/Nginx再将响应结果转发给客户端Client。
(2)被调用方解决跨域:Apache/Nginx(http服务器)在Tomcat(或其它服务器,如Jetty)返回头中添加一些字段来支持跨域。
实现方式:
a.服务器端(Tomcat或Jetty等)实现(推荐)
步骤如下:
首先添加过滤器:

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletResponse;

@WebFilter(urlPatterns="/*")
public class CrossFilter implements Filter {

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		// TODO Auto-generated method stub

	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		System.out.println("CrossFilter");
		HttpServletResponse res=(HttpServletResponse) response;
		//res.addHeader("Access-Control-Allow-Origin", "*");
		res.addHeader("Access-Control-Allow-Origin", "http://localhost:8080");
		//res.addHeader("Access-Control-Allow-Methods", "*");
		res.addHeader("Access-Control-Allow-Methods", "POST");
		chain.doFilter(request, response);
	}

	@Override
	public void destroy() {
		// TODO Auto-generated method stub

	}

}

此过滤器的作用是将响应头添加两个字段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

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.ajaxserver.utils.JSONResult;

@RestController
@RequestMapping("/ajaxserver")
public class AjaxServerController {
	
	/**
	 * 注意:由于在Ajax请求时要求响应数据必须时JSON格式
	 * 所以在返回数据时都需要转为JSON格式
	 * @return
	 */
	@RequestMapping("hello")
	public JSONResult getString(){
		System.out.println("AjaxServerController-->getString");
		return JSONResult.ok("Hello world!");
	}

}

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

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

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

由此说明,此次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.请求分类


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

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

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

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

<a href="#" onclick="getRequest2();">非简单请求</a>

function getRequest2(){
		console.log("getRequest2");
		$.ajax({
			url:"http://localhost:8081/ajaxserver/getuser",
			dataType:"JSON",
			type:"POST",
			data:{"name":"Jasper","age":20,"sex":"man"},//请求的数据为JSON对象
			//cache:true,//表示请求可以被缓存
			async:true,
			success:function(data_response){
				console.log(data_response);
			},
			error:function(){
				alert("error");
			}
		});
	}

Controller层添加如下代码:

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

添加UserVO类:

public class UserVO {
	
	private String name;
	
	private Integer age;
	
	private String sex;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Integer getAge() {
		return age;
	}

	public void setAge(Integer age) {
		this.age = age;
	}

	public String getSex() {
		return sex;
	}

	public void setSex(String sex) {
		this.sex = sex;
	}
	
}

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


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


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

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









猜你喜欢

转载自blog.csdn.net/qq_20788055/article/details/80722377