资讯专栏INFORMATION COLUMN

关于跨域以及跨域的实现方式

崔晓明 / 2007人阅读

摘要:关于跨域为什么会有跨域我们得先了解下同源策略。简而言之,同协议同域名同端口号什么是跨域跨域就是采取技术方案突破同源策略的限制,实现不同域之间交互请求响应。

关于跨域 why?

为什么会有跨域?

我们得先了解下 ==同源策略(SOP, Same Origin Policy)==。

浏览器出于安全方面的考虑,只能访问与包含它的页面位于同一个域中的资源,该策略为通信设置了“相同的协议、相同的域、相同的端口”这一限制。试图访问上述限制之外的资源,都会引发安全错误。这种安全策略可以预防某些恶意行为。

简而言之,

同协议 Same Protocol

同域名 Same Hostname

同端口号 Same Port

Same Protocol && Same Hostname && Same Port

What?

什么是跨域?

==跨域就是采取技术方案突破同源策略的限制,实现不同域之间交互(请求响应)。==

How?

那么如何实现跨域呢?
有以下几种方法。

==方法一==

CORS (Cross-Origin Resource Sharing,跨域源资源共享),是一种ajax跨域请求资源的方式,支持现代浏览器,IE支持10以上,通过XMLHttpRequest实现Ajax通信的一个主要限制就是同源策略。
CORS是W3C的一个工作草案,定义了在必须访问跨境资源时,浏览器和服务器该如何沟通。CORS的基本思想,就时使用自定义的HTTP头部让浏览器和服务器进行沟通,从而决定请求或者响应应该成功还是失败。
实现思路:使用XMLHttpRequest发送请求时,浏览器会给该请求自动添加一个请求头:Origin。服务器经过一系列处理,如果确定请求来源页面属于白名单,则在响应头部加入首部字段:Access-Control-Allow-Origin。浏览器比较请求头部的Origin 和响应头部的 Access-Control-Allow-Origin是否一致,一致的话,浏览器得到响应数据。如果服务器没有设置Access-Control-Allow-Origin 或者这个头部源信息不匹配,浏览器就会驳回请求。

模拟CORS的实现

步骤1.

如何伪装一个网站(在本地)?

1.编辑hosts文件

苹果mac: 直接在git bash上输入命令行操作即可 “sudo vi /etc.hosts” ,或者下载一些图形界面应用软件直接修改。

Windows操作系统:

win键(四个方块的键)+ R = 弹开运行窗口

复制该文件路径 c:windowssystem32driversetc

选中hosts文件,右键-属性-安全-选择组或用户名(添加修改保存的权限的对象)- 编辑 - 再次选择组或用户名(添加修改保存的权限的对象 - 勾选权限(选项在此不表)

打开hosts文件,写入 127.0.0.1 localhost;127.0.0.1 bai.com;127.0.0.1 google.com;可以写入你任何你想模拟的网站,按照这种对应关系格式即可, ip地址+域名。

步骤2.
所需工具
node.js && git bash(模拟服务器),一个简单的html页面里面有个跨域请求的Ajax通信。





    
    Google
    


    

hello world

//nodeJS模拟后端响应CORS的实现
var http = require("http");
var fs = require("fs");
var url = require("url");
var path = require("path");

http.createServer(function(req, res){

     var urlObj = url.parse(req.url, true)

    switch (urlObj.pathname){

    case "/getNews":
    var news = ["NBA Break News","CBA Break News"]
    //CORS的实现
    res.setHeader("Access-Control-Allow-Origin","http://google.com:8080")
    /*res.setHeader("Access-Control-Allow-Origin","*")
    服务器设置公用接口
    */
    res.end(JSON.stringify(news));
    break;

    case "/" :
    if(urlObj.pathname == "/") {
        urlObj.pathname += "index.html"
    }

    default: 
    var filePath = path.join(__dirname, urlObj.pathname);
    fs.readFile(filePath,"binary", function(error, fileContent){
        if(error){
            console.log("404")
            res.writeHeader(404, "not found")
            res.end("

404,not found

") }else { res.write(fileContent, "binary") } }) } }).listen(8080);

上面代码就是CORS实现的过程。

在本地修改hosts文件,127.0.0.1 google.com, 页面的url为 http://google.com:8080。

在title为google的页面上添加一个ajax请求,该请求以get方法会向baiduServer的端口("http://baidu.com:8080/getNews")发送一个请求。

浏览器会给请求头加上Origin: http://google.com:8080, Request URL: http://baidu.com:8080/getNews。

baiduServer后端,响应头添加首部字段。Access-Control-Allow-Origin: http://google.com:8080。 表明该服务器(baiduServer)接受请求并给予响应。

浏览器比较请求头部的Origin 和响应头部的 Access-Control-Allow-Origin是否一致,一致的话,浏览器得到响应数据。如果服务器没有设置Access-Control-Allow-Origin: http://google.com:8080 或者这个头部源信息不匹配,浏览器就会驳回请求。

当然服务器也可以设置公用接口, res.setHeader("Access-Control-Allow-Origin","*")

服务器设置公用接口, 任何人都可以使用该服务器这个端口的数据。


==方法二==

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

JSONP的原理,通过动态

nodeJS

var http = require("http");
var fs = require("fs");
var path = require("path");
var url = require("url");

http.createServer(function(req,res){
    var urlObj = url.parse(req.url, true);
    switch(urlObj.pathname) {
        case "/getNews" :
        var news = ["NBA Break News","CBA Break News"];
        res.setHeader("Content-Type","text/javascript; charset=utf-8");
        if(urlObj.query.newsData){
            var data = urlObj.query.newsData + "(" + JSON.stringify(news) + ")" ;
            res.end(data);
        } else {
            res.end(JSON.stringify(news))
        }
        break;

        case "/" :
        if(urlObj.pathname == "/") {
            urlObj.pathname +=  "index.html"
        }

        default:
            fs.readFile(path.join(__dirname, urlObj.pathname), function(error, data) {
                if(error) {
                    res.writeHeader(404, "not found");
                    res.end("

404, Not Found

"); } else { res.end(data) } }); }; }).listen(8080);

==方法三==

降域,主要应用场景是同一页面下不同源的框架iframe请求

基于iframe实现的跨域,要求两个域都必须属于同一个基础域, 比如 a.xx.com, b.xx.com,都有一个基础域xx.com, 使用同一协议和端口,这样在两个页面中同时添加documet.domain,就可以实现父页面操控子页面(框架)。

关于document.domain, 用来得到当前网页的域名。在浏览器输入URL,wwww.baidu.com。 http://wwww.baidu.com, document.domain 为 "www.baidu.com"。 也可以为document.domain赋值, 不过有限制,就是前面提到的,只能赋值为当前的域名或者基础域名。
范例:

document.domain = "www.baidu.com" //successed 赋值成功, 当前域名。

document.domain = "baidu.com" // successed 赋值成功, 基础域名。

但是下面的赋值会报错(参数无效)。

"VM50:1 Uncaught DOMException: Failed to set the "domain" property on "Document": "a.baidu.com" is not a suffix of "www.baidu.com".

at :1:17"。

范例
document.domain = "google.com" // fail, 参数无效

document.domain = "a.baidu.com" // fail, 参数无效

因为google.com 和 a.baidu.com不是当前的域名,也不是当前域名的基础域名。
原因: 浏览器为了防止恶意修改document.domain来实现跨域偷取数据。

-- --
==模拟降域的实现==

错误范例:

hosts 文件设置 win10系统路径为 c:windowssystem32driversetchosts
127.0.0.1 a.com
127.0.0.1 b.com

a.com的一个网页(a.html)里面 利用iframe引入一个b.com里的一个网页(b.html )。在a.html里面可以看到b.html的内容,但不能用Javascript来操作它。
原因: 这两个页面属于不同的域,在操作之前,浏览器会检测两个页面的域是否相等,相等则允许操作,不相等则报错。
这个例子里,不可能把a.html与b.html,利用JS改成同一个域。原因:两个域的基础域名不相等。

在http://a.com:8080/a.html的控制台(console), 输入代码window.frames[0].document.body //VM150:1 Uncaught DOMException: Blocked a frame with origin "http://a.com:8080" from accessing a cross-origin frame.

at :1:18




    
    a.com:8080/a.html


    





    
    b.com:8080/b.html


    

this is b.html

//nodeJS 
var http = require("http");
var fs = require("fs");
var path = require("path");
var url = require("url");

http.createServer(function(req,res){
    var urlObj = url.parse(req.url, true);
    switch(urlObj.pathname) {
        case "/getNews" :
        var news = ["NBA Break News","CBA Break News"];
        res.setHeader("Content-Type","text/javascript; charset=utf-8");
        if(urlObj.query.newsData){
            var data = urlObj.query.newsData + "(" + JSON.stringify(news) + ")" ;
            res.end(data);
        } else {
            res.end(JSON.stringify(news))
        }
        break;

        case "/" :
        if(urlObj.pathname == "/") {
            urlObj.pathname +=  "index.html"
        }

        default:
            var filePath = path.join(__dirname, "static" ,urlObj.pathname);
            console.log(filePath)
            fs.readFile(filePath, function(error, data) {
                if(error) {
                    res.writeHeader(404, "not found");
                    res.end("

404, Not Found

"); } else { res.end(data) } }); }; }).listen(8080);

可以把iframe的src改变为"http://a.com:8080/b.html",这样就可以了,是不会有这个问题的,因为域相等。
控制台不会报错,但是这样没完成跨域。可以使用html5中的postMessage来实现,针对基础域不同的框架,这里暂且不表, 在方法四,会用到这种方法。

window.frames[0].document.body

​this is b.html ​

​​




    
    a.com:8080/a.html


    
    

==正确范例:
降域的实现==

hosts文件设置

基础域名相同

127.0.0.1 a.shawroc.com

127.0.0.1 b.shawroc.com

a.shawroc.com的里面一个网页(a.html)引入b.shawroc.com里的一个网页(b.html),a.shawroc.com还是不能操作b.shawroc.com里面的内容。
原因:document.domain不一样,a.shawroc.com vs b.shawroc.com。
但是两个页面的基础域名是一样的,通过JS,将两个页面的domain改成一样。
在a.html 和 b.html 里都加入

这样在两个页面中同时添加document.domain, 就可以实现父页面操控子页面(框架)。

控制台
window.frames[0].document.body
//console输出


    

this is http://b.shawroc.com:8080/b.html

代码




    
    a.shawroc.com:8080


    
    
    




    
    b.shawroc.com:8080/b.html


    

this is http://b.shawroc.com:8080/b.html

==方法四==

html5的postMessage API

html5引入的postMessage()方法,允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。

postMessage(data, origin) 方法,接受两个参数。

1.data:要传递的数据,html5规范中提到该参数可以是JavaScript的任意基本类型或可复制的对象,然而并不是所有浏览器都做到了这点儿,部分浏览器只能处理字符串参数,所以我们在传递参数的时候需要使用JSON.stringify()方法对对象参数序列化,在低版本IE中引用json2.js可以实现类似效果。

2.origin:字符串参数,指明目标窗口的源,协议+主机+端口号[+URL],URL会被忽略,所以可以不写,这个参数是为了安全考虑,postMessage()方法只会将message传递给指定窗口,当然如果愿意也可以建参数设置为"*",这样可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。

范例

模拟postMessage的工作机制

改写hosts文件

127.0.0.1 a.com

127.0.0.1 b.com






    
    a.com:8080


    




    
    b.shawroc.com:8080/b.html


    

this is http://b.com:8080/b.html

//nodeJS  模拟后端
var http = require("http");
var fs = require("fs");
var path = require("path");
var url = require("url");

http.createServer(function(req,res){
    var urlObj = url.parse(req.url, true);
    switch(urlObj.pathname) {
        case "/getNews" :
        var news = ["NBA Break News","CBA Break News"];
        res.setHeader("Content-Type","text/javascript; charset=utf-8");
        if(urlObj.query.newsData){
            var data = urlObj.query.newsData + "(" + JSON.stringify(news) + ")" ;
            res.end(data);
        } else {
            res.end(JSON.stringify(news))
        }
        break;

        case "/" :
        if(urlObj.pathname == "/") {
            urlObj.pathname +=  "index.html"
        }

        default:
            var filePath = path.join(__dirname, "postMessage" ,urlObj.pathname);
            fs.readFile(filePath, function(error, data) {
                if(error) {
                    res.writeHeader(404, "not found");
                    res.end("

404, Not Found

"); } else { res.end(data) } }); }; }).listen(8080);

解析代码
步骤1, a.com:8080/a.html页面下的input发生输入事件时, 向目标窗口发一个MessageEvent事件