时光再倒退50年,那时候的互联网只有纯文本和纯文件,在1989年,英国科学家蒂姆·伯纳斯-李发明了万维网,自此以后,互联网的世界变得丰富多彩起来,一个名为“ Web浏览器”的软件也开始流行,再之后,有了cookie,有了DOM,有了Javascript...,浏览器各页面之间可以交互了,因此,在浏览器的范围内,我们需要一种安全的交互方式。
作为对此的解决方案,Netscape工程师决定使用称为同域策略(SOP)的规则来管理这些资源之间的关系...
什么是同源策略
同源策略限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互,以隔离潜在恶意文件、减少可能被攻击的媒介的重要安全机制。
同源的定义
如果两个URL的protocol
(协议)、port
(端口)、host
(域名)都相同的话,说明这两个URL是同源的
如:url -> http://happy.com/home
URL | 是否同源 | 原因 |
---|---|---|
http://happy.com/mine | 是 | protocol、port、host都相同,只有路径不同 |
https://happy.com/home | 否 | protocol不同 |
http://happy.com:81/home | 否 | port不同 |
http://nothappy.com/home | 否 | host不同 |
同源策略的规则
-
每个站点都有自己的资源,例如Cookie,DOM和Javascript命名空间
-
每个页面的来源都来自其URL【通常是架构/协议(schema/protocol),域(host)和端口(port)】
-
脚本在加载源的上下文中运行。指的不是从何它的加载来源的地方,而是最终执行它的地方
-
媒体和图像等许多资源都是被动资源。他们无法在加载的上下文中访问对象和资源
根据这些规则,我们可以假设有一个
originA
和一个originB
,对于originA
来讲:
- 可以从
originB
加载脚本,但是可以在originB
的上下文中使用 - 无法获取脚本的原始内容和源代码
- 可以从
originB
加载CSS - 无法获取
originB
中CSS文件的原始文本 - 可以通过iframe从
originB
加载页面 - 无法获取从
originB
加载的iframe的DOM - 可以从
originB
加载图像 - 无法获取
originB
加载的图像的位(bits) - 可以播放来自
originB
的视频 - 无法捕获从
originB
加载的视频的图像
如果没有同源策略会怎么样
设想一个场景,当你登录了bilibli.com
后,你的用户信息存在Cookie/LocalStorage中,这时候你不小心访问了一个钓鱼网站,由于没有同源策略的限制,钓鱼网站也能访问到你的用户信息...
第二天早上,你又啪一下打开bilibili.com
,发现你的币都没了...
同源策略限制的的三个层面
同源策略的限制主要表现在DOM、Web数据和网络三个层面。
DOM层面
限制了来自不同源的javascript脚本对当前dom对象的读写操作,举个🌰
首先,我们在掘金的首页点开了热门文章的第一篇,他在新的窗口打开了
这时候我们发现
掘金首页的地址为:https://juejin.cn/
打开的专栏地址为:https://juejin.cn/post/6898896417360707591
符合同源的定义,也就是说这两个页面的关系是同源
当我们在专栏文章的页面输入以下代码
// opener 属性是一个可读可写的属性,可返回对创建该窗口的 Window 对象的引用
let pdom = opener.document;
pdom.body.style.display = 'none';
我们会发现掘金首页被隐藏了!!
也就是说,同源页面间可以互相操作对方的dom
数据层面
由于同源策略的限制,我们不能从不同源的站点访问当前源站点的Cookie、LocalStorage 和 IndexDB等数据
具体的试验方法也如上,通过window.opener这个属性
网络层面
这个限制指的是,同源策略限制了通过XMLHttpRequest等方式将站点的数据发送给不同源的站点
如何突破限制(跨域)
数据层面
Document.domain
严格遵循同源策略
的规则可能会导致一些问题,比如我们有login.example.com,home.example.com这两个网页,他们的一级域名相同,但二级域名不同,我们如何让他们之间能共享cookie??
在这样的情况下,我们可以稍微放松一下同源策略
,在两个网页中设置相同的document.domain
,扩展域限制,以允许所有内容扩展到基本域,从而实现共享cookie
document.domain = 'example.com';
需要注意的点:
- 这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexDB 无法通过这种方法,规避同源政策
- document.domain设置的值必须为其当前域或其当前域的父域
或通过服务器指来定Cookie的所属域名
Set-Cookie:key=value; domain=.example.com; path=/
DOM层面
iframe是一个HTML内联框架元素,通过它,我们可以在一个html页面中嵌入其他的html页面
对于两个不同源页面,无法获取对方的dom来讲,iframe窗口和window.open是一个典型
举个🌰:
我们在code.html
中通过iframe
插入了new.html
,这时候尝试在new.html
中获取code.html
中的DOM节点
结果当然是...不可以,
那么如何将不可以变成可以!!
- 对于部分同源的网站,我们可以采用
document.cookie
的方式 - 对于完全不同源的网站
window.postmessage
window.postMessage() 方法提供了一种受控机制来规避同源限制,安全地实现跨源通信
otherWindow.postMessage(message, targetOrigin, [transfer]);
举个🌰,父窗口与子iframe通信:
父窗口发送:
// 若iframe的id为otherWindow
var iframeWindow = document.getElementById('otherWindow')
//url为iframe的地址
iframeWindow.postMessage('1111', url)
子ifame接收:
window.addEventListener("message", function( event ) {
//你想要的操作
....
}
子iframe发送:
//url为父窗口的地址
window.opener.postMessage('222', url);
父窗口接收消息与子iframe接收消息方式相同,就不列举啦~
canvas图片
网络层面
JSONP
jsonp是json的一种“使用模式”,我们可以简单的理解为带有callback的json,他的思想是利用<script>
的src
属性实现跨域,向服务器请求json数据,服务器收到请求后,在一个指定的callback中将数据返回
// 理想中的返回数据
['a', 'b'];
// 现实中返回的数据
callbackFunc(['a', 'b'])
创建一个简单的jsonp:
function jsonp(req) {
// 动态创建一个script标签
var script = document.createElement('script');
// 以拼接的形式创建url
var url = req.url + '?callback=' + req.callback.name;
script.src = url;
// 将这个script标签添加到head中
document.getElementsByTagName('head')[0].appendChild(script);
}
使用方法
function cb(res) {
console.log(res);
}
jsonp({
url: 'http://XXX.com',
callback: cb
})
Nginx反向代理
nginx反向代理就是通过代理服务器来接收Internet
上的请求,然后将请求转发给内部服务器,并且将从内部服务器得到的响应资源又返回给客户端
通过nginx解决跨域问题的核心就是修改配置,例如nginx的端口号为8080,内部服务器的端口为9090(localhost:8080 ->localhost:9090)
server {
listen 8080;
server_name localhost;
location / {
proxy_pass http://localhost:9090; #反向代理
add_header Access-Control-Allow-Origin '*'; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
}
}
CORS
CORS需要浏览器以及服务器同时支持,目前,所有浏览器都支持CORS,所以可以说CORS的的关键在于服务器
如果后端设置了CORS,浏览器会将CORS请求分为两类:简单请求以及复杂请求
-
简单请求
简单请求需要满足以下条件
- 请求的方法为:HEAD、GET、POST
- http header的content-type只限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain
- 请求中没有自定义的http头部(x-token之类)
- 请求中的任意
XMLHttpRequestUpload
对象均没有注册任何事件监听器;XMLHttpRequestUpload
对象可以使用XMLHttpRequest.upload
属性访问
// 对于简单请求,服务器只需要这样设置 app.use((req, res) => { res.setHeader('Access-Control-Allow-Origin', 'xxx') });
-
复杂请求(带预检的跨域请求)
对于复杂请求,会在正式通信之前,增加一次http查询请求(预检请求),也就是option请求,通过该请求来了解服务端是否允许跨域请求
app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', 'XXX'); // 允许返回的头 res.setHeader('Access-Control-Allow-Headers', 'XXX'); // 允许使用的方法 res.setHeader('Access-Control-Allow-Methods', 'XXX'); // 预检的存活时间 res.setHeader('Access-Control-Max-Age', 6); // 当method为OPTIONS时,不做任何处理 if(req.method === "OPTIONS") { res.end(); } });
对于本地开发时,cors请求跨域,也可以使用google插件: