同源策略
什么是同源策略:同 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
它们的域名是不同的,所以它们是不同源的,所以这个请求是跨域的请求,而浏览器是不允许跨域请求的。那么浏览器为什么会提出同源策略,这是出于安全的考虑。同源策略包括两种:
DOM
同源策略:禁止对不同源页面DOM
进行操作,这里主要场景是iframe
跨域的情况,不同域名的iframe
是限制互相访问的XMLHttpRequest
同源策略:禁止使用xhr
对象向不同源的服务器地址发起HTTP
请求
在这里我们只关注 ajax
跨域,我们来看一下如果没有同源策略会有什么危害:
- 用户登录了自己的银行页面 http://mybank.com,http://mybank.com 向用户的
cookie
中添加用户标识 - 用户浏览了恶意页面 http://evil.com,执行了页面中的恶意
AJAX
请求代码 - http://evil.com 向 http://mybank.com 发起
AJAX HTTP
请求,请求会默认把 http://mybank.com 对应cookie
也同时发送过去 - 银行页面从发送的
cookie
中提取用户标识,验证用户无误,response
中返回请求数据,此时数据就泄露了 - 而且由于
Ajax
在后台执行,用户无法感知这一过程
所以有了同源策略我们才能够安全的上网,但是很多时候我们还是有跨域的需求的,并且现在随着前后端分离的流行,前端与后端的不同源是很有可能,所以迫切需要解决这个问题,下面就介绍两种跨域的方法。
JSONP
同源策略提升了 Web
前端的安全性,但牺牲了 Web
拓展上的灵活性。设想若把 html
、js
、css
、flash
、image
等文件全部布置在一台服务器上,小网站这样凑活还行,大中网站如果这样做服务器根本受不了的。所以,现代浏览器在安全性和可用性之间选择了一个平衡点。在遵循同源策略的基础上,选择性地为同源策略开放了后门, 例如 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端口...');
})
结果如下
我来理一下上面的步骤
首先创建了一个函数
getData
,该函数的作用是用来处理数据的,接收的那个参数就是数据function getData(data) {
console.log(data);
}创建一个
script
标签,并且设置了请求的地址,以及将处理数据函数的名称即getData
作为参数传入script.src = 'http://127.0.0.1:4000?callback=getData';
服务器接到请求,将要返回的数据作为传入的回调函数的参数传入
res.end(`${qs.callback}("${data}")`); // 即getData(data)
script
标签得到服务器返回的数据,即getData("Hello World")
,然后执行,这样数据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
请求分为两类,分别为简单请求和非简单请求。如果满足以下两类请求,就是简单请求
- 请求方法是以下三种方法之一:
GET
POST
HEAD
- HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type
:只限于三个值application/x-www-form-urlencoded
、multipart/form-data
、text/plain
如果不满足以上的两大条件,那么就是非简单请求,而浏览器对于简单请求和非简单请求的处理是不一样的。
简单请求
在请求中需要附加一个额外的
Origin
头部,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给予响应。例如:Origin: http://127.0.0.1:5501
如果服务器认为这个请求可以接受,就在
Access-Control-Allow-Origin
头部中回发相同的源信息(如果是公共资源,可以回发*
),例如:Access-Control-Allow-Origin:http://127.0.0.1:5501
没有这个头部或者有这个头部但源信息不匹配,浏览器就会驳回请求,注意,请求和响应都不包含
cookie
信息如果需要包含
cookie
信息,ajax
请求需要设置xhr
的属性withCredentials
为true
,服务器需要设置响应头部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
以下的浏览器 - 第一次发送非简单请求时会多一次请求