跨域

头像
bamboo
    阅读 12 分钟

    一、同源策略

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

    1、本域

    同协议:如都是http或者https
    同域名:如都是http://jirengu.com/ahttp://jirengu.com/b
    同端口:如都是80端口
    举个例子

    http://jirengu.com/a/b.js 和 http://jirengu.com/index.php (同源)
    不同源
    http://jirengu.com/main.js 和 https://jirengu.com/a.php (协议不同)
    http://jirengu.com/main.js 和 http://bbs.jirengu.com/a.php (域名不同,域名必须完全相同才可以)
    http://jiengu.com/main.js 和 http://jirengu.com:8080/a.php (端口不同,第一个是80)

    2、通过ajax获取数据,演示同源和不同源

    首先打开hosts文件(window的地址是C:WindowsSystem32driversetchosts ),添加两条host记录

    clipboard.png
    新建一个index.html文件,里面实现一个ajax获取数据的功能

    <h1>hello world</h1>
    <script>
      var xhr = new XMLHttpRequest()
      xhr.open('GET','http://localhost:8080/getWeather', true)
      xhr.send()
      xhr.onload = function(){
        console.log(xhr.responseText)
      }
    </script>

    新建一个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)

    gitbash cd到当前文件夹,通过node server.js打开服务器。
    在浏览器输入localhost:8080,结果成功获取ajax数据
    clipboard.png
    我把ajax请求地址改成http://a.com:8080/getWeather,结果报错了

    clipboard.png

    我把浏览器的地址改为a.com 把ajax的地址改为b.com或者localhost,或者127.0.0.1都会出现跨域报错,即使他们的地址都是指向服务器。

    二、JSONP(JSON with padding)

    1、概念

    HTML 中 script 标签可以加载其他域下的js,也就是说script的src能使用任何网站对应得文件,只要该网站愿意去提供这个东西。

    <script src="http://api.jirengu.com/weather.php"></script>

    这时候会向天气接口发送请求获取数据,获取数据后做为 js 来执行。 但这里有个问题, 数据是 JSON 格式的数据,直接作为 JS 运行的话我如何去得到这个数据来操作呢?

    这样我们可以和后端商量一下,这样执行:

    <script>
    function showData(ret){
    console.log(ret);
    }
    </script>
    <script src="http://api.jirengu.com/weather.php?callback=showData"></script>

    前端提前定义好showdata这个函数。当这个请求到达后端后,后端会去解析callback这个参数获取到字符串showData,在发送数据做如下处理:
    之前后端返回数据: {"city": "hangzhou", "weather": "晴天"} 现在后端返回数据: showData({"city": "hangzhou", "weather": "晴天"}) 前端script标签在加载数据后会把 「showData({“city”: “hangzhou”, “weather”: “晴天”})」做为 js 来执行。实际上就是调用showData这个函数,同时参数是 {“city”: “hangzhou”, “weather”: “晴天”}。

    总结:JSONP是通过 script 标签加载数据的方式去获取数据并把数据当做 JS 代码来执行。 提前在页面上声明一个函数,函数名通过接口传参的方式传给后台,后台解析到函数名后在原始数据上「包裹」这个函数名,发送给前端。换句话说,JSONP 需要对应接口的后端的配合才能实现。

    2、举个栗子

    2.1代码

    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)
    <!DOCTYPE html>
    <html>
    <body>
      <div class="container">
        <ul class="news">
        </ul>
        <button class="show">show news</button>
      </div>
    
    <script>
    
      $('.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;
      }
    
      function $(id){
        return document.querySelector(id);
      }
    </script>
    
    </html>

    打开终端,cd到对应的文件夹,输入 node server.js ,浏览器打开 http://localhost:8080/index.html。

    2.2执行结果

    ![clipboard.png](/img/bVbk
    clipboard.png

    2.3分析

    2.3.1

    res.end(pathObj.query.callback + '(' + JSON.stringify(news) + ')')

    数组和字符串相加,会把数组toString(),将数组转换为以‘,’分隔的字符串【1,2,3】=>'1,2,3'。如果我不用json.stringify传递的news就是一个字符串,html里面就无法当成数组使用

    clipboard.png

    2.3.2

     if(pathObj.query.callback){
     res.end(pathObj.query.callback + '(' + JSON.stringify(news) + ')')

    以下是pathobj的输出值,可以看到pathObj.query是一个对象,所以才可以使用pathObj.query.callback获取对应的值

    clipboard.png

    2.3.3

     function appendHtml(news){
        console.log(news);  
        var html = '';
        for( var i=0; i<news.length; i++){
          html += '<li>' + news[i] + '</li>';
        }
        console.log(html);
        $('.news').innerHTML = html;
      }
    

    appendHtml函数收到的参数news是一个数组,和html参数是一个字符串

    clipboard.png

    2.3.4

     script.src = 'http://127.0.0.1:8080/getNews?callback=appendHtml';
        document.head.appendChild(script);
        document.head.removeChild(script);

    把script放在document.head,是为了让这句代码执行发送请求。删除removechild,是为了美观,如果不删除,每次点击news都会重新产生一个script。如下图我点击news一次,head就会新增一个script
    clipboard.png

    三、CORS

    关于cors,我只是记录了一个简单的情况,经用于入门。详细可以看这篇文章跨域资源共享 CORS 详解

    1、概念

    CORS 全称是跨域资源共享(Cross-Origin Resource Sharing),是一种 ajax 跨域请求资源的方式,支持现代浏览器,IE支持10以上。

    2、实现原理

    当你使用 XMLHttpRequest 发送请求时,浏览器发现该请求不符合同源策略,会给该请求加一个请求头:Origin
    后台进行一系列处理,如果确定接受请求则在返回结果中加入一个响应头:Access-Control-Allow-Origin(允许访问控制的域)和对应的值;
    浏览器判断该相应头中是否包含 Origin 的值,如果有则浏览器会处理响应,我们就可以拿到响应数据,如果不包含浏览器直接驳回,这时我们无法拿到响应数据。

    3、代码

    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('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>

    4、执行结果

    http://127.0.0.1:8080/index.html,不跨域的打开index.html发送请求时,请求头内部没有origin

    clipboard.png

    当我用http://localhost:8080打开index.html,出现跨域时。
    请求头内部有Origin: http://localhost:8080,响应头有Access-Control-Allow-Origin: http://localhost:8080。两者相等,正常的获取数据

    clipboard.png

    当我使用了a.com打开index.html时(我修改了host文件让a.com也指向127的本机服务器地址),出现了报错。因为服务器不允许a.com的网页使用资源

    clipboard.png

    5、代码解析

    res.setHeader('Access-Control-Allow-Origin','http://localhost:8080')
    //res.setHeader('Access-Control-Allow-Origin','*')

    这个表示服务器添加允许控制的域对应的地址,*表示允许所有接口用服务器的资源

    四、降域

    1、iframe和网页不同源

    网页的内联iframe和网页不同源,我们不能通过js操作该iframe.比如我们在自己的网站上嵌套一个淘宝的frame,等用户先登陆了淘宝,然后登陆我们的网页的时候,处于frame的淘宝也是登陆状态,如果我能用js去操作这个用户的淘宝,那我就可以做很多坏事了。
    2、举个例子

    <html>
    <head>
      <meta charset="utf-8">
    
    <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="http://a.com:8080/a.html">
      </div>
    
      <iframe src="http://b.com:8080/b.html" frameborder="0" ></iframe>
    
    </div>
    
    
    <script>
    //URL: http://a.jrg.com:8080/a.html
    
    document.querySelector('.main input').addEventListener('input', function(){
      console.log(this.value);
      window.frames[0].document.querySelector('input').value = this.value;
    })
    
    
    document.domain = "jrg.com"
    
    
    
    </script>
    </html>

    首先修改host文件,把a.com和b.com指向127.0.0.1,打开http-server。

    clipboard.png

    用a.com网址打开a.html,其中b.jrg.com的iframe的地址是b.com,和网页不同源的。可以看到该frame可以正确加载,但我们不能用js操作它
    clipboard.png
    用b.com地址打开a.html,其中b.jrg.com的iframe的地址是b.com,和网页同源。我们就可以用js去操作该iframe。

    clipboard.png

    2、降域

    如果当前页面和iframe域名后面部分一致都是jrg.com,我们可以使用document.domain = "jrg.com"降域的方式来实现跨域

    clipboard.png

    五、postMessage

    通过postMessage实现不同主域下frame的操作

    window.frames[0].postMessage(this.value,'*');//* ,表示任何域下都可接受请求
     window.parent.postMessage(this.value, '*');
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Document</title>
    </head>
    <body>
        <html>
    <style>
        html,body{
            margin: 0;
        }
        input{
            margin: 20px;
            width: 200px;
        }
    </style>
    
        <input id="input" type="text"  placeholder="http://b.jrg.com:8080/b.html">
    <script>
    
    // URL: http://b.jrg.com:8080/b.html
     
    $('#input').addEventListener('input', function(){
        window.parent.postMessage(this.value, '*');
        
    })
    
    window.addEventListener('message',function(e) {
            $('#input').value = e.data
            console.log(e.data);
    });
    
    function $(id){
        return document.querySelector(id);
    }    
    
    </script>
    
    </body>
    </html>
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Document</title>
    </head>
    <body>
    
    <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>
    
    <div class="ct">
        <h1>使用postMessage实现跨域</h1>
        <div class="main">
            <input type="text" placeholder="http://a.jrg.com:8080/a.html">
        </div>
    
        <iframe src="http://localhost:8080/b.html" frameborder="0" ></iframe>
    
    </div>
    
    
    <script>
    //URL: http://a.jrg.com:8080/a.html
    
    $('.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);
    });
    
    
    
    function $(id){
        return document.querySelector(id);
    }
    
    </script>
        
    </body>
    </html>

    打开http-server,查看结果
    clipboard.png


    bamboo
    75 声望9 粉丝

    bamboo前端学习笔记