New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

浅谈浏览器端JavaScript跨域解决方法 #5

Open
rccoder opened this Issue Feb 29, 2016 · 30 comments

Comments

Projects
None yet
@rccoder
Owner

rccoder commented Feb 29, 2016

由于安全的原因,浏览器做了很多方面的工作,由此也就引入了一系列的跨域问题,需要注意的是:

跨域并非浏览器限制了发起跨站请求,而是跨站请求可以正常发起,但是返回结果被浏览器拦截了。最好的例子是 CSRF 跨站攻击原理,请求是发送到了后端服务器无论是否跨域!注意:有些浏览器不允许从HTTPS的域跨域访问HTTP,比如Chrome和Firefox,这些浏览器在请求还未发出的时候就会拦截请求,这是一个特例

1. JSONP

JSONP的全称是 "JSON With Padding", 词面意思上理解就是 "填充式的JSON"。它不是一个新鲜的东西,隶属于 JSON 的一种使用方法,或者说是一种使用模式,可以解决一些常见的浏览器端网页跨域问题。

正如他的名称一样,它是指被包含在调用函数中的JSON,比如这样:

callback({"Name": "小明", "Id" : 1823, "Rank": 7})

由于 jQuery 的一些原因,使得 JSONP 常常与 Ajax 混淆。实际上,他们没有任何关系。

由于浏览器的同源策略,使得在网页端出现了这个“跨域”的问题,然而我们发现,所有的 src 属性并没有受到相关的限制,比如 img / script 等。

JSONP 的原理就要从 script 说起。script 可以执行其他域的js 函数,比如这样:

a.html
...
<script>
  function callback(data) {
    console.log(data.url)
  }
</script>

<script src='b.js'></script>
...


b.js
callback({url: 'http://www.rccoder.net'})

显然,上面的代码是可以执行的,并且可以在console里面输出http://www.rccoder.net

利用这一点,假如b.js里面的内容不是固定的,而是根据一些东西自动生成的, 嗯,这就是JSONP的主要原理了。回调函数+数据就是 JSON With Padding 了,回调函数用来响应应该在页面中调用的函数,数据则用来传入要执行的回调函数。

至于这个数据是怎么产生的,说粗鲁点无非就是字符串拼接了。

简单总结一下: Ajax 是利用 XMLHTTPRequest 来请求数据的,而它是不能请求不同域上的数据的。但是,在页面上引用不同域的 js 文件却是没有任何问题的,这样,利用异步的加载,请求一个 js 文件,而这个文件的内容是动态生成的(后台语言字符串拼接出来的),里面包含的是 JSON With Padding(回调函数+数据),之前写的那个函数就因为新加载进来的这段动态生成的 js 而执行,也就是获取到了他要获取的数据。

重复一下,在一个页面中,a.html这样写,得到 UserId 为 1823 的信息:

a.html

...
src="http://server2.example.com/RetrieveUser?UserId=1823&callback=parseResponse">
...

请求这个地址会得到一个可以执行的 JavaScript。比如会得到:

  parseResponse({"Name": "小明", "Id" : 1823, "Rank": 7})

这样,a.html里面的 parseResponse() 这个函数就能执行并且得到数据了。

等等,jQuery到底做了什么:

jQuery 让 JSONP 的使用API和Ajax的一模一样:

$.ajax({
  method: 'jsonp',
  url: 'http://server2.example.com/RetrieveUser?UserId=1823',
  success: function(data) {
    console.log(data)
  } 
})

之所以可以这样是因为 jQuery 在背后倾注了心血,它会在执行的时候生成函数替换callback=dosomthing ,然后获取到数据之后销毁掉这个函数,起到一个临时的代理器作用,这样就拿到了数据。

JSONP 的后话

JSONP的这种实现方式不受同源策略的影响,兼容性也很好;但是它之支持 GET 方式的清楚,只支持 HTTP 请求这种特殊的情况,对于两个不同域之间两个页面的互相调用也是无能为力。

2. CORS

XMLHttpRequest 的同源策略看起来是如此的变态,即使是同一个公司的产品,也不可能完全在同一个域上面。还好,网络设计者在设计的时候考略到了这一点,可以在服务器端进行一些定义,允许部分网络访问。

CORS 的全称是 Cross-Origin Resource Sharing,即跨域资源共享。他的原理就是使用自定义的 HTTP 头部,让服务器与浏览器进行沟通,主要是通过设置响应头的 Access-Control-Allow-Origin 来达到目的的。这样,XMLHttpRequest 就能跨域了。

值得注意的是,正常情况下的 XMLHttpRequest 是只发送一次请求的,但是跨域问题下很可能是会发送两次的请求(预发送)。

更加详细的内容可以参见:

https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS

CORS 的后话:

相比之下,CORS 就支持所有类型的 HTTP 请求了,但是在兼容上面,往往一些老的浏览器并不支持 CORS。

Desktop:

浏览器 版本
Chrome 4
Firefox (Gecko) 3.5
Internet Explorer 8 (via XDomainReques) 10
Opera 12
Safari 4

Mobile:

设备 版本
Android 2.1
Chrome for Android yes
Firefox Mobile (Gecko) yes
IE Mobile ?
Opera Mobile 12
Safari Mobile 3.2

3. window.name

window.name 在一个窗口(标签)的生命周期之内是共享的,利用这点就可以传输一些数据。

除此之外,结合 iframe 还能实现更加强大的功能:

需要3个文件: a/proxy/b

a.html

<script type="text/javascript">
    var state = 0, 
    iframe = document.createElement('iframe'),
    loadfn = function() {
        if (state === 1) {
            var data = iframe.contentWindow.name;    // 读取数据
            alert(data);    //弹出'I was there!'
        } else if (state === 0) {
            state = 1;
            iframe.contentWindow.location = "http://a.com/proxy.html";    // 设置的代理文件
        }  
    };
    iframe.src = 'http://b.com/b.html';
    if (iframe.attachEvent) {
        iframe.attachEvent('onload', loadfn);
    } else {
        iframe.onload  = loadfn;
    }
    document.body.appendChild(iframe);
</script>
b.html

<script type="text/javascript">
    window.name = 'I was there!';    // 这里是要传输的数据,大小一般为2M,IE和firefox下可以大至32M左右
                                     // 数据格式可以自定义,如json、字符串
</script>

proxy 是一个代理文件,空的就可以,需要和 a 在同一域下

4. document.domain

在不同的子域 + iframe交互的时候,获取到另外一个 iframe 的 window对象是没有问题的,但是获取到的这个window的方法和属性大多数都是不能使用的。

这种现象可以借助document.domain 来解决。

example.com

<iframe id='i' src="1.example.com" onload="do()"></iframe>
<script>
  document.domain = 'example.com';
  document.getElementById("i").contentWindow;
</script>
1.example.com

<script>
  document.domain = 'example.com';  
</script>

这样,就可以解决问题了。值得注意的是:document.domain 的设置是有限制的,只能设置为页面本身或者更高一级的域名。

document.domain的后话:

利用这种方法是极其方便的,但是如果一个网站被攻击之后另外一个网站很可能会引起安全漏洞。

5.location.hash

这种方法可以把数据的变化显示在 url 的 hash 里面。但是由于 chrome 和 IE 不允许修改parent.location.hash 的值,所以需要再加一层。

a.html 和 b.html 进行数据交换。

a.html

function startRequest(){
    var ifr = document.createElement('iframe');
    ifr.style.display = 'none';
    ifr.src = 'http://2.com/b.html#paramdo';
    document.body.appendChild(ifr);
}

function checkHash() {
    try {
        var data = location.hash ? location.hash.substring(1) : '';
        if (console.log) {
            console.log('Now the data is '+data);
        }
    } catch(e) {};
}
setInterval(checkHash, 2000);
b.html

//模拟一个简单的参数处理操作
switch(location.hash){
    case '#paramdo':
        callBack();
        break;
    case '#paramset':
        //do something……
        break;
}

function callBack(){
    try {
        parent.location.hash = 'somedata';
    } catch (e) {
        // ie、chrome的安全机制无法修改parent.location.hash,
        // 所以要利用一个中间域下的代理iframe
        var ifrproxy = document.createElement('iframe');
        ifrproxy.style.display = 'none';
        ifrproxy.src = 'http://3.com/c.html#somedata';    // 注意该文件在"a.com"域下
        document.body.appendChild(ifrproxy);
    }
}
c.html

//因为parent.parent和自身属于同一个域,所以可以改变其location.hash的值
parent.parent.location.hash = self.location.hash.substring(1);

这样,利用中间的 c 层就可以用 hash 达到 a 与 b 的交互了。

6.window.postMessage()

这个方法是 HTML5 的一个新特性,可以用来向其他所有的window对象发送消息。需要注意的是我们必须要保证所有的脚本执行完才发送MessageEvent,如果在函数执行的过程中调用了他,就会让后面的函数超时无法执行。

https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage

参考资料

http://www.cnblogs.com/rainman/archive/2011/02/20/1959325.html

http://www.cnblogs.com/rainman/archive/2011/02/21/1960044.html


捐赠

写文不易,赠我一杯咖啡增强一下感情可好?

alipay

@rccoder rccoder changed the title from # 浅谈浏览器端JavaScript跨域解决方法 to 浅谈浏览器端JavaScript跨域解决方法 Feb 29, 2016

@rccoder rccoder added the JavaScript label Feb 29, 2016

@Mrcooder

This comment has been minimized.

Mrcooder commented Feb 29, 2016

非常不错的一篇文章 给你点赞了

@ystarlongzi

This comment has been minimized.

ystarlongzi commented Feb 29, 2016

+1 总结的好

@henryzp

This comment has been minimized.

henryzp commented Feb 29, 2016

点赞

@riskers

This comment has been minimized.

riskers commented Mar 3, 2016

订阅一下

@ystarlongzi

This comment has been minimized.

ystarlongzi commented Mar 3, 2016

其实postMessage,严格来说,应该属于跨窗口通信

@neilwong2012

This comment has been minimized.

neilwong2012 commented Mar 3, 2016

CRFS 应该是 CSRF

@rccoder

This comment has been minimized.

Owner

rccoder commented Mar 3, 2016

@404io 谢谢指正,已经修改

@hiyangguo

This comment has been minimized.

hiyangguo commented Mar 4, 2016

棒棒的

@rccoder rccoder closed this Mar 4, 2016

@rccoder rccoder reopened this Mar 4, 2016

@Lojze

This comment has been minimized.

Lojze commented Mar 8, 2016

非常棒!

@Monine Monine referenced this issue Mar 10, 2016

Closed

Gulp 实践 #4

@mishe

This comment has been minimized.

mishe commented Jul 1, 2016

虽然原文不是我写的,但转的文章最好还是加上(转)会比较好

@rccoder

This comment has been minimized.

Owner

rccoder commented Jul 1, 2016

@mishe 问一下原文是在哪里?

@mishe

This comment has been minimized.

mishe commented Jul 2, 2016

@lscho

This comment has been minimized.

lscho commented Jul 2, 2016

@mishe 这。。。虽然有相同的地方,但是也不能叫原文吧

@rccoder

This comment has been minimized.

Owner

rccoder commented Jul 2, 2016

@mishe 是的,参考资料我早已给出相关链接,你给的链接并不是原文链接

@mishe

This comment has been minimized.

mishe commented Jul 4, 2016

抱歉,没注意到参考链接。

@dryyun

This comment has been minimized.

dryyun commented Aug 16, 2016

mark

@MuYunyun

This comment has been minimized.

MuYunyun commented Feb 3, 2018

关于 JSONP 的有一点我想请教下博主您,比如我后台(node.js)拼接了如下代码返回给前台

const obj = {
   "text": 'jsonp',
}
const callback = req.url.split('callback=')[1]
const json = JSON.stringify(obj)
const build = callback + `(${json})`
res.end(build)

前端代码:

function handleResponse(res) {
    console.log(res) // {text: "jsonp"}
}
const script = document.createElement('script')
script.src = 'http://127.0.0.1:3000?callback=handleResponse'
document.head.appendChild(script)

正如您说的,是以拼接JSON方式(拼接字符串)返回给前台,但是是在什么时候将JSON转为函数的呢?望博主赐教

@rccoder

This comment has been minimized.

Owner

rccoder commented Feb 3, 2018

@MuYunyun 以 script 标签的方式引入了一段 JS (这段 JS 到底是怎么产生的不重要,能直接执行就好),这段 JS 就是可执行的。

类似于通过 createElement 加了 script,引用了一段 JS

callback({text: 'jsonp'})

引入的时候不就是可执行的么

@MuYunyun

This comment has been minimized.

MuYunyun commented Feb 3, 2018

😄 确实纠结在 JSON 数据是如何转换成 JS 代码的这个点上了,我姑且认为浏览器通过 JSONP 格式跨域得到的结果会这样处理吧

@bupthly

This comment has been minimized.

bupthly commented Feb 3, 2018

@MuYunyun

This comment has been minimized.

MuYunyun commented Feb 4, 2018

@bupthly 我对您这个解释做了个实验,在 response-headers 设置 Content-Type:text/plain 或者不设置 Contenty-Type ,浏览器照样会将返回内容解析为 js 脚本~

@rccoder

This comment has been minimized.

Owner

rccoder commented Feb 4, 2018

@MuYunyun 和 content-type 关系不大,你有没有好奇过为啥在 HTML 里通过

<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>

就能引入一个 $ 的变量呢。。。

核心是 通过 script 引入

@bupthly

This comment has been minimized.

bupthly commented Feb 4, 2018

@bupthly

This comment has been minimized.

bupthly commented Feb 4, 2018

@rccoder

This comment has been minimized.

Owner

rccoder commented Feb 4, 2018

@bupthly  只是说不大,还是有一定的关系的,可以参考:https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types

Note that text/plain does not mean any kind of textual data. If they expect a specific kind of textual data, they will likely not consider it a match. Specifically if they download a text/plain file from a element declaring a CSS files, they will not recognize it as a valid CSS files if presented with text/plain. The CSS mime type text/css must be used.

@MuYunyun

This comment has been minimized.

MuYunyun commented Feb 4, 2018

我总结下楼主您的意思是说通过 script 标签引用 jq 脚本将 $ 挂到全局上,然后类比 JSONP 也类似通过 script 标签引用从而将返回的callback(Obj)挂到全局。至于浏览器如何将callback(Obj)这个 JSON
对象转成 JS 的我们可以不关心,把它当成一个黑盒。

@rccoder

This comment has been minimized.

Owner

rccoder commented Feb 4, 2018

@MuYunyun 不是... 前半部分对,后半部分不太t对...

@MuYunyun

This comment has been minimized.

MuYunyun commented Feb 4, 2018

我在红宝书587页,看到它介绍JSONP的优点有提到一句它的优点在于能够直接访问响应文本,里面涉及到的知识点它没有深讲了,愿闻其详 😄

@lscho

This comment has been minimized.

lscho commented Feb 4, 2018

不要被 JSONP 这个名字误导啊,他只是一个名字而已。script 标签是开放性的,没有域名的限制,也就是说通过 script 标签引入的任何脚本,都能被执行。所以可以利用这个特性来绕过异步请求同源的限制。

思路很简单,就是你先在本地创建一个回调函数,姑且称为 callback ,然后利用 script 标签不管引入来自哪个域名的脚本,都可以调用 callback。而数据就通过给 callback 传递参数(一般都是json,当然字符串也可以),来达到跨域数据交换。所以 jsonp 要求后端返回的数据是 callback(Object) 这种类型的。

@MuYunyun

This comment has been minimized.

MuYunyun commented Feb 4, 2018

也写了篇跨域小结

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment