同源策略

什么是同源策略:同 domain(或IP),同端口,同协议视为同一个域,一个域内的脚本仅仅具有本域内的权限,可以理解为本域脚本只能读写本域内的资源,而无法访问其它域的资源,这种安全限制称为同源策略。我们看几个例子判断两个链接是否是同源

url1 url2 是否同源 原因
http://example.com:8080 http://example.com:80 端口不同
http://example.com:8080 https://example.com:8080 协议不同
http://www.baidu.com http://www.taobao.com 域名不同
http://127.0.0.1:8080 http://localhost:8080 域名与域名对应的IP地址不同源
http://example.com:8000/a.js http://example.com:8000/b.js 协议,域名,端口相同,子域名不同

我们首先启动一个 Node 程序作为服务端,启动在本地的 4000 端口,如下

const http = require('http');

const server = http.createServer();

server.on('request', function(req, res) {
res.end('hello world');
})

server.listen(4000, function() {
console.log('服务启动在4000端口...');
})

现在我们同时在本地的 5501 端口启动一个静态服务器显示网页,在网页中我们发起一个 ajax 请求,向本地的 4000 端口请求数据,即我们刚刚搭的 Node 程序

const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText);
}
}
xhr.onerror = function() {
console.error('出错了')
}
xhr.open('get', 'http://127.0.0.1:4000');
xhr.send(null);

这个时候我们打开控制台,发现给出这样的错误信息

因为网页的地址 http://127.0.0.1:5501 与服务器的地址 http://127.0.0.1:4000 它们的端口是不同的,所以它们是不同源的,所以这个请求是跨域的请求,而浏览器是不允许跨域请求的。那么浏览器为什么会提出同源策略,这是出于安全的考虑。同源策略包括两种:

  1. DOM 同源策略:禁止对不同源页面 DOM 进行操作,这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的
  2. XMLHttpRequest 同源策略:禁止使用 xhr 对象向不同源的服务器地址发起 HTTP 请求

在这里我们只关注 Ajax 跨域,我们来看一下如果没有同源策略会有什么危害:

  1. 用户登录了自己的银行页面 http://mybank.com,http://mybank.com 向用户的 cookie 中添加用户标识
  2. 用户浏览了恶意页面 http://evil.com,执行了页面中的恶意 Ajax 请求代码
  3. http://evil.comhttp://mybank.com 发起 Ajax HTTP 请求,请求会默认把 http://mybank.com 对应 cookie 也同时发送过去
  4. 银行页面从发送的 cookie 中提取用户标识,验证用户无误,response 中返回请求数据,此时数据就泄露了
  5. 而且由于 Ajax 在后台执行,用户无法感知这一过程

所以有了同源策略我们才能够安全的上网,但是很多时候我们还是有跨域的需求的,并且现在随着前后端分离的流行,前端与后端不同源是很有可能,所以迫切需要解决这个问题,下面就介绍两种跨域的方法。

JSONP

同源策略提升了 Web 前端的安全性,但牺牲了 Web 拓展上的灵活性。设想若把 htmljscssflashimage 等文件全部布置在一台服务器上,小网站这样凑活还行,大中网站如果这样做服务器根本受不了的。所以,现代浏览器在安全性和可用性之间选择了一个平衡点。在遵循同源策略的基础上,选择性地为同源策略开放了后门, 例如 img script style 等标签,都允许垮域引用资源,严格说这都是不符合同源要求的。

正是利用 script 标签引用的资源没有同源策略的限制,我们可以通过 script 向服务器发送请求,而服务器返回一个回调,将数据作为参数返回,这一样就可以拿到想要的数据。可能你现在不能明白是什么意思,看下面的代码,我们在页面中添加下面的内容

<script>
function getData(data) {
console.log(data);
}
// 创建一个 script 标签用以请求数据
let scriptElement = document.createElement('script');
// 设置请求的地址 并且设置了处理数据的回调函数名称
scriptElement.src = 'http://127.0.0.1:4000?callback=getData';
// 这一步很重要,不设置的话浏览器会报错 Uncaught SyntaxError: Unexpected identifier
scriptElement.setAttribute('type', 'text/javascript');
// 将这个 script 标签添加到 body 中
document.getElementsByTagName('body')[0].appendChild(scriptElement);
</script>

Node 服务端的代码

const http = require('http');
const querystring = require('querystring');

const server = http.createServer();

server.on('request', function(req, res) {
let qs = querystring.parse(req.url.split('?')[1]); // 解析请求参数

let data = "Hello World"; // 返回的数据

// 一定要在 ${data} 两端加上双引号,否则浏览器得到的是 getData(Hello World)会报错
res.end(`${qs.callback}("${data}")`); // 即getData(data)
})

server.listen(4000, function() {
console.log('服务启动在4000端口...');
})

结果如下

我来理一下上面的步骤

  1. 首先创建了一个函数 getData,该函数的作用是用来处理数据的,接收的那个参数就是数据

    function getData(data) {
    console.log(data);
    }
  2. 创建一个 script 标签,并且设置了请求的地址,以及将处理数据函数的名称即 getData 作为参数传入

    script.src = 'http://127.0.0.1:4000?callback=getData';
  3. 服务器接到请求,将要返回的数据作为传入的回调函数的参数传入

    res.end(`${qs.callback}("${data}")`); // 即getData(data)
  4. script 标签得到服务器返回的数据,即 getData("Hello World")script 得到的数据是可执行的代码,所以会执行 getData() 方法,这样数据 Hello World 就作为参数传递给了 getData()

优点:

  • 使用简便,没有兼容性问题,是目前最流行的跨域方法

缺点:

  • 只支持 GET 请求,并且代码是从其他域得到的,不排除可能有恶意代码,所以使用该方法,服务端必须确保值得信任
  • 要确定 JSONP 请求失败并不容易

CORS

CORS(Cross-origin resource sharing,跨域资源共享) 是一个 W3C 标准,定义了在必须访问跨域资源时,浏览器与服务器应该如何沟通。CORS 背后的基本思想,就是使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10

对于开发者来说,我们只要正常的发出 Ajax 请求即可,与之前没有任何区别,如

const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText);
}
}
xhr.onerror = function() {
console.error('出错了')
}
xhr.open('get', 'http://127.0.0.1:4000');
xhr.send(null);

但是在服务端,我们要设置一下信赖的请求来源,如下

const http = require('http');

const server = http.createServer();

server.on('request', function(req, res) {
// 表示来自 http://127.0.0.1:5501 的请求可以拿到数据
res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5501')
res.end('hello world');
})

server.listen(4000, function() {
console.log('服务启动在4000端口...');
})

除了可以设置请求来源,还可以设置允许的方法,允许的头部等等

响应头 作用
Access-Control-Allow-Origin 设置允许的请求来源,如果允许任意来源,可以设置为 *
Access-Control-Allow-Methods 允许的方法,多个以逗号分隔,如 GET, POST
Access-Control-Allow-Headers 允许的请求头,多个以逗号分隔

浏览器将 CORS 请求分为两类,分别为简单请求和非简单请求。如果满足以下两类请求,就是简单请求

  1. 请求方法是以下三种方法之一:
    • GET
    • POST
    • HEAD
  2. HTTP 的头信息不超出以下几种字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type:只限于三个值 application/x-www-form-urlencodedmultipart/form-datatext/plain

如果不满足以上的两大条件,那么就是非简单请求,而浏览器对于简单请求和非简单请求的处理是不一样的。

简单请求

  1. 在请求中需要附加一个额外的 Origin 头部,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给予响应。例如:Origin: http://127.0.0.1:5501

  2. 如果服务器认为这个请求可以接受,就在 Access-Control-Allow-Origin 头部中回发相同的源信息(如果是公共资源,可以回发 *),例如:Access-Control-Allow-Origin:http://127.0.0.1:5501

  3. 没有这个头部或者有这个头部但源信息不匹配,浏览器就会驳回请求,注意,请求和响应都不包含 cookie 信息

  4. 如果需要包含 cookie 信息,ajax 请求需要设置 xhr 的属性 withCredentialstrue,服务器需要设置响应头部 Access-Control-Allow-Credentials: true

非简单请求

非简单请求可能对服务器数据产生副作用,浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求,

  • Origin:与简单的请求相同
  • Access-Control-Request-Method: 请求自身使用的方法
  • Access-Control-Request-Headers: (可选)自定义的头部信息,多个头部以逗号分隔
Origin: http://127.0.0.1:5501
Access-Control-Request-Method: POST
Access-Control-Request-Headers: NCZ

发送这个请求后,服务器可以决定是否允许这种类型的请求,一旦服务器通过 Preflight 请求,允许该请求之后,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样了。

优点:

  • CORS 通信与同源的 AJAX 通信没有差别,代码完全一样,容易维护
  • 支持所有类型的 HTTP 请求

缺点:

  • 存在兼容性问题,特别是 IE10 以下的浏览器
  • 第一次发送非简单请求时会多一次请求

参考链接