开发中涉及到一些跨域问题,从而对同源策略有更深的理解,同源策略是保证Web浏览器安全中最基本的安全功能,无论是前端还是后端,如果对同源策略不了解很容易导致一些开发上的难题。

同源策略

对Javascript来说,满足同源策略可以简单理解为下面的三项必须完全相同:

  • 协议相同
  • 域名相同
  • 端口相同

也就是说,只要两个URL中的协议、域名或端口存在任何一项不同,则被浏览器视为是不同源的。
举个例子,下列与http://test.example.com/page1/index.html的同源情况

http://test.example.com/page2/index.html # 同源
https://test.example.com/page1/index.html # 不同源,协议不同
http://demo.example.com/page1/index.html # 不同源,域名不同
http://test.example.com:3000/page1/index.html # 不同源,端口不同

注意:http://127.0.0.1:8080/index.htmlhttp://localhost:8080/index.html 当然也是不同源的

HTML中的<link><img><script><iframe>等标签都可以跨域加载资源,它们实际上是发送了一次GET请求,而在Javascript中,浏览器不允许脚本行为的跨域,也就是由XMLHttpRequest发起HTTP的请求会受到同源策略的限制。所以当需要由AJAX跨域加载资源时,就会涉及到跨域的处理。

跨域的处理通常有以下两种常用的解决方案。

CORS跨域

CORS跨域需要后端的解决,其主要原理是由服务端来授权控制谁可以跨域获取资源,这是一种比较安全的解决方案。

Access-Control-Allow-Origin

服务端返回的响应头应该在Access-Control-Allow-Origin中设置允许接受请求的域,例如这样设置

// 表示允许来自所有站点的跨域请求
Access-Control-Allow-Origin: *

// 表示仅允许来自http://localhost:8000的跨域请求
Access-Control-Allow-Origin: http://localhost:8000

OPTIONS预请求

当一个HTTP请求不是一个简单请求时,浏览器有一个预请求的过程,也就是发送一个OPTIONS请求,浏览器会预先发送一个OPTIOMS请求给目的站点,去探测目的站点是否允许来自这个源的站点跨域,这个过程是浏览器自行判断并发起的,不需要我们来写,除了GET请求、HEAD请求、没有传参的POST请求和以表单提交方式发出的POST请求外,其它的任何HTTP请求都会发出预请求去探测是否可以继续。

OPTION请求
POST请求

Access-Control-Allow-Credentials

如果是跨域并携带cookie的通信则要注意了,当XMLHttpRequest对象实例中withCredentials属性为true的情况下,服务端还必须设置Access-Control-Allow-Credentials:true来确定是否接受cookie,如果没有设置这个值,尽管客户端发送了cookie值过来,服务端也会忽略,并且请求失败。

需要特别注意的是,如果此时响应头中Access-Control-Allow-Origin的值是*,此时请求还是会失败的,因为在带有cookie的HTTP请求中,Access-Control-Allow-Origin的值不能用*号通配符,只能设置具体的域名。否则Chrome会报如下错误:

因此只有对于一个不带有credentials的跨域请求,Access-Control-Allow-Origin才可以指定为*,表示允许来自所有域的请求。

Access-Control-Max-Age

响应头还可以设置Access-Control-Max-Age来告诉客户端这一次预请求的有效期,在有效期内再次跨域访问,浏览器不会预先发送预请求直到时间失效

Access-Control-Max-Age: 864000  # 单位是秒,表示10天内,浏览器对于该域的跨域请求,不需要再发送预请求

Access-Control-Allow-Headers

在会发出预请求的情况下必须配置,指明可以自定义的请求头

Access-Control-Allow-Headers: Content-Type

如果POST请求需要传JSON格式的参数,前端设置了'Content-Type', 'application/json',则服务端必须设置Access-Control-Allow-Headers,否则也不成功,Chrome会报下面的错:

Access-Control-Allow-Methods

表示允许的请求方式,可以配置多个值

Access-Control-Allow-Methods: POST, GET, OPTION, PUT, DELETE

CORS方式可以灵活可控地解决跨域问题,并且支持所有类型的HTTP请求,但是它存在一定的局限性,这种方式的兼容性差,只支持IE9+,不过很适合用于移动端。

  • 前端代码:
var xhr = new XMLHttpRequest();
var postData = {
    name: 'lonica',
    email: 'test@126.com'
};
xhr.open('POST', 'http://127.0.0.1:3001/', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.withCredentials = true;
xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
        console.log(JSON.parse(xhr.responseText));
    }
};
xhr.send(JSON.stringify(postData));
  • 服务端代码:
var http = require('http');

http.createServer(function(req, res) {
    var data = {
        name: 'lonica'
    };
    res.writeHead(200, {
        'Content-Type' : 'text/plain',
        'Access-Control-Allow-Origin' : 'http://localhost:8000',
        'Access-Control-Allow-Methods' : 'GET, POST, OPTIONS, PUT, DELETE, HEAD',
        'Access-Control-Allow-Credentials' : true,
        'Access-Control-Allow-Headers' : 'Content-Type',
        'Access-Control-Max-Age' : '3600'
    });
    res.end(JSON.stringify(data));
}).listen(3001);

JSONP跨域

JSONPJSON with Padding的简称,这种方式的兼容性好,但是只能处理GET请求的跨域,使用的时候需要特别注意。

我们都知道,浏览器不允许使用XMLHttpRequest脚本去获取不同域的数据,但是可以通过<script><link><img>等标签来发起GET请求来获取数据,JSONP就是利用了这个原理,所以也不难理解JSONP为什么只能支持GET请求了。

JSONP的原理:利用<script>标签去获取一段javascript代码,这段javascript代码的内容(由服务端拼接)包含了一个方法的调用,调用方法时所传入的参数就是前端要拿到的数据,前端只需要在这个方法的实现里面处理传入的数据即可,JSONP需要前后端一起配合,确定方法名由前端传参给后端,并由后端拼接成调用方法的语句再返回给前端。

  • 前端代码
<script type="text/javascript">
    function callback(data) {
        // 获取到的数据
        console.log(data);
    }

    var ele = document.createElement('script');
    ele.type = 'text/javascript';
    ele.src = 'http://127.0.0.1:3000?callback=callback';
    document.body.appendChild(ele);

</script>
  • 后端代码实现:
var http = require('http');
var url = require('url');

http.createServer(function(req, res) {
    var params = url.parse(req.url, true).query;
    var func = params.callback;
    var data = {
        tags: ['jsonp', 'javascript']
    };
    res.writeHead(200, {'Content-Type' : 'text/plain'});
    res.end(func + '(' + JSON.stringify(data) +')');
}).listen(3000);