ajax跨域解决方案总结

ajax跨域解决方案总结

前端开发中经常会碰到各种跨域问题,在此做一总结。
实验代码: https://github.com/GerryLon/cors

什么是跨域?

先来讲讲什么是跨域?

  • XMLHttpRequest同源策略:禁止使用XHR对象向不同源的服务器地址发起HTTP请求。
  • DOM同源策略:禁止对不同源页面DOM进行操作。这里主要场景是iframe跨域的情况,不同域名的iframe是限制互相访问的。

只要协议主机名端口任意一个不同都认为是不同源的,此时相互调用就会造成跨域。

为什么有跨域限制?

那么为什么设计之初不允许跨域操作呢? 其实也很简单,这是因为安全问题。
暂时不作详解,后续补充.

为什么我们要进行跨域操作?

既然跨域有问题, 我们都在一个域名下调用不就行了吗?
话说我觉得这想法真是一点都没错。 可是啊,可是,美好的东西好像都要有个可是。
实际中我们经常有这样的需要,A域名下要调用B域名下的接口,然而有可能B域名还不是我们自己公司的,不能随便改。
就算自己公司有多个域名,也经常存在着跨域的API调用,可能企业是为了有的服务单独部署, 功能单一强大稳定一些。

总之,我们就是有跨域操作的需求!

下面对于最常见的跨域现象(ajax调用)做实验,解析并给出相应的解决方法.

实验准备

  • 现代浏览器一枚,方便查看错误信息
  • 本地配置一些host(将如下内容添加到hosts文件最后):
    127.0.0.1 a.test.com
    127.0.0.1 b.test.com
  • 安装nodejs,npm,http-server(npm包,全局安装最方便,本地启动一个http服务用)等

  • git clone https://github.com/GerryLon/cors.git
    或者直接下载zip包

  • 进入到cors工程主目录,执行:

    npm install
    node server.js
    http-server
  • 然后在浏览器中打开 http://a.test.com:8080

实验现象

cors仓库下的img目录有一些测试时的截图
自己也可以打开页面和控制台观察

实验分析

接口所在主机为a.test.com,以下简称为A,请求的域名为b.test.com,以下简称为B,且端口不一致.
已经构成跨域.
公用代码:

var baseUrl = 'http://b.test.com:8900/api'; // 接口所在服务器
  • 用例1: A直接调B的接口

    // Client
    $.ajax({
    url: baseUrl + '/get1'
    });
    
    // Server
    response.end('get1 success');

    控制台报错:

    Failed to load http://b.test.com:8900/api/get1: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://a.test.com:8080’ is therefore not allowed access.

    这是最常见的情况,相信做web开发的都遇到过.
    既然说Access-Control-Allow-Origin没有,那我们加上如何?

  • 用例2: 同用例1,不过服务端代码有改:

    // Client
    $.ajax({
    url: baseUrl + '/get1'
    });
    
    // Server
    response.setHeader('Access-Control-Allow-Origin','*');
    response.end('get2 success');

    加上这个头就好了,截图如下:

    不过有时被调用方不是由我们控制的,比如我们调用别人家的接口,这种改服务端的方法就不行了,那么我们就改客户端代码,也就是常说的JSONP,见下面的

  • 用例3: JSONP方式跨域

    JSONP的原理就是利用<script>不受跨域限制的特点,动态加载一个js文件.
    其本质是一个普通的GET请求,所以它只能处理GET的情况,POST不行.

    // Client
    $.ajax({
    url: baseUrl + '/get3',dataType: 'jsonp'
    })
    
    // Server
    response.end(urlObj.query.callback + '(' + '"get3 success"' + ')');

    其中urlObj.query.callback就是获取GET请求的callback参数.
    结果如图:

    返回结果是类似jQuery18207395608856456597_1525337994247("get3 success")这样的字符串.

    有人就说,这种方法其实也要改服务端代码. 是这样的.
    不过现在大多数网站,都提供JSONP格式的选项,JSONP用得还是挺多的.
    豆瓣的Api

  • 用例4: 使用代理接口解决跨域问题

    例子2和3都能解决问题,但是都不太完美.
    例2直接设置Access-Control-Allow-Origin*,这样其实不太好.
    一般我们只想给自己域名下的api调用添加这个头: 比如常见的: a.test.com,b.test.com.
    我们希望的是在test.com下都添加上这个头,其他不添加.
    但事实上Access-Control-Allow-Origin的值要么是*(有限制,下面会说到),要么是具体的协议+域名,
    如: https://a.test.com,参见: Access-Control-Allow-Origin wildcard subdomains,ports and protocols

    HTTP请求头中有个Origin,代表请求来自于哪个站点,可以根据这个判断,以下引用了MDN的解释:

    请求首部字段 Origin 指示了请求来自于哪个站点。该字段仅指示服务器名称,并不包含任何路径信息。该首部用于 CORS 请求或者 POST 请求。除了不包含路径信息,该字段与 Referer 首部字段相似。

    实际代码:

    // Client
    $.ajax({
    url: baseUrl + '/get4'
    });
    
    // Server
    // 获取Origin请求头,这里应该大写的O,
    // 不过node下请求头全是小写的,应该是作了封装
    let origin = req.headers['origin'];
    
    // 需要的才加
    // 这里用正则判断是test.com或者xx.test.com的形式都允许跨域访问
    if (origin && /^https?:\/\/(?:\w+\.)?test\.com.*$/.test(origin)) {
    response.setHeader('Access-Control-Allow-Origin',origin);
    }
    response.end('get4 success');

    这种动态设置Access-Control-Allow-Origin的方法在一个公司还是很实用的.
    不过还是有问题,要是接口在别人家的服务器上呢(如a.test.com请求a.example.com的接口,example.com是别人家的)?
    就是说不能改服务端代码,这又怎么办?
    其实还可以这样,在本域内做一个代理接口,原理如下:
    从a.test.com请求a.example.com的接口,构成跨域.
    这因为是在浏览器中的ajax进行的,所以会有这个问题,如果直接在浏览器地址栏输入或者curl什么的,就不会有这个问题,那么我们就自己在服务端写一个代理的接口,然后请求自己的接口,转发请求,回送响应. 这个方法是万能的,不过就是多了一层,麻烦一些. 而且还要区分GET,POST等处理.

    实际效果:

    // Client
    $.ajax({
    type: 'get',url: baseUrl + '/proxy',dataType: 'json',data: {
      q: '伟大的数学公式',start: 0,count: 1,// https://developers.douban.com/wiki/?title=book_v2#get_book_search
      __target: 'https://api.douban.com/v2/book/search'
    }
    });
    
    // Server
    let query = urlObj.query,method = req.method.toUpperCase(),// 区分出来目标地址
    target = query.__target;
    
    delete query.__target;
    
    // 如果不加这个,也会报跨域错误
    // 这个其实可以作为全局代码,此仅为示例
    if (origin && /^https?:\/\/(?:\w+\.)?test\.com.*$/.test(origin)) {
    res.setHeader('Access-Control-Allow-Origin',origin);
    }
    
    // request库
    request({
    url: target,qs: query
    }).pipe(res);

    其中使用了request库,具体不展开说明,感觉相当强大方便.
    这种解决方案可以说是完美了,原理也很简单明了.
    沿用此思路,利用nginx也是可以了,不过没有代码方便一些.

  • 用例5: 带凭据的请求

    以最常见的携带cookie为例.
    这种首先要添加Access-Control-Allow-Origin响应头,并且值不能为*;
    如果为*,会报错:

    假如在B下存在一条cookie: name=gl,那么在A下ajax请求B时,
    默认是不会带上这条cookie的.
    如果请求时携带凭据,服务端不作设置:

    // Client
    $.ajax({
    url: baseUrl + '/get5',xhrFields: {
      withCredentials: true
    }
    });

    此时就会报错:

    Failed to load http://b.test.com:8900/api/get5: The value of the ‘Access-Control-Allow-Credentials’ header in the response is ” which must be ‘true’ when the request’s credentials mode is ‘include’. Origin ‘http://a.test.com:8080’ is therefore not allowed access. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

    意思是,客户端携带上了凭据,那么服务端要设置Access-Control-Allow-Credentialstrue
    才行.

    在服务端设置相应头:

    res.setHeader('Access-Control-Allow-Credentials','true');

    此时请求成功,发现cookie也带上了:

结论

ajax跨域问题一般通过JSONP或者服务端添加CORS头就能解决绝大多数的情况,第三方接口跨域调用可以采用代理接口的方法.

欢迎补充指正!

相关文章

$.AJAX()方法中的PROCESSDATA参数 在使用jQuery的$.ajax()方...
form表单提交的几种方式 表单提交方式一:直接利用form表单提...
文章浏览阅读1.3k次。AJAX的无刷新机制使得在注册系统中对于...
文章浏览阅读1.2k次。 本文将解释如何使用AJAX和JSON分析器在...
文章浏览阅读2.2k次。/************************** 创建XML...
文章浏览阅读3.7k次。在ajax应用中,通常一个页面要同时发送...