同源限制理解与解决

同源策略是对JavaScript代码能够操作哪些web内容的一条完整的安全限制。

什么是同源

URL由协议、域名、端口和路径组成,如果两个URL的协议、域名和端口相同,则表示他们同源。
下表给出了相对http://store.company.com/dir/page.html同源检测的示例:

URL 结果 原因
http://store.company.com/dir2/other.html 成功 只有路径不同
http://store.company.com/dir/inner/another.html 成功 只有路径不同
https://store.company.com/secure.html 失败 不同协议 ( https和http )
http://store.company.com:81/dir/etc.html 失败 不同端口 ( http:// 80是默认的)
http://news.company.com/dir/other.html 失败 不同域名 ( news和store )

同源策略阻止什么

同源策略最初由网景公司添加到浏览器,当前所有的浏览器都支持此功能。

初始的功能是为了限制非同域网页之间cookie的访问,例如a.html页面无法访问非同域的b.html页面的cookie。

功能是为了保护网站信息安全,比如,一个电脑可以访问很多网站,总不能此网站可以读取另一个网站的信息,尤其是涉及到重要信息,比如用户名和密码。

随着浏览器功能的增强,同源策略由最初限制cookie的访问,发展到限制多种本地信息:

  • Cookie、LocalStorage 和 IndexedDB访问限制。
    a.html的脚本无法访问b.html页面下的上述种类浏览器缓存,为了防止恶意网站通过js获取用户其他网站的cookie。

  • DOM获取限制。
    在浏览器中,<script><img><iframe><link>等含有src属性的标签都可以加载跨域资源,而不受同源限制,但浏览器限制了JavaScript的权限使其不能读、写加载的内容。
    如果没有这一条,恶意网站可以通过iframe打开银行页面,可以获取dom就相当于可以获取整个银行页面的信息。

  • 限制 ajax 请求,准确来说是限制操作 ajax 响应结果,本质上跟上一条是一样的

    假设有一个黑客叫做小黑,他从网上抓取了一堆美女图做了一个网站,每日访问量爆表。
    为了维护网站运行,小黑挂了一张收款码,觉得网站不错的可以适当资助一点,可是无奈伸手党太多,小黑的网站入不敷出。
    于是他非常生气的在网页中写了一段js代码,使用ajax向淘宝发起登陆请求,因为很多数人都访问过淘宝,所以电脑中存有淘宝的cookie,不需要输入账号密码直接就自动登录了,然后小黑在ajax回调函数中解析了淘宝返回的数据,得到了很多人的隐私信息,转手一卖,小黑的网站终于盈利了。
    如果跨域也可以发送AJAX请求的话,小黑就真的获取到了用户的隐私并成功获利了!!!

同源策略允许什么

  • 同源策略只对网页的HTML文档做了限制,对加载的其他静态资源如javascript、css、图片等仍然认为属于同源。
  • 页面中的<script><img><ifram><link>等标签、重定向以及表单提交是不会受到同源策略限制的,比如在网站www.foo.com下提交一个表单到www.bar.com是完全可以的。
  • 跨域资源嵌入是允许的。
    脚本本身的来源和同源策略并不相关,相关的是脚本所嵌入的文档的来源,理解这一点很重要。例如、假设一个来自主机A的脚本被包含到(使用<script>标记的src属性)宿主B的一个web页面中。这个脚本的来源就是主机B而不是A,并且可以完整的访问包含它的文档内容。如果脚本打开一个新的窗口载入来自主机B的另一个文档,脚本对这个文档的内容也完全具有访问权限。但是,如果脚本打开第三个窗口并载入一个来自主机C吃文档(或者是主机A),同源策略会发挥作用,阻止脚本访问这个文档。

如何跨域

1.iframe跨域 参考happy哥

  • document.domain跨域

    此方案仅限主域相同,子域不同的跨域应用场景。

    同源策略会给那些使用多个子域的大站点带来一些问题。如,来自home.example.com的文档里脚本想要合法的读取从developer.example.com上的文档的属性。为了支持这种类型的多域名站点,可以使用Document对象的domain属性。在默认情况下,属性domain存放的是载入文档的服务器的主机名。可以设置这一个属性,不过字符串必须具有有效的域前缀或他本身。因此,如果一个domain属性的初始值是字符串home.example.com,就可以把它设置为example.com。但是不能设置为home.example或者ample.com。另外domain值中必须有一个.,不能把它设置为com或其他顶级域名。如果两个窗口(或窗体)包含的脚本把domain设置成了相同的值,那么这两个窗口就不会再受同源策略的约束。
    home.example.comdeveloper.example.com的文档同时使用脚本设置

    1
    document.domain = 'example.com'

    实现跨域,cookie也可以用这种方法实现跨域

  • location.hash 跨域
    当主域不同时,可以使用此方法
    在url中,http://www.baidu.com#helloword#helloworad就是location.hash,改变hash值不会导致页面刷新,所以可以利用hash值来进行数据的传递,当然数据量是有限的。
    父窗口可以把信息,写入子窗口的location.hash

    1
    2
    var src = originURL + '#' + data;
    document.getElementById('myIFrame').src = src;

    子窗口通过监听hashchange事件得到通知。

    1
    2
    3
    4
    5
    window.onhashchange = checkMessage;
    function checkMessage() {
    var message = window.location.hash;
    // ...
    }

    同样的,子窗口也可以改变父窗口的片段标识符。

    1
    parent.location.href= target + "#" + hash;
  • window.name跨域
    window.name(一般在js代码里出现)的值不是一个普通的全局变量,而是当前窗口的名字,要注意的是每个iframe都有包裹它的window,而这个window是top window的子窗口,而它自然也有window.name的属性,window.name属性的神奇之处在于name值在不同的页面(甚至不同域名)加载后依旧存在(如果没有修改则值不会变化),并且可以支持非常长的name值(2MB)
    举个简单的例子:你在某个页面的控制台输入:

    1
    2
    window.name = "hello world"
    window.location = "http://www.baidu.com"

    页面跳转到了百度首页
    在这里插入图片描述
    但是window.name却被保存下来了,还是hello world

    这种方法的优点是,window.name容量很大,可以放置非常长的字符串;缺点是必须监听子窗口window.name属性的变化,影响网页性能。

  • 跨文本消息 postMessage
    postMessage 是 HTML5 新增加的一项功能,跨文档消息传输(Cross Document Messaging),目前:Chrome 2.0+、Internet Explorer 8.0+, Firefox 3.0+, Opera 9.6+, 和 Safari 4.0+ 都支持这项功能。
    举例来说,父窗口http://aaa.com向子窗口http://bbb.com发消息,调用postMessage方法就可以了。

    1
    2
    var popup = window.open('http://bbb.com', 'title');
    popup.postMessage('Hello World!', 'http://bbb.com');

    子窗口向父窗口发送消息的写法类似。

    1
    window.opener.postMessage('Nice to see you', 'http://aaa.com');

    父窗口和子窗口都可以通过message事件,监听对方的消息。

    1
    2
    3
    window.addEventListener('message', function(e) {
    console.log(e.data);
    },false);

    message事件的事件对象event,提供以下三个属性。

    1
    2
    3
    event.source:发送消息的窗口
    event.origin: 消息发向的网址
    event.data: 消息内容
  1. Ajax跨域
  • Jsonp
    JSONP是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。

    它的基本思想是,网页通过添加一个<script>元素,向服务器请求JSON数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。

    首先,网页动态插入<script>元素,由它向跨源网址发出请求。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function addScriptTag(src) {//定义添加script标签函数
    var script = document.createElement('script');
    script.setAttribute("type","text/javascript");
    script.src = src;
    document.body.appendChild(script);
    }
    window.onload = function () {//当文档加载完成时,发送Jsonp请求
    addScriptTag('http://example.com/ip?code=123&callback=foo');
    //url中包含code需要的数据,与需要的回调函数callback
    }
    function foo(data) {//回调函数,当数据返回时,调用
    console.log('Your public IP address is: ' + data.ip);
    };

    上面代码通过动态添加<script>元素,向服务器example.com发出请求。注意,该请求的查询字符串有一个callback参数,用来指定回调函数的名字,这对于JSONP是必需的。
    服务器收到这个请求以后,会将数据放在回调函数的参数位置返回。

    1
    2
    3
    foo({
    "ip": "8.8.8.8"
    });

    由于<script>元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了foo函数,该函数就会立即调用。

  • CORS跨域资源共享
    CORS的主要工作在后端,是HTML5规范定义的如何跨域访问资源。
    Origin表示本域,也就是浏览器当前页面的域。当JavaScript向外域(如sina.com)发起请求后,浏览器收到响应后,首先检查Access-Control-Allow-Origin是否包含本域,如果是,则此次跨域请求成功,如果不是,则请求失败,JavaScript将无法获取到响应的任何数据。
    用一个图来表示就是:
    在这里插入图片描述
    假设本域是my.com,外域是sina.com,只要响应头Access-Control-Allow-Originhttp://my.com,或者是*,本次请求就可以成功。

    可见,跨域能否成功,取决于对方服务器是否愿意给你设置一个正确的Access-Control-Allow-Origin,决定权始终在对方手中。

    上面这种跨域请求,称之为“简单请求”。简单请求包括GET、HEAD和POST(POST的Content-Type类型 仅限application/x-www-form-urlencodedmultipart/form-datatext/plain),并且不能出现任何自定义头(例如,X-Custom: 12345),通常能满足90%的需求。

    无论你是否需要用JavaScript通过CORS跨域请求资源,你都要了解CORS的原理。最新的浏览器全面支持HTML5。在引用外域资源时,除了JavaScript和CSS外,都要验证CORS。例如,当你引用了某个第三方CDN上的字体文件时:

    1
    2
    3
    4
    5
    /* CSS */
    @font-face {
    font-family: 'FontAwesome';
    src: url('http://cdn.com/fonts/fontawesome.ttf') format('truetype');
    }

    如果该CDN服务商未正确设置Access-Control-Allow-Origin,那么浏览器无法加载字体资源。

    对于PUTDELETE以及其他类型如application/json的POST请求,在发送AJAX请求之前,浏览器会先发送一个OPTIONS请求(称为preflighted请求)到这个URL上,询问目标服务器是否接受:

    1
    2
    3
    4
    OPTIONS /path/to/resource HTTP/1.1
    Host: bar.com
    Origin: http://my.com
    Access-Control-Request-Method: POST

    服务器必须响应并明确指出允许的Method:

    1
    2
    3
    4
    HTTP/1.1 200 OK
    Access-Control-Allow-Origin: http://my.com
    Access-Control-Allow-Methods: POST, GET, PUT, OPTIONS
    Access-Control-Max-Age: 86400

    浏览器确认服务器响应的Access-Control-Allow-Methods头确实包含将要发送的AJAX请求的Method,才会继续发送AJAX,否则,抛出一个错误。

    由于以POST、PUT方式传送JSON格式的数据在REST中很常见,所以要跨域正确处理POST和PUT请求,服务器端必须正确响应OPTIONS请求。

  1. cookie跨域
  • document.domain跨域,与iframe类似

  • Jsonp跨域

  • Ajax+cors跨域
    默认情况下,标准的跨域请求是不会发送不同源的cookie的

    1
    2
    var xhr = new XMLHttpRequest();
    xhr.withCredentials = true;

    为了安全,标准里不允许 Access-Control-Allow-Origin: **必须指定明确的、与请求网页一致的域名。

  1. LocalStorage跨域
  • postMessage

参考:
https://blog.csdn.net/hansexploration/article/details/80314948
http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html
https://www.liaoxuefeng.com/wiki/1022910821149312/1023022332902400
https://blog.csdn.net/shuidinaozhongyan/article/details/78155310
https://blog.csdn.net/chou_out_man/article/details/80664413
https://blog.csdn.net/itcats_cn/article/details/82318092
https://www.cnblogs.com/happy-8090/p/11570998.html


同源限制理解与解决
https://luoluoqinghuan.cn/2020/03/02/同源限制理解与解决/
作者
David Mu
发布于
2020年3月2日
许可协议