Ajax跨域请求方案详细介绍

前言

目前在开发中前后端分离的模式比较普遍,那么跨域问题也就时常会遇到。网上资料都很片面,不全面,以及都没有说为什么这么解决。
本文会通过前端ajax访问java后端接口的场景,分别从浏览器、后端响应头设置、代理服务器apache和nginx配置、调用端反向代理等方面考虑跨域解决方案。从简单请求、非简单请求和带cookie的请求等多种请求方式逐步分析如何规避跨域限制。

内容详情

什么是跨域?狭义的理解跨域是指受到浏览器同源策略限制的一类请求,通常我们说的跨域就是指的这一类请求。当协议、域名(包含子域名)、端口号中任意一个不相同时,都属于不同域。不同域之间相互请求资源,就会受到浏览器的同源策略限制。

同源策略

同源策略是一种约定,由Netscape公司1995年引入浏览器,是浏览器最核心也最基本的安全功能。保证用户信息的安全,防止恶意的网站窃取数据。比较常见的就是XSSCSFR等攻击。
既然有安全问题,那为什么又要跨域呢? 举个例子,假如公司内部有多个不同的子域,一个是location.company.com ,另一个是app.company.com , 这时想从 app.company.com去访问 location.company.com 的资源就需要跨域。

ajax跨域请求

下面,通过ajax访问不同域的后端java接口的案例来分析,如何规避这种限制。(后面我们把这个案例称为案例一)

  1. 首先新建spring boot项目A,端口号使用默认的8080,快速开发一个java接口如下
    @RestController
    @RequestMapping("/getData ")
    public class GetDataController {
        @GetMapping("/getFirstData")
        private ResultBean getFirstData() {
            System.out.println("getFirstData success");
            return new ResultBean("getFirstData success");
        }
    }
  1. 再次新建一个spring boot项目B,端口号设置为8081,编辑前端页面如下:
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
<script src="jquery-1.9.1.min.js"></script>
<script type="text/javascript">
function getFirstData(){
   $.ajax({
       type : "GET",
       url:"http://localhost:8080/getData/getFirstData",
       success:function(json){
         console.log(json);
      }
   });
}
</script>
</head>
<body>
<a href="#" onclick="getFirstData()">发送getFirstData请求</a>
</body>
</html>
  1. 然后启动两个项目,浏览器中访问 http://localhost:8080/getData/getFirstData,正常返回json数据{"data":"getFirstData success"},此时说明java接口正常。
  2. 接着浏览器先访问项目B的页面http://localhost:8081/index.html,点击页面上的a标签,结果浏览器控制台并没有打印出接口预期返回的json数据。
  3. 查看A项目的控制台,输出了"getFirstData success"。在浏览器开发者模式下查看网络,发现请求的状态为200 ,但是出现了以下错误提示。


结论:跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。

浏览器端解决跨域

根据上面的结论,我们可以让浏览器不做限制,以chorme浏览器为例, 如果设置为支持跨域模式,只需以下几步。

  1. 在电脑上新建一个目录,例如:C:\MyChromeDevUserData。这是保存个人信息的目录,是chrome浏览器防止用户使用跨域模式泄露自己的个人信息采用的措施,这里不详细探讨。
  2. 在属性页面中的目标输入框里加上 --disable-web-security --user-data-dir=C:\MyChromeDevUserData,--user-data-dir的值就是刚才新建的目录。
  3. 点击应用和确定后关闭属性页面,并打开chrome浏览器。发现有“--disable-web-security”相关的提示,说明chrome能正常跨域工作了。

说明:这种解决方式意义不大,因为需要所有的客户端做改动,并且每个用户使用的浏览器也各不相同,处理的方式也不同。在这里介绍这种解决方式,只是为了更进一步说明,产生跨域访问限制的根源就是浏览器。

script标签解决跨域

其实通过script标签可以跨域请求数据的,如下,在B项目的页面中访问百度的一个接口


效果如下:



以上是在案例一中B项目的域里,通过动态创建script标签,跨域访问百度的接口,并成功获取到了资源。这种请求属于script请求,可见浏览器并不会限制这类请求。详细原因可以查看同源策略文档。
但是这种方式需要服务端提供一种约定,约定请求的参数里面如果包含指定的参数(比如上面的cb参数),会把原来的返回对象先转变成js代码然后返回给浏览器解析,而其中Js代码是函数调用的形式,比如上例中百度接口返回的函数的函数名是cb的值,函数的参数就是原来需要返回的结果对象。
假如没有传递cb参数,请求可以正常访问到数据并返回,但是浏览器判断是script请求,所以仍然会以解析脚本的格式解析返回的结果,后果就是无法解析(Uncaught TypeError)。
通过案例一的请求过程,我们发现ajax的请求类型是XHR(XMLHttpRequest)类型,如下。


注:XHR获取数据的目的是为了持续修改一个加载过的页面,是Ajax设计的底层概念,想了解XHR可以点击这里。*

大胆的猜想一下,是否可以让ajax发出的请求封装成script,然后由script向后端发出请求,这样浏览器就不会做限制了。
那么这也正是接下来要介绍的jsonp的解决思路。

jsonp解决跨域

  • 什么是jsonp
  1. 全称是json padding,请求时通过动态创建一个script,在script中发出请求,通过这种变通的方式让请求资源可以跨域。
  2. 它不是一个官方协议,是一个约定,约定请求的参数里面如果包含指定的参数(默认是callback),就说明是一个jsonp请求,服务器发现是jsonp请求,就会把原来的返回对象变成js代码。Js代码是函数调用的形式,它的函数名是callback的值,它的函数的参数就是原来需要返回的结果。
  • jsonp的实现方式
  1. 通过例子来说明Jsonp的请求方式,如下
$.ajax({
  url:"http://localhost:8080/getData/getSecondData",
  dataType:"jsonp",
  jsonp:"callback",
  success:function(json){ 
    console.log(json); 
 } 
});
  1. 查看浏览器网络,发现jsonp发出去的请求是script类型。因为动态创建的script标签在发送请求以后会马上被删除,所以在浏览器中无法查看的到,我们可以采用断点查看。


  2. 当jquery-1.9.1.js中的代码执行完如上的位置时,在页面上动态创建了一个script。如下
<script async="" src="http://localhost:8080/getData/getSecondData? callback=jQuery19102645927304195774_1530774759805&_=1530774759806">
</script>

可以看到jsonp在请求url后面追加了两个参数,callback和一个下划线作为参数名的参数,这个callback就是上面提到的约定参数,而下划线作为参数名的参数值是一个随机数,作用是为了防止请求的结果被缓存了,如果想让结果被缓存可以添加cache:true,如

$.ajax({
     url:"http://localhost:8080/getData/getSecondData",
     dataType:"jsonp",
     jsonp:"callback",
     cache:true,
     success:function(json){
         console.log(json);
     }
 });

此时后台不做改动的话返回的还是json对象,浏览器把对象当做script对象解析,所以会报错。如下可以看到返回的参数类型。



接下来修改后台代码,给提供接口的controller提供一个切面,返回“callback”,编码如下

    @ControllerAdvice
    public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice{
        public JsonpAdvice(){
            super("callback"); //其中参数“callback”可以修改,但是必须与页面请求的回调参数对应
        }
    }

CORS解决跨域方案

  • 引言

回到案例一,来看看跨域所报的错误,大概意思是说请求的资源上没有“Access-Control-Allow-Origin”头信息(此处说的是响应头)。
那么可以从这个地方考虑,在返回资源的时候加上这个头信息。这也就是接下来说的CORS请求的解决思路。
CORS是W3C标准, 全名叫跨域资源共享Cross-origin resource sharing,允许浏览器向跨域服务器发出XMLHttpRequest请求。

  • 简单请求跨域

当我们在做跨域请求资源的时候,会发现多了Origin的字段(此字段指定了当前页的域名和端口号,它的值是由浏览器自动获取的,无法通过手动修改) ,如下:



然后在响应的时候,浏览器会判断响应头里面有没有跨域信息,如果没有就会报错。
那我们尝试修改后台代码,在响应头里添加这个信息。

    @Bean
    public FilterRegistrationBean registerFilter(){
        FilterRegistrationBean frBean = new FilterRegistrationBean();
        frBean.addUrlPatterns("/*");
        frBean.setFilter(new CrosFilter());
        return frBean;
    }

以下是过滤器实现部分。

    public class CrosFilter implements Filter {
        @Override
        public void destroy() {  }

        @Override
        public void doFilter(ServletRequest request, ServletResponse response,  FilterChain filterChain) throws IOException, ServletException {
            HttpServletResponse hsr =(HttpServletResponse)response;
            hsr.addHeader("Access-Control-Allow-Origin","http://localhost:8081");  //添加Origin
            filterChain.doFilter(request,response);
        }

        @Override
        public void init(FilterConfig arg0) throws ServletException { }
    }

此时就可以通过ajax访问到资源了,这种情况只能允许一种域名访问,如果想让多个域名都访问到此接口,可以用*号代替上面设置的参数。如
hsr.addHeader("Access-Control-Allow-Origin", "*");

  • 预检命令

其实浏览器将CORS分为两类, 简单请求和非简单请求,每次请求会先判断是否为简单请求。

  1. 如果是简单请求就先执行后判断资源信息是否允许跨域,这也是案例一中为什么请求的状态是200,但是无法获取数据的原因了。
  2. 如果不是简单请求会先发一个预检命令,检查通过以后才会把跨域请求发送过去。
  3. 像PUT,DELETE方法的ajax请求就属于非简单请求,而像GET、HEAD、POST方法的ajax请求,如果不考虑其他因素都属于简单请求,但是带json参数或者自定义头的ajax请求就属于非简单请求。

如下,实现一种带json参数的ajax请求

var params={username :"user", password:"123"};
function getFirstData(){
$.ajax({
    type : "POST",
    data: JSON.stringify(params),
    url:"http://localhost:8080/getData/postUser",
    contentType:"application/json;charset=UTF-8",
   success:function(json){
               console.log(json);
     }
  });
}

后台代码

    @PostMapping("/postUser")
    private DataSource postUser(@RequestBody User user){
        System.out.println("postUser success");
        return new DataSource("postUser success");
    }

如下,访问了一次,出现了两条请求数据。



第一条OPTIONS方法的请求就是预检请求,通过实例测试会发现,在预检的时候,请求头里面会出现一个头信息
Access-Control-Request-Headers:content-type
意思是说它会询问一下后台服务器是否允许这个头,如果响应头里没有对应的信息就会报错,所以跨域请求就失败了。如下



因此我们需要在过滤器中加上对应的响应头,如下: hsr.addHeader("Access-Control-Allow-Headers", "Content-Type");
到这里有个问题了,如果每次请求都会预检未免多此一举,那么我们可以利用下面这个响应头设置预检结果缓存时间,单位为秒。 hsr.addHeader("Access-Control-Max-Age", "3600"); 这样设置以后,浏览器再次访问此域名时,一个小时内都不用预检。

携带cookie跨域请求

还有一种情况,在请求资源的时候往往需要带上cookie信息,cookie中记录了用户的信息以及session会话的id等。可以用下面这种方式,在ajax请求中携带cookie信息。

$.ajax({                    
    type : "GET",
    url:"http://localhost:8080/getData/getCookie",
  xhrFields{
           withCredentials:true
   },
   success:function(json){
           console.log(json);
     }
});

后台代码

    @GetMapping("/getCookie")
    private DataSource getCookie(@CookieValue(value="name")String cookie){
        System.out.println("getCookie success");
        return new DataSource("getCookie success");
    }

然后在过滤器中还需要设置响应头,如:hsr.addHeader(“Access-Control-Allow-Credentials”,”true”);
接着在浏览器控制台下添加一个cookie信息,如:document.cookie=“name=tj”
测试发现,如果响应头里是设置了hsr.addHeader("Access-Control-Allow-Origin", ""),是不允许通过的,需要设置全匹配,不能用通配符。


如果我们需要支持多个域名可以访问,服务端可以先从request中将origin中的域名信息取出来,然后赋值给响应头的origin就可以了。

HttpServletRequest req =(HttpServletRequest) request;
String origin = req.getHeader("origin");
if(origin!=null){
       hsr.addHeader("Access-Control-Allow-Origin",origin);
}

自定义头的跨域请求

还有一种自定义头的跨域也属于非简单跨域,解决方式和cookie的类似。
添加头的操作

   type:"get",
   url:"http://localhost:8080/getData/getFourthData",                           
   headers:{
         "myheader":"qunar"                          
  },                    
 success:function(json){
         console.log(json);                
    }             
 });

后台先从request中取出头信息,然后判断是否为空,然后赋值给响应头。

String headers =req.getHeader("Access-Control-Request-Headers");
if(headers!=null){
     hsr.addHeader("Access-Control-Allow-Headers",headers); 
 }

如此便成功完成了跨域请求。下面是拦截器中完整的配置。

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain filterChain) throws IOException, ServletException {

        HttpServletResponse hsr = (HttpServletResponse)response;
        HttpServletRequest req = (HttpServletRequest) request;
        String origin = req.getHeader("origin");
        String headers = req.getHeader("Access-Control-Request-Headers");
        if(origin!=null){
            hsr.addHeader("Access-Control-Allow-Origin", origin);
        }
        if(headers!=null){
            hsr.addHeader("Access-Control-Allow-Headers", headers);
        }
        hsr.addHeader("Access-Control-Max-Age", "3600");
        hsr.addHeader("Access-Control-Allow-Credentials","true");
        hsr.addHeader("Acccess-Control-Allow-Methods", "POST, GET, OPTIONS,DELETE,PUT");

        filterChain.doFilter(request, response);
    }

springMVC注解实现跨域请求

其实后端实现跨域请求并不用这么麻烦,springMVC的4.2版本以后提供了注解的方式解决跨域问题,在类上添加CrossOrigin注解,如下

@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/getData")
public class GetDataController {
   ...//此处省略
}

注释掉前面过滤器的配置,上面的所有请求也都能访问了。

小结: 有如此利器,还要介绍前面的解决方案,是为了更好的理解跨域解决思路。在不满足注解的框架中也能很好的实现跨域请求。

代理服务器实现跨域

到这里,基本上已经了解了java后端解决跨域的办法。而实际应用的部署环境往往会添加代理服务器,如下

那么我们可以考虑从代理服务器端解决跨域问题。解决的思路相同,直接说配置方式。

被调用端支持跨域配置

这种场景配置的是被调用端的代理服务器。在浏览器某个域(http://location.company.com)中请求不同域(http://app.company.com)的资源,首先将请求发送给被调用端的代理服务器,由代理服务器将请求路由到相应的资源服务器,而资源服务器并不用管请求方是谁,这种代理方式我们称之为正向代理。

Nginx中配置

在nginx.conf文件中配置如下。

注意:请求头的参数在这里都需要小写,并且“-”需要转成下划线, If后面需要带上空格,否则语法会报错。
Apache中配置

在httpd-vhosts.conf中配置虚拟主机相关配置。



注意:在httpd.conf中将vhost相关配置打开。并且将proxy模块、proxy http模块、Heard模块、rewrite模块打开。

调用端反向代理实现跨域

   以上都是从被调用端来解决的,属于支持跨域。当无法修改被调用方的时候,可以配置调用端代理服务器来实现跨域。浏览器向同一域下的反向代理服务器发出请求,再由反向代理服务器转发,向其他域请求资源并返回给浏览器,浏览器不知道请求的资源在哪个服务器上,这种代理方式我们称之为反向代理。

Nginx中配置



Apache配置


总结

全文主要是通过ajax请求不同域下接口资源的场景,详细的分析了跨域的原理以及如何规避这种跨域。首先介绍了浏览器端解决跨域,script标签解决跨域和jsonp的方式解决跨域,但是这些方式都有明显的缺陷,接着重点介绍了cors如何解决跨域,相信通过本文,可以对跨域有一定的理解和解决思路了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • 【20180601】儿童节 今天又是一年一度的儿童节,度过了昨天的忙碌的一天,现在又会忆起自己的小时候过六一节的快...
    过云雨Milo阅读 150评论 0 0
  • 今早在马桶上蹦出了一个话题。 要给"心"开一个手术。(自剖) 很多时候,说要做真实的自己。 然而究竟真实的自己是什...
    一步微光2017阅读 151评论 0 1
  • druid是什么 Druid首先是一个数据库连接池。Druid是目前最好的数据库连接池,在功能、性能、扩展性方面,...
    carway阅读 4,874评论 0 8