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
- 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!";
- }
- }
添加配置:
[html] view plain copy
- #配置端口号
- server.port=8081
- #热部署生效
- spring.devtools.restart.enabled=true
验证服务器端(浏览器访问http://localhost:8081/ajaxserver/hello):
创建客户端:
[java] view plain copy
- 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
[html] view plain copy
- <!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>
添加配置:
[html] view plain copy
- ############################################################
- #
- # 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/**
引入依赖:
[html] view plain copy
- <!-- 引入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请求,浏览器也不会报跨域问题
[html] view plain copy
- <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,并修改服务器代码,添加如下类:
[java] view plain copy
- 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;当
[java] view plain copy
- 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
[java] view plain copy
- 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!");
- }
- }<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;">
- </span>
然后,在项目的启动程序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(添加如下代码)
[html] view plain copy
- <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层添加如下代码:
[java] view plain copy
- @RequestMapping("getuser")
- public JSONResult getUser(UserVO userVO){
- System.out.println("AjaxServerController-->getUser");
- System.out.println(userVO.getName());
- return JSONResult.ok(userVO);
- }
添加UserVO类:
[java] view plain copy
- 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跨域请求相关问题已全部介绍完毕,欢迎大家指出不足之处!
相关推荐
结束数据方法的参数,该如何定义?-- 集合为自定义实体类中的结合属性,有几个实体类,改变下标就行了。<input id="add" type="button" value="新增visitor&quo