[TOC]

跨域

1. 浏览器阻止跨域的目的

  • 安全问题

    如CSRF就是利用跨域攻击的。

    CSRF(Cross-site request forgery),中文名称:跨站请求伪造

    CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账......造成的问题包括:个人隐私泄露以及财产安全。

    原理: 浏览器在发送请求的时候,会在请求中附加cookie信息,那么根据cookie信息,服务器就会认为是本人亲自操作,前提是用户登陆过这个网站。

  • 禁止那些不允许跨域调用其他页面的对象。

2. 跨域介绍

当一个资源从与该资源本身所在的服务器不同的域或端口请求一个资源时,资源会发起一个跨域 HTTP 请求

出于安全原因,浏览器限制从脚本内发起的跨源HTTP请求。 例如,XMLHttpRequest和Fetch API遵循同源策略。 这意味着使用这些API的Web应用程序只能从加载应用程序的同一个域请求HTTP资源,除非使用CORS头文件。

HTTP访问控制 MDN

当访问API发生跨域的时候,浏览器会报如下错误:

Failed to load http://api.douban.com/v2/movie/top250: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://127.0.0.1:8080' is therefore not allowed access.

跨域发生时: 数据已经返回了(API正常返回了结果),但是浏览器检查发现不是同源的,那么阻止了。

3. 同源策略(Same origin Policy)

同源策略是浏览器自己的行为,如果访问接口不是在浏览器上发生则不会有这个问题产生。

浏览器出于安全方面的考虑,只允许与本域下的接口交互。不同源的客户端脚本在没有明确授权的情况下,不能读写对方的资源。

如果两个页面的协议,端口(如果有指定)和域名都相同,则两个页面具有相同的

  • 同协议:如都是http或者https
  • 同域名:如都是http://www.abc.com/a.htmlhttp://www.abc.com/b.html
  • 同端口:如都是80端口

相对http://store.company.com/dir/page.html同源检测的示例:

URL 结果 原因
http://store.company.com/dir2/other.html 成功
http://store.company.com/dir/inner/another.html 成功
https://store.company.com/secure.html 失败 不同协议 ( https和http )
http://store.company.com:81/dir/etc.html 失败 不同端口 ( 81和80)
http://news.company.com/dir/other.html 失败 不同域名 ( news和store )

浏览器的同源策略 MDN

4. 一个跨域的例子

服务器端

  1. 安装node.js

    如果没有没有安装nodejs,那么去官网下载:https://nodejs.org/en/download/

  2. 创建一个目录,在这个目录中创建一个server.js文件,内容如下:

    var http = require('http')
    var fs = require('fs')
    var path = require('path')
    var url = require('url')
    
    http.createServer(function(req, res){
    
      var pathObj = url.parse(req.url, true)
    
      switch (pathObj.pathname) {
        case '/getWeather':
          res.end(JSON.stringify({beijing: 'sunny'}))
          break;
    
        default:
          fs.readFile(path.join(__dirname, pathObj.pathname), function(e, data){
            if(e){
              res.writeHead(404, 'not found')
              res.end('<h1>404 Not Found</h1>')
            }else{
              res.end(data)
            }
          }) 
      }
    }).listen(8080)
    
  3. 终端打开这个项目目录,运行node server.js启动服务,服务地址:http://localhost:8080/

页面端服务

  1. 安装http-server服务

    如果没有安装,终端运行:npm install http-server -g, npm命令是安装nodeJS带的。

    http-server介绍:https://github.com/indexzero/http-server

  2. 在上面的目录中新建一个index.html文件,代码如下:

    <h1>饥人谷</h1>
    <script>
      var xhr = new XMLHttpRequest()
      xhr.open('GET','http://localhost:8080/getWeather', true)
      xhr.send()
      xhr.onload = function(){
        console.log(xhr.responseText)
      }
    </script>
    
  3. 使用http-server命令启动web服务

    http-server
    Starting up http-server, serving ./
    Available on:
      http://127.0.0.1:8081
      http://192.168.43.183:8081
    Hit CTRL-C to stop the server
    

访问页面则提示产生跨域问题

当在浏览器中打开:http://127.0.0.1:8081页面是,浏览器就是提示如下错误:

Failed to load http://localhost:8080/getWeather: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://127.0.0.1:8081' is therefore not allowed access.

5. 使用JSONP来实现跨域访问

5.1 JSONP的原理

JSONPJSON with Padding的简称,一般用来解决Ajax跨域的问题。它是这样产生的:

  1. 页面上调用js文件时不受跨域的影响,而且,凡是拥有src属性的标签都拥有跨域的能力,比如<script><img><iframe>
  2. 可以在远程服务器上把数据装进js格式的文件里,供客户端调用处理,实现跨域。
  3. 目前最常用的数据交换方式是JSON,客户端通过调用远程服务器上动态生成的js格式文件(一般以JSON后缀)。
  4. 客户端成功调用JSON文件后,对其进行处理。
  5. 为了便于客户端使用数据,逐渐形成了一种非正式传输协议,人们把它称作JSONP,该协议的一个要点就是允许用户传递一个callback参数给服务端,然后服务端返回数据时会将这个callback参数作为函数名来包裹住JSON数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。

浅谈 JSONP 的原理与实现

5.2 JSONP的实现

  1. 在页面中声明一个函数,然后写一个script 标签,标签里面的src链接为API接口,通过接口传参的方式把函数名写上。
  2. 后台API在解析到函数名后,会把返回的数据拼接成函数参数function(data),然后返回给页面。
  3. 页面在加载完脚本后,会当JS文件去执行,那么就相当于调用函数,此时页面声明的函数就可以处理API返回的数据了。
  4. 需要前端、后端共同配合。

一个例子:

server.js


var http = require('http')
var fs = require('fs')
var path = require('path')
var url = require('url')

http.createServer(function(req, res){
  var pathObj = url.parse(req.url, true)

  switch (pathObj.pathname) {
    case '/getNews':
      var news = [
        "第11日前瞻:中国冲击4金 博尔特再战200米羽球",
        "正直播柴飚/洪炜出战 男双力争会师决赛",
        "女排将死磕巴西!郎平安排男陪练模仿对方核心"
        ]
      res.setHeader('Content-Type','text/json; charset=utf-8')
      if(pathObj.query.callback){
        res.end(pathObj.query.callback + '(' + JSON.stringify(news) + ')')
      }else{
        res.end(JSON.stringify(news))
      }

      break;

    default:
      fs.readFile(path.join(__dirname, pathObj.pathname), function(e, data){
        if(e){
          res.writeHead(404, 'not found')
          res.end('<h1>404 Not Found</h1>')
        }else{
          res.end(data)
        }
      }) 
  }
}).listen(8080)

index.html

<!DOCTYPE html>
<html>
<body>
  <div class="container">
    <ul class="news">
    </ul>
    <button class="show">show news</button>
  </div>
<script>
  function $(id){
    return document.querySelector(id);
  }
  $('.show').addEventListener('click', function(){
    var script = document.createElement('script');
    script.src = 'http://127.0.0.1:8080/getNews?callback=appendHtml';
    document.head.appendChild(script);
    document.head.removeChild(script);
  })
  function appendHtml(news){
    var html = '';
    for( var i=0; i<news.length; i++){
      html += '<li>' + news[i] + '</li>';
    }
    console.log(html);
    $('.news').innerHTML = html;
  }
</script>
</html>

6. CORS 跨域资源共享(Cross-Origin Resource Sharing)

CORS是一个W3C标准,它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

6.1 CORS步骤

  1. 浏览器发现跨域,在请求中添加Origin字段

    当使用XMLHttpRequest发送请求时,浏览器发现有跨域,会自动在请求头中添加一个Origin字段:

    // Request Headers
    Host: localhost:8081
    Origin: http://127.0.0.1:8081
    Pragma: no-cache
    

    Origin用来说明请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

  2. 服务器设置允许跨域的地址:Access-Control-Allow-Origin

    如果请求中Origin指定的地址 禁止跨域,服务器会返回一个正常的HTTP相应。

    如果请求中Origin指定的地址 允许跨域,服务器会在响应头中添加几点字段:

    Access-Control-Allow-Origin: http://localhost:8081
    Access-Control-Allow-Credentials: true
    Access-Control-Expose-Headers: FooBar
    

    Access-Control-Allow-Origin: 必须字段,它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

    Access-Control-Allow-Credentials:可选字段,它的值是一个布尔值,表示是否允许发送Cookie。

    Access-Control-Expose-Headers:可选字段,设置XMLHttpRequest对象的getResponseHeader()方法只能拿到其他字段。

6.2 一个CORS例子

server.js

var http = require('http')
var fs = require('fs')
var path = require('path')
var url = require('url')

http.createServer(function(req, res){
  var pathObj = url.parse(req.url, true)

  switch (pathObj.pathname) {
    case '/getNews':
      var news = [1,2,3]
      res.setHeader('Access-Control-Allow-Origin','http://localhost:8080')
      //res.setHeader('Access-Control-Allow-Origin','*')
      res.end(JSON.stringify(news))
      break;
    default:
      fs.readFile(path.join(__dirname, pathObj.pathname), function(e, data){
        if(e){
          res.writeHead(404, 'not found')
          res.end('<h1>404 Not Found</h1>')
        }else{
          res.end(data)
        }
      }) 
  }
}).listen(8080)

index.html

<!DOCTYPE html>
<html>
<body>
  <div class="container">
    <ul class="news"></ul>
    <button class="show">show news</button>
  </div>
<script>
  $('.show').addEventListener('click', function(){
    var xhr = new XMLHttpRequest()
    xhr.open('GET', 'http://127.0.0.1:8080/getNews', true)
    xhr.send()
    xhr.onload = function(){
      appendHtml(JSON.parse(xhr.responseText))
    }
  })
  function appendHtml(news){
    var html = ''
    for( var i=0; i<news.length; i++){
      html += '<li>' + news[i] + '</li>'
    }
    $('.news').innerHTML = html
  }
  function $(selector){
    return document.querySelector(selector)
  }
</script>
</html>

注:http://localhost:8080http://127.0.0.1:8080不同源。

跨域资源共享 CORS 详解 阮一峰

7. nginx服务中使用API代理

1、所有接口地址有一个统一的前缀,比如/API

2、在nginx代理里面配置:

http {
    keepalive_timeout  65;
    server {
        listen       8080;                             #服务的端口号
        server_name  localhost;
        location / {
            root   /www/cloudlink;                     #访问的项目目录
            index  index.html index.htm;               #访问的文件名
        }
        location ^~ /cloudlink/v1/ {
            proxy_pass http://192.168.100.92:8050/;    #项目需要的代理地址
        }
        location ^~ /cloudlink/zipkin/ {
            proxy_pass http://192.168.100.90:9411/;    #项目需要的代理地址
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

8. iframe跨域方法

当页面使用iframe标签内嵌其他页面的时候,那么浏览器会禁止JS操作内嵌页面元素,只有同源的页面才允许操作。

如果想要不同源的页面可以操作那么可使用如下方法:

8.1 降域:document.domain

如果两个窗口一级域名相同,只是二级域名不同, document.domain方法:获取/设置当前文档的原始域部分:

//初始值 "home.example.com" 
document.domain = "example.com"; 		//OK
document.domain = "home.example.com"; 	//NO,不能由松散变紧绷
document.domain = "example"; 			//NO,必须有一个点号
document.domain = "another.com"; 		//NO, 必须是有效域前缀或其本身

一个例子:

  1. 修改hosts文件,模拟2个域名
## sudo vi /etc/hosts 
// 在文件里添加如下2行内容
127.0.0.1 a.etc.com
127.0.0.1 b.etc.com
  1. 创建2个文件,然后启动http-server

    a.html

    <html>
    <head>
      <style>
        .ct{ width: 910px; margin: auto; }
        .main{ float: left; width: 450px; height: 300px; border: 1px solid #ccc; }
        .main input{ margin: 20px; width: 200px; }
        .iframe{ float: right; }
        iframe{ width: 450px; height: 300px; border: 1px dashed #ccc; }
      </style>
    </head>
    <div class="ct">
      <h1>使用降域实现跨域</h1>
      <div class="main">
        <input type="text" placeholder="a.html">
      </div>
      <iframe src="http://b.etc.com:8080/b.html" frameborder="0" ></iframe>
    </div>
    <script>
      document.querySelector('.main input').addEventListener('input', function(){
        console.log(this.value);
        window.frames[0].document.querySelector('input').value = this.value;
      })
      document.domain = "etc.com"
    </script>
    </html>
    

    b.html

    <html>
    <head>
      <style>
        html,body{ margin: 0; }
        input{ margin: 20px; width: 200px; }
      </style>
    </head>
    <body>
    <input id="input" type="text"  placeholder="b.html">
    <script>
      document.querySelector('#input').addEventListener('input', function(){
        window.parent.document.querySelector('input').value = this.value;
      })
      document.domain = 'etc.com';
    </script>
    </body>
    </html>
    
  2. 终端打开该文件目录,启动服务:

    ~ http-server
    
    Starting up http-server, serving ./
    Available on:
      http://127.0.0.1:8080
      http://192.168.43.183:8080
    Hit CTRL-C to stop the server
    
  3. 浏览器中打开链接:http://a.etc.com:8080/a.html就能看见效果。

    如果不在两个页面中设置document.domain = 'etc.com';, 那么久没有效果。

Document.domain MDN

8.2 window.name实现跨域

整理、转载自:https://www.cnblogs.com/zhuzhenwei918/p/7403796.html

window.name这个属性不是一个简单的全局属性 --- 只要在一个window下,无论url怎么变化,只要设置好了window.name,那么后续就一直都不会改变,同理,在iframe中,即使url在变化,iframe中的window.name也是一个固定的值,利用这个,我们就可以实现跨域了。

localhost:8088/test2.html

<html lang="en">
<head>
  <title>test2</title>
</head>
<body>
  <h2>test2页面</h2>
  <script>
    var person = { name: 'wayne zhu', age: 22, school: 'xjtu' }
    window.name = JSON.stringify(person)
  </script>
</body>
</html>

把test2.html中的数据传递出去,到localhost:8081/test1.html中去。

localhost:8081/test1.html

<html lang="en">
<head>
  <title>test1</title>
</head>
<body>
  <h2>test1页面</h2>
  <iframe src="http://localhost:8088/test2.html" frameborder="1"></iframe>
  <script>
    var ifr = document.querySelector('iframe')
    ifr.style.display = 'none'
    var flag = 0;
    ifr.onload = function () {
        if (flag == 1) {
            console.log('跨域获取数据', ifr.contentWindow.name);
            ifr.contentWindow.close();
        } else if (flag == 0) {
            flag = 1;
            ifr.contentWindow.location = 'http://localhost:8081/proxy.html';
        }
    }
  </script>
</body>
</html>

这里的意图很明确,就是使用iframe将test2.html加载过来,因为只是为了实现跨域,所以将之隐藏,但是,这时已经完成了最重要的一步,就是将iframe中window.name已经成功设置,但是现在还获取不了,因为是跨域的,所以,我们可以把src设置为当前域的proxy.html。

另外,这里之所以要设置flag,是因为每当改变location的时候,就会重新来一次onload,所以我们希望获取到数据之后,就直接close(),故采用此种方法。

proxy.html内容如下:

<html lang="en">
<head>
  <title>proxy</title>
</head>
<body>
  <p>这是proxy页面</p>
</body>
</html>

8.3 window.postMessage页面通信

postMessge()是HTML5新定义的通信机制。所有主流浏览器都已实现。

postMessage()方法允许跨窗口通信,不论这两个窗口是否同源。

  • 发送消息
otherWindow.postMessage(message, targetOrigin);

otherWindow:其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames

message: 将要发送到其他 window的数据。

targetOrigin:接收消息的窗口的源(origin),即"协议 + 域名 + 端口"。也可以设为*,表示不限制域名,向所有窗口发送。

  • 监听处理消息

父窗口和子窗口都可以通过message事件,监听对方的消息。

window.addEventListener('message', function(e) {
  console.log(e.data);
});

message事件的事件对象event,提供以下三个属性。

  • event.source:发送消息的窗口
  • event.origin: 消息发向的网址
  • event.data: 消息内容

window.postMessage mdn

例子

上面的代码修改一下:

a.html

<script>
  function $(id){
    return document.querySelector(id);
  }
  $('.main input').addEventListener('input', function(){
    console.log(this.value);
    window.frames[0].postMessage(this.value,'*');
  })
  window.addEventListener('message',function(e) {
      $('.main input').value = e.data
      console.log(e.data);
  });
</script>

b.html

<script>
	$('#input').addEventListener('input', function(){
		window.parent.postMessage(this.value, '*');
	})
	window.addEventListener('message',function(e) {
		$('#input').value = e.data
	    console.log(e);
	});
	function $(id){
		return document.querySelector(id);
	}
</script>

参考资料

饥人谷跨域课件

跨域资源共享 CORS 详解 阮一峰

浏览器同源政策及其规避方法 阮一峰

关于跨域 SegmentFault

Last Updated: 4/10/2024, 2:40:05 PM