细说浏览器的同源策略

966 阅读7分钟

时光再倒退50年,那时候的互联网只有纯文本和纯文件,在1989年,英国科学家蒂姆·伯纳斯-李发明了万维网,自此以后,互联网的世界变得丰富多彩起来,一个名为“ Web浏览器”的软件也开始流行,再之后,有了cookie,有了DOM,有了Javascript...,浏览器各页面之间可以交互了,因此,在浏览器的范围内,我们需要一种安全的交互方式。

作为对此的解决方案,Netscape工程师决定使用称为同域策略(SOP)的规则来管理这些资源之间的关系...

什么是同源策略

同源策略限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互,以隔离潜在恶意文件、减少可能被攻击的媒介的重要安全机制。

同源的定义

如果两个URL的protocol(协议)、port(端口)、host(域名)都相同的话,说明这两个URL是同源的

如:url -> http://happy.com/home

URL是否同源原因
http://happy.com/mineprotocol、port、host都相同,只有路径不同
https://happy.com/homeprotocol不同
http://happy.com:81/homeport不同
http://nothappy.com/homehost不同

同源策略的规则

  • 每个站点都有自己的资源,例如Cookie,DOM和Javascript命名空间

  • 每个页面的来源都来自其URL【通常是架构/协议(schema/protocol),域(host)和端口(port)】

  • 脚本在加载源的上下文中运行。指的不是从何它的加载来源的地方,而是最终执行它的地方

  • 媒体和图像等许多资源都是被动资源。他们无法在加载的上下文中访问对象和资源

    根据这些规则,我们可以假设有一个originA和一个originB,对于originA来讲:

  1. 可以从originB加载脚本,但是可以在originB的上下文中使用
  2. 无法获取脚本的原始内容和源代码
  3. 可以从originB加载CSS
  4. 无法获取originB中CSS文件的原始文本
  5. 可以通过iframe从originB加载页面
  6. 无法获取从originB加载的iframe的DOM
  7. 可以从originB加载图像
  8. 无法获取originB加载的图像的位(bits)
  9. 可以播放来自originB的视频
  10. 无法捕获从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.comhome.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请求分为两类:简单请求以及复杂请求

  • 简单请求

    简单请求需要满足以下条件

    1. 请求的方法为:HEAD、GET、POST
    2. http header的content-type只限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain
    3. 请求中没有自定义的http头部(x-token之类)
    4. 请求中的任意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插件:

img

参考链接