Ajax的跨域问题

什么是跨域及来源

跨域问题来源于浏览器的同源策略,JavaScript只能访问和操作自己域下的资源,不能访问和操作其他域下的资源。

什么是同源策略

同源策略是浏览器为安全性考虑实施的非常重要的安全策略。只有协议、端口、和域名相同,则允许相互访问。

下面几种情况列举了不同情况下的地址,以及能否成功访问。
  • Ajax跨域代码示例
    在这个代码中,我们创建了一个oBtn按钮对象,当点击它的时候会创建名为xhr的XMLHttpRequest对象,想让它获取api.binstd.com上的天气数据。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>ajax</title>
</head>
<body>
    <div id="mydiv">
        <button id="btn">点击</button>
    </div>
</body>
<script type="text/javascript">
    window.onload = function() {
      var oBtn = document.getElementById('btn');
      oBtn.onclick = function() {
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function() {
            if (xhr.readyState == 4 && xhr.status == 200) {
                    alert( xhr.responseText );
            }
        };
        xhr.open('get', 'https://api.binstd.com/weather2/query?appkey=0baae6a71fc7209b&city=安顺&date=2018-01-01', true);
        xhr.send(); 
    };

点击按钮发现浏览器提示如下,也就是说,我们的访问被拒绝了。这个就是实际的跨域问题。

如何解决跨域问题

1.通过JSONP解决

JSONP 是 JSON with padding(填充式 JSON 或参数式 JSON)的简写。

本质上是利用HTML元素的src属性都可以跨域的思路来解决的。

imgscriptiframe等标记的src属性的值都可以赋成其它域名的合法地址。我们回想一下,在添加img标签的时候,里面有src属性,属性的链接可以是任何图片的有效链接,这时候是没有跨域的问题的。

下图描述了用JSONP解决跨域问题的基本原理。


该过程涉及到客户端服务器端两部分:

  • 在客户端,我们在src里面写上请求地址以及回调函数func(),并把回调函数放在全局,然后向服务器发送请求
  • 服务器在收到我们的请求以后,先准备我们请求的数据,然后返回函数执行字符串'func('+JSON.stringify(data)+')'
  • 客户端执行这条语句(回调函数)。

现在我们用JSONP方案来改进Ajax跨域代码示例,代码如下。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>JSONP实现跨域2</title>
  </head>
  <body>
    <div id="mydiv">
        <button id="btn">点击</button>
    </div>
  </body>
  <script type="text/javascript">
    function handleResponse(response){
            console.log(response);
    }
  </script>
  <script type="text/javascript">
    window.onload = function() {
      var oBtn = document.getElementById('btn');
      oBtn.onclick = function() {     
        var script = document.createElement("script");
        script.src = "https://api.binstd.com/weather2/query?appkey=0baae6a71fc7209b&city=安顺&date=2018-01-01&callback=handleResponse";
        //在body的第一个孩子之前,插入script节点
        document.body.insertBefore(script, document.body.firstChild);   
    };
  };
  </script>
</html>

代码是在oBtn点击事件里面添加了一个script标签,它的src属性里面包含了请求地址和回调函数。
点击按钮,请求数据成功,控制台打印如下。


JSONP之需要注意的几点:
1、jsonp没有使用XMLHttpRequest对象。
2、jsonp只支持Get方式

2.通过CORS标准解决

细心的你可能会发现,示例代码的错误里面有这句话... origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header ...,意思是原要求被CORS政策限制了。

什么是CORS

CORS:全称"跨域资源共享"(Cross-origin resource sharing)。

CORS需要浏览器和服务器同时支持,才可以实现跨域请求,目前几乎所有浏览器都支持CORS,IE则不能低于IE10。CORS的整个过程都由浏览器自动完成,前端无需做任何设置,跟平时发送http请求并无差异。

所以,实现CORS的关键在于服务器,只要服务器实现CORS接口,就可以实现跨域通信。

CORS请求类型

CORS请求类型分为简单请求和非简单请求,后者需要预检请求。

  • 简单请求
    符合以下条件的,为简单请求
使用下列方法之一:
* GET
* HEAD
* POST
Content-Type的值仅限于下列三者之一:
* text/plain
* multipart/form-data
* application/x-www-form-urlencoded

简单请求的流程如下:


客户端:在客户端的header里面添加Origin信息,将域名写在里面,然后正常的发请求到服务器。
服务端:服务端根据Access-Control-Allow-Origin属性来判别这个请求源是否可以请求成功。
Access-Control-Allow-Origin:* 标识任何外域
Access-Control-Allow-Origin: 允许访问源(多个源用,分隔)

如果客户端的origin在上述允许源的范围内,则可以正确返回数据。这里其实相当于服务器给特定的源开一个“会员”通道,只有满足条件的才能进。

  • 非简单请求
    非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)

下图描述了该类型请求的流程:

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

举例来讲,现在发一个put请求,并添加header信息:

var url = 'https://api.binstd.com/weather2';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

浏览器发现这是一个非简单请求以后,就会自动发出“预检请求”,请求的一部分如下:

OPTIONS /cors HTTP/1.1
Origin: 原域名
Access-Control-Request-Method: PUT //请求方式
Access-Control-Request-Headers: X-Custom-Header //请求头

服务器在收到上述的预检请求以后,检查上述字段是否在“会员通道”中,如果在的话,会确认该跨源请求,并作出回应。

HTTP/1.1 200 OK
...
Access-Control-Allow-Origin: 允许的域名 //可以为特定的域名,也可以用*代表允许任何跨源请求
Access-Control-Allow-Methods: GET, POST, PUT //允许的方式
Access-Control-Allow-Headers: X-Custom-Header //允许的头信息
Content-Type: text/html; charset=utf-8
...

一旦通过了预检请求以后,后面的就和简单请求一样了。

3.使用代理服务器

有时候处于安全考虑或者因为在实际开发中多个环境,在部分环境下,我们需要前端自己解决跨域问题。这个时候我们会采用一种名为代理服务器方法。

下图为该方法的原理:



该方法涉及到客户端,代理服务器和服务器三个部分:

  • 客户端:发送请求到代理服务器上面
  • 代理服务器:代理服务器设置CORS(代理服务器可以自己设置),并将浏览器的请求转发到服务器
  • 服务器服务器和代理服务器之间是没有同源的策略的,所以服务器可以处理代理服务器的转发请求

代理服务器不是请求的生产者,只是请求的搬运工。

现在我们用代理服务器方案来进行代码示例:
需求:用代理服务器解决ajax请求天气数据的跨域问题。
TIPS:数据接口API可以上聚合数据或者是进制数据,里面有免费数据API。

这个方法中需要改变的是客户端和代理服务器的设置:

  • 客户端:正常发送ajax请求,将请求url写成代理服务器的地址
ajax({
   type: 'get',
   url: 'http://localhost:7777', //代理服务器的域名
   data: {
        city: city,
        key: ************
   },
   success: resultSuccess,
   error: function (error) {
      console.log('error', error);
    }
});

function ajax (options) {
    options = options || {};   
    options.data = options.data || {};   
    var json = json(options);   
    // ajax请求   
    function json(options) {   
      // 请求方式,默认是GET
      options.type = (options.type || 'GET').toUpperCase();
      var xhr = null;    
      // 实例化XMLHttpRequest对象   
      if(window.XMLHttpRequest) {   
        xhr = new XMLHttpRequest();   
      } else {   
        // IE6及其以下版本   
        xhr = new ActiveXObjcet('Microsoft.XMLHTTP');
      };
      // 监听事件
      xhr.onreadystatechange = function() {
        if(xhr.readyState == 4) {
          var status = xhr.status;
          if(status >= 200 && status < 300) {
            options.success(JSON.parse(xhr.responseText));
          } else {
            //options.error && options.error(status);
            options.error(status);
          }
        }
      };
      // 连接和传输数据
      if(options.type == 'GET') {
        xhr.open(options.type, options.url + '/?city='+ options.data.city + "&key=" + options.data.key, true);
        xhr.send(null);
      } else {
        xhr.open(options.type, options.url, true);
        //设置提交时的内容类型
        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
        xhr.send(options.data);
      }
    }
  }   
  • 代理服务器:设置CORS标准,允许客户端访问,并将请求转发到真正的请求地址上。
//设置CORS标准的两种方式
response.writeHead(200,{"Access-Control-Allow-Origin":"http://127.0.0.1:5500/weather/index.html",
                         "Content-Type":"text/plain;charset=utf-8"});
response.setHeader("Access-Control-Allow-Origin","http://127.0.0.1:5500");
//转发请求到聚合数据API上
http.get('http://apis.juhe.cn/simpleWeather/query?' + parsedUrl.query, res => {
      var body = '';

      res.on('data', data => {
        body += data;
});

请求成功啦,header信息如下:


代理服务器之需要注意的几点
1、这里的CORS标准就是“会员通道”的通行证,我们还可以设置其他条件(详情见第二小节)。
2、当使用 response.setHeader() 设置响应头时,它们将与传给 response.writeHead() 的任何响应头合并,其中 response.writeHead() 的响应头优先。详情请移步链接[9][10]

参考链接:

[1]轻松搞定JSONP跨域请求
[2]珠峰培训官方
[3]原生JS的面试题:jsonp的实现原理
[4]HTTP访问控制(CORS)
[5]cors实现请求跨域
[6]跨域资源共享 CORS 详解
[7]跨域 CORS
[8]前端项目中nginx 本地反向代理配置
[9]response.setHeader(name, value)
[10]response.writeHead(statusCode[, statusMessage][, headers])

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

推荐阅读更多精彩内容