跨域方式知多少

前言

好记性不如烂笔头,所以学完跨域之后,我还是老实结合demo来整理一篇跨域实现方式的详记。当然,因为我现在学习阶段,并没有跨域的实战经历,所以这篇整理,纯粹停留在我对跨域的理解层面。
  等后面接触到更多跨域知识,以及经历项目实战的跨域处理,有更透彻的进阶理解,就再做跨域知识的补充或跨域实战的记录。
  这次说的跨域方式有四种:

1、JSONP
2、 CORS
3、 降域(document.domain)
4、 postMessage

一、为何要跨域——因为“同源策略”

1、什么是跨域
通俗来说,就是两个不同域名的网站的JavaScript脚本的交互。
常见交互有:一方发送请求要获取数据,对方响应并传输数据;操作网站页面的DOM元素等。

2、为何要跨域

现实场景中,肯定有很多时候是需要跨域请求、传输数据的。这些合理的用途会被浏览器默认阻止。

那么重点来了,为什么浏览器会默认阻止跨域操作呢?
  因为所有浏览器都奉行“同源策略”

同源策略:只允许同源的JS脚本(或者说接口)进行交互。不同源的情况下,不能读写对方的任何资源。

举例来说,http://www.jianshu.com这个网址,http://协议,www.jianshu.com是域名,端口号不写的情况下默认是80
  那么,同源需要满足下面三点,任何一点不同就视为不同源:

1、同协议:常见协议有http://、https://、file://(本地文件)、ftp://协议
2、同域名:比如www.example.com/dir2/other.html和www.example.com/dir/page.html
3、同端口号:URL默认不写端口,默认端口就是80。
(注意默认80端口和8080端口是不同的,两者不等同。)

同源策略是浏览器出于信息安全考虑,防止恶意网站窃取用户数据。
  例如用户在登录某一网上银行网站后,又去登录其他恶意网站。如果没有同源策略,恶意网站的后台就可以获取网银网页的用户信息。因为浏览器对提交表单并没有同源策略的限制,假如用户登录网银后忘记退出登录。因为登录密码信息存储在cookie中,恶意网站就可以获取用户登录密码等信息,冒充用户,进行恶意操作。

二、使用Ajax跨域请求失败例子

在讲解跨域实现的方式之前,先来实例演示下,不使用任何跨域方式,只是纯粹ajax技术跨域或同域名请求数据时浏览器会做出的默认举动:
  例子:当前页面显示3个新闻标题,点击“换一组”按钮,从指定接口获取数据,随机生成新的3个新闻标题。

index.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>ajax获取数据</title>
    <style>
        .container{
            width: 900px;
            margin: 0 auto;
        }
    </style>
</head>
<body>
    <div class="container">
        <ul class="news">
            <li>随机变换新闻标题1</li>
            <li>随机变换新闻标题2</li>
            <li>随机变换新闻标题3</li>
        </ul>
        <button class="change">换一组</button>
    </div>
    <script>
        function $(str){
            return document.querySelector(str);
        }
        var change=$('.change');
        /*ajax的写法*/
        change.addEventListener('click',function(){
            var xhr=new XMLHttpRequest();
            xhr.open('get','http://bbhuangyh.com:8080/getNews',true); //指定接口获取数据
            xhr.send();
            xhr.onreadystatechange=function(){
                if(xhr.readyState===4 && (xhr.status===200 || xhr.status===304)){
                    //将服务端返回的数据,作为自定义appendHtml()函数的参数
                    appendHtml(JSON.parse(xhr.responseText)) 
                }
            }
        })

        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>
</body>
</html>

在模拟的服务端,返回JSON格式的数据:

app.get('/getNews',function(req,res){
    var news=[
        "输入内容1",
        "输入内容2",
        "输入内容3",
        "输入内容4",
        "输入内容5",
        "输入内容6",
        "输入内容7",
        "输入内容8",
        "输入内容9"
    ]
    var data=[];
    for(var i=0;i<3;i++){
        var index=parseInt(Math.random()*news.length); //随机选择news数组的下标
        data.push(news[index]);
    }
    /*ajax的写法*/
    res.send(data);
    //console.log(data) //随机得到:[ '输入内容1', '输入内容4', '输入内容3' ]
})
1、同源域名登录的情况

设置host文件,让www.bbhuangyh.com和www.aahuangyh.com等于127.0.0.1。然后使用http://bbhuangyh.com:8080/域名登录。此时网页的域名和数据请求接口的域名xhr.open('get','http://bbhuangyh.com:8080/getNews',true)相同。点击“换一组”按钮,就能从接口返回数据,获取替换页面数据,实现效果。

334.JPG

2、不同源域名登录的情况

数据请求接口的域名是'http://bbhuangyh.com:8080/getNews,而现在使用localhost:8080或者http://aahuangyh.com:8080/这个完全不同的域名(即使实际都是指向127.0.0.1)来登录,点击“换一组”。这种情况下就是,不同源请求获取数据,浏览器会进行阻拦。点击“换一组”的操作无效。

数据请求情况
preview没有数据返回
response没有数据返回

浏览器返回的错误提醒:

XMLHttpRequest cannot load http://bbhuangyh.com:8080/getNews. 
No 'Access-Control-Allow-Origin' header is present on the requested resource. 
Origin 'http://localhost:8080' is therefore not allowed access.

三、跨域方法——JSONP

1、JSONP的原理

(1) <script> 标签是不受同源策略的限制的,它可以载入任意地方的 JavaScript 文件。
  (2)对页面script元素的src指定接口路径。默认执行src所指路径,就会向后台发起跨域数据请求。
  (3)在前端自定义会执行某项操作的函数。前端后台共同约定好这个自定义函数名。
  (4)后端响应数据请求后,对响应数据进行包装:把响应数据转化为JSON格式的字符串,然后用约定好的函数名称包裹住。然后将这包装后的 JSON数据返回到前端的script标签中,作为JS语句在script中执行。这样恰好就可以调用执行约定好的函数,并且将数据作为参数传入。

下面demo例子中的src路径执行后的返回包装数据

通过这样的实现思路,其实就是把跨域的冲突给消解了,变成是执行获取script元素的src所指内容,而通过后台数据包装,src所指内容获取到的返回数据,刚好能够执行前端html定义的指定函数。所以在这种情况下,只要后端服务有打开,前端的html文件即使通过file协议在浏览器打开,也能正常执行获取数据。

适用情况:

(1)前后端间会有接触,能至少共同约定通过URL传递的参数名称(如例子中的callback,通过req.query.callback得到函数名)或者是共同约定自定义的函数名称。
(2)JSONP只能发GET请求

2、JSONP的实例

前面讲解了原理,我们现在来看下具体的例子吧:
  讲解这个跨域方法,我们还是使用前面点击“换一组”的例子demo。但JSONP的实现和Ajax没任何关系了,所以我们不再需要创建新的XMLHttpRequest对象。其本质是通过script标签的src属性来实现的,并且仍然需要服务器端支持,让src的link链接有数据返回。
  对应的demo例子index.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>JSONP获取数据</title>
    <style>
        .container{
            width: 900px;
            margin: 0 auto;
        }
    </style>
</head>
<body>
    <div class="container">
        <ul class="news">
            <li>随机变换新闻标题1</li>
            <li>随机变换新闻标题2</li>
            <li>随机变换新闻标题3</li>
        </ul>
        <button class="change">换一组</button>
    </div>
    <script>
        function $(str){
            return document.querySelector(str);
        }
        var change=$('.change');
        // /*JSONP的写法*/
        change.addEventListener('click',function(){
            var script=document.createElement('script');
            script.src='http://bbhuangyh.com:8080/getNews?callback=appendLi';
            //callback为传给后端的参数,值为appendLi
            //appendLi为约定好的函数名,可随意更改,只要和前端定义好的函数名一样就行
            document.head.appendChild(script);
            document.head.removeChild(script);
        })
        function appendLi(news){
            var html='';
            for(var i=0; i<news.length;i++){
                html+='<li>'+news[i]+'</li>';
            }
            console.log(html);
            $('.news').innerHTML=html;
        }

    </script>
</body>
</html>

在模拟的服务端,改写的代码:

app.get('/getNews',function(req,res){
    var news=[
        "输入内容1",
        "输入内容2",
        "输入内容3",
        "输入内容4",
        "输入内容5",
        "输入内容6",
        "输入内容7",
        "输入内容8",
        "输入内容9"
    ]
    var data=[];
    for(var i=0;i<3;i++){
        var index=parseInt(Math.random()*news.length); //随机选择news数组的下标
        data.push(news[index]);
    }
    var cb=req.query.callback; 
    //这里的callback是自定义的名字,跟随html的url请求里的自定义名称一样就行。
    //优化做法:可以满足普通get方法获取,也可以jsonp方法获取
    if(cb){
        res.send(cb+'(' +JSON.stringify(data) + ')');
          //JSON.stringify(data)转化为JSON格式的字符串
         //console.log(cb+'(' +JSON.stringify(data) + ')')随机得到:appendLi(["输入内容8","输入内容6","输入内容4"])
    }else{
        res.send(data);
    }
})

使用localhost:8080、aahuangyh.com或者file协议打开都能随机返回三条数据做显示,实现跨域

localhost:8080打开

aahuangyh.com打开

四、跨域方法——CORS

前面举了个使用AJAX跨域失败的例子。而现在如果使用CORS的跨域方法的话,前面AJAX写法的demo,就能跨域生效。
  CORS是跨源资源分享(Cross-Origin Resource Sharing)的缩写。“它是W3C标准,是跨源AJAX请求的根本解决方法。”——引用

1、实现方法

CORS跨域方法的重点就在于,在后台代码上写上:res.header(“Accsess-Control-Allow-Origin”,“*”)
  一般,使用ajax跨域请求时,浏览器会在请求头Request headers加上“origin:发起请求的域名”
  在后台代码加上res.header(“Accsess-Control-Allow-Origin”,“*”),这样会让浏览器在后端响应头Response headers加上“Accsess-Control-Allow-Origin:*”*号表示对任何域名发送的请求,都给予回应和答应。

2、兼容性

header('Access-Control-Allow-Origin:*')是html5新增的一项标准功能。我们可以来看看CORS的兼容适用情况:

CORS的兼容情况.JPG

  可以看到,IE11以下是不支持的。,无法兼容低版本的IE浏览器来使用。但胜在CORS用法简单,支持所有类型的HTTP请求。使用普通的XMLHttpRequest对象,用AJAX的写法就能实现跨域。

3、实例

来看实例demo吧:
  还是前面AJAX跨域demo,html部分的写法相同,模拟后台部分的代码添加上这一句:

    app.get('/getNews',function(req,res){
    var news=[
        "AAA",
        "BBB",
        "CCC",
        "DDD",
        "EEE",
        "FFF",
        "GGG",
        "HHH",
        "III"
    ];
    var data=[];
    for(var i=0;i<3;i++){
        var index=parseInt(Math.random()*news.length);
        data.push(news[index]);
    }
    //cors跨域写法
    res.header("Access-Control-Allow-Origin","*");
    res.send(data);
    })
CORS方式下的Response Headers

当然,也可以采用指定写法:res.header(“Accsess-Control-Allow-Origin”,“具体URL域名”),这样浏览器就只会对这个指定的URL域名的请求进行回应。
例如:

res.header("Access-Control-Allow-Origin","http://bbhuangyh.com:8080");

就是指定允许http://bbhuangyh.com:8080来跨域访问xhr.open('get','http://aahuangyh.com:8080/getNews',true);指定的这个接口资源

使用指定的对应域名打开正常获取数据
使用别的域名来打开就会报错
(4)其他

当然,还有其他一些配置。例如允许浏览器携带cookie来访问接口资源,后台要写:Access-Control-Allow-Credentials: true
对应的“Accsess-Control-Allow-Origin:*”就不能是*号。而且前端部分的代码就需要配合写上:

var xhr=new XMLHttpRequest();
xhr.open('get',URL,true); //指定接口获取数据
xhr.withCredentials = true; //允许浏览器携带cookie来访问接口,前端配合要写这句
xhr.send();
xhr.onreadystatechange=handler;

五、跨域方法——降域

(1)适用情况

降域方法的实现是使用document.domain适用情景其实非常有限小众,常见于控制<iframe>窗口。比如当前页面下,有个iframe,其src地址是和我当前页面的网址域名不同域,但二级域名相同的情况下,而我需要对iframe的元素进行操作,这种场景下,我就使用降域方法。

(2)降域说明

什么是降域,实际举个例子,一目了然:
  比如我当前页面的网址URL是:http://a.huangyh.com:8080/a.html <iframe>的src地址是:http://b.huangyh.com:8080/b.html
  只有在两个域名是协议相同、端口相同、二级域名相同的情况下,使用document.domain="huangyh.com"降域,都只取域名的huangyh.com这部分,形同同域,然后在a.html和b.html都可以使用window对象对彼此进行dom操作。

(3)demo实例

在这个demo中,实现的是页面上有两个input,其中一个是iframe标签下打开的另一个页面的input。然后在其中任意一个input输入内容,另一个input会显示相同的内容。
  (1)当前页面a.html的代码:点击查看
  (2)iframe引用的b.html的代码:点击查看。需在服务端运行的情况下,查看效果。

通过对比来加深理解和印象:
  对着两部分代码,可以先看看不降域,先设置同源的情况是如何:
  (1)在当前页面的URL和iframe的src路径相同的情况下:

<body>
    <div class="ct">
        <h1>使用降域实现跨域</h1>
        <div class="main">
            <input type="text" placeholder="http://a.huangyh.com:8080/a.html">
        </div>
        <iframe src="http://a.huangyh.com:8080/b.html" frameborder="0"></iframe>
    </div>
</body>
</html>

(2)window.frames[0].document.body.querySelector('#input'),就能够操作到右侧iframe窗口的input。因为本质是同源的。

降域截图.jpg

(3)在左边Input输入值,右边input同步

现在,来修改iframe的src,与页面的URL不同域,就会报错:

<div class="main">
<input type="text" placeholder="http://a.huangyh.com:8080/a.html">
</div>
<iframe src="http://a.huangyh.com:8080/b.html" frameborder="0"></iframe>
不降域的情况下报错

这种情况下,就进行降域:
  在两边的html的script中都加入“document.domain="huangyh.com"”。结果两边的input输入都可以同步,实现页面URL和iframe的src不相同的情况下,也能跨域处理:

降域下的跨域操作
降域下的跨域操作结果.JPG

六、跨域方法——postMessage

(1)实现方式

HTML5中新增了window对象的window.postMessage方法,可以实现不同源的域名之间发送、监听消息。具体:

发送消息:windowObj.postMessage(message, targetOrigin);

  • windowObj:需要接收消息的window对象
  • message:发送的消息
  • targetOrigin:接收消息的window对象所在的域名。可以是*号,也可以指定一个明确的URL

监听消息:使用监听事件message

  • oragin:发送消息过来的域名
  • data:发送的数据
  • source:发送消息的window对象
(2)demo实例

简单说明之后,还是来看例子吧。
  这里还是使用降域的这个例子,实现一样的操作效果。但换成postMessage的方式来做,对原有代码做一些改动:
  a.html中的script部分:

<script>
document.querySelector('.main input').addEventListener('input',function(){
window.frames[0].postMessage(this.value,'*');
//window.frames[0]指iframe窗口
//postmessage是向外发送消息,把input输入的内容发送到是任何域名的iframe窗口上,因为有*号
})
window.addEventListener('message',function(e){
document.querySelector('.main input').value=e.data;
//自己所在的页面,监听消息,如果监听到消息,就把数据放到input上
})
</script>

b.html中的script部分:

<script>
document.querySelector('#input').addEventListener('input',function(){
window.parent.postMessage(this.value,'*')
//window.parent指iframe窗口的父窗口
//postmessage是向外发送消息,把input输入的内容发送到是任何域名的父窗口上
})
window.addEventListener('message',function(e){
document.querySelector('#input').value=e.data;
//自己所在的页面,监听消息,如果监听到消息,就把数据放到input上
})
</script>

实际在模拟后台运行后,打开a.html查看效果,结果在页面url和iframe的地址完全不同,也可以实现跨域数据互通:

postMessage跨域效果
跨域实现两个input的数据互递

后又后记:

这篇文断续写了挺久。写的时候,对跨域的认识相比学的时候更清晰了些。常翻常新。

参考文章:

https://segmentfault.com/a/1190000006908944
https://segmentfault.com/a/1190000003642057
http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html

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

推荐阅读更多精彩内容