跨域是个大问题

最近工作中经常需要在前端调用接口,则时常会碰到跨域的问题,积累一些思考,总结一下。另外也深刻感觉到,如果一个东西,不自己实践一下,仅看书是很难理解透彻的。

首先跨域就要区分什么是域。这个很容易理解,不再赘述,无非就是协议、域名、端口等都要一致。

而跨域就是在不同的域之间数据的通信。而且需要注意的是,跨域只发生在前端,后端是没有跨域这一说的。

比如a.com 中,想要获取b.com的内容,这个时候通常就是a.com中通过js发起一个http请求(也就是Ajax),去请求b.com的数据。 这样:

1
2
3
4
5
6
7
8
<script>
var xml = new XMLHttpRequest();
 xml.open('get','http://xxx.com/api',true)
 xml.send(null)
 xml.onreadystatechange = function(){
    alert('change')
 }
</script>

但是打开页面,这一段js代码执行之后,就会发现报了这样一个错:

1
XMLHttpRequest cannot load http://xxx.com/api. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access.

大致是说,http://xxx.com/api 这里面的响应头(response header)里没有指定 Access-Control-Allow-Origin,因此我们的请求头 origin:null 不允许访问这个地址。

这是什么问题?这还要从浏览器的同源策略说起,浏览器为了安全起见,不允许去请求与当前页面不在同一个域的资源!所以当你请求其他域的资源,浏览器会阻断你,除非在响应头里指明了Access-Control-Allow-Origin,并且在这个Access-Control-Allow-Origin中有你当前所在的域,才会让你访问!

这就是为什么前面说的,跨域只发生在前端,后端没有跨域这一说法,正是浏览器不让我们访问! 但是有时候,正常的跨域请求又是很必须的,怎么办?

就按照上面说说,在Access-Control-Allow-Origin指明你的域,如Access-Control-Allow-Origin:a.com 或者Access-Control-Allow-Origin : * 。这也就是通常所说的,CROS跨域资源共享。使用这种方法,需要对方服务器允许我们跨域,也就是说,要么对方服务是开放的,要么就得提前与对方协商。麻烦是麻烦,但是增加安全性。

举个例子: a.com/index.html中需要去请求b.com/api.php的页面,想要跨域成功,就需要api.php中,设定响应头:

1
2
<?php header('Access-Control-Allow-Origin:a.com');
//或者header('Access-Control-Allow-Origin: *');

这样就能跨域成功,请求不会被浏览器阻断。

这是一种方式,但是还有另外更常用的【奇技淫巧】供我们使用。也就是jsonp。jsonp的原理:

虽然一般情况下不能请求其他域的数据,但是却有两个例外:script标签,和img标签,这两个标签可以随意地引用其他页面的数据也不会被阻断。例如:

1
<script src="http://c.com/index.js"><script src="http://d.com/test.js"></script><img src="http://xxx.com/a.png" alt="">

那我们就可将需要请求的数据,交给这两个标签来请求就好了! <script src="http://c.com/api.php"></script> 这样不就相当于给其他域发了一个请求吗?对的就是这样。 但是有两个问题: 只能发送get请求,对于需要通过post传递的数据,就无能为力了。 另外,如果此时打开一看,就发现还是报错了 Uncaught SyntaxError: Unexpected token : //也可能是报其他的错,一般都是Unexpected token xxx 但是如果通过抓包能看到,请求正常发出,并且服务器也返回了数据。但是!我们这里是通过script标签引入的,也就是说引入的是一堆数据,而我们知道script需要引入的是js文件啊,当然出错了! 那如何解决呢?这时很多网上的断章取义文章都会给你说,指定一个回调函数,处理获取到的数据就行了,例如:

1
2
3
4
5
6
function test(response){
   console.log(response)
}
<script src="http://c.com/api.php?callback=test"></script>

//在后面增加callback参数,加载数据之后,就会调用test函数来处理获取到的数据。

如果真的是这样的话,那就好了。但是想一下,如果http://c.com/api.php?callback=test返回的是 ‘helloworld’,也就相当于:

1
<script>hello world</script>

那你能调用个鬼啊!

也就是说,这里我们期望的数据是这样的:

1
<script src="test(response)"></script>

只有如此才能调用我们的test()函数。jsonp的重点就在这里,服务器返回的是一串以我们callback参数为函数名包裹着的参数,这时才能调用我们的回调函数。这才是重点,网上很多一知半解的人写的文章根本都没讲到这一点。

也就是说,如果服务器不特别为你返回这种形式的响应,那所谓的jsonp中的回调函数还是没法调用的。

如何返回的,还是举一个服务器端的例子:

1
2
3
4
5
6
7
8
//c.com/api.php
$callback = $_GET['callback'];

$result = [];
$result['msg'] = 'hello';
$result = json_encode($result);

echo "$callback($result)";

首先先获取到 传过来的 callback 参数,当然不一定要叫callback 。然后再把json化后的result数组放入$callback($result)。于是乎运行下来:响应的值就是:

test({"msg":"hello"}) 也就相当于:<script src="test(%7B" msg></script>。直到这里,才能调用我们我们预定义好的回调函数test。

也就是说,如果服务器不为你特别返回这种形式的响应,那所谓jsonp还是不能完成跨域的。

上面说到,img标签也能跨域请求,但是script标签相比,它不能操作获取到的响应数据,所以较jsonp相比,更加有限。

总结一下:

所谓的跨域方法,并非就能突破浏览器的同源策略限制,如果对方服务器不支持,还是没法跨域。 跨域发生在前端,因为有浏览器的同源策略限制,才会有跨域这种说法。后端请求数据的话,则无限制,也就说说,如果前端无法跨域获取数据,可以考虑在后端获取数据返回给前端使用。