# 一、什么是跨域

跨域是指,由于浏览器的同源策略限制,将会阻止一个域的脚本与另一个域的内容获取及交互操作。

同源是指协议、域名、端口均相等,同源策略是浏览器安全机制的基础。常见的同源限制包括:

  • 无法读取非同源网页的 cookie、localStorage、sessionStorage、indexedDB;
  • 无法获取非同源网页的 DOM;
  • 无法向非同源地址发送 Ajax 请求(其实请求可以成功发出,只是同源策略禁止读取跨域地址返回的响应)。

其实,同源策略的本质是,一个域名下的 js,在未经允许的情况下,不得读取另一个域名的内容(也就是不给其它域操控当前域的机会,从而确保自身安全)。但是并不阻止向另一域名发送内容。
这也就是为什么 from 表单可以跨域提交数据,而 ajax 却不能完成跨域请求。
(from 表单提交后,是不会有任何数据返回,也就不会读到另一个域的数据,就不会受到同源策略的限制。而 ajax 发送跨域请求后,是一定会返回响应报文的,也就会读到另一个域的数据,这就违背了同源策略的限制。)

# 二、如何实现跨域通信

常用方案:

# 1. jsonp

实现原理:浏览器允许三个html标签跨域加载资源,分别是:link、script、img。基于此特性,可以通过动态创建 script 标签,加载一个带有回调函数名称的地址,实现跨域通信。

(1) 原生实现

    var script = document.createElement('script');

    script.src = 'http://www.jerryzhang.com:8080/login?user=admin&callback=handleCallback';
    document.body.appendChild(script);

    // 回调执行函数
    function handleCallback(res) {
        alert(JSON.stringify(res));
    }

服务端返回数据如下:

    handleCallback({"status": true, "user": "admin"})

前端获取服务端返回的数据,即可执行预先定义的回调函数,拿到参数中的数据。

(2) jquery ajax

ajax 本身只能同源使用,而 ajax 的 jsonp 只是对动态创建 script 标签实现 jsonp 的封装。

    $.ajax({
        url:'http://www.jerryzhang.com/login',
        type:'GET',
        dataType:'jsonp',   // 请求方式为 jsonp,此时 ajax 与 XMLHttpRequest 无关,只是对动态创建 script 标签实现 jsonp 的封装。
        jsonpCallback:'callback',
        data:{
            "username":"jerryzhang"
        }
    })

jsonp 缺陷:

  • 仅支持跨域 http/https 请求,不支持跨域 js 交互(如获取另一个域中的 DOM 节点);
  • 仅支持 get 请求 (为什么? 因为:jsonp 的实现原理核心是 script 标签,而 script 仅支持 get 方式加载资源,所以 jsonp 仅支持 get!)

若需要支持 get 之外的其它 http/https 跨域请求,可选择 CORS。

# 2. CORS

实现原理:CORS全称“跨域资源共享”(Cross-origin resource sharing),能实现任意方式(get、post、put等)的 http/https 请求。

关于CORS的更多细节,可参考阮一峰的跨域资源共享 CORS 详解 (opens new window)
对于前端开发者来说,CORS通信与同源AJAX通信没有区别,浏览器一旦发现AJAX请求跨域,就会自动添加一些附加的请求头信息(非简单请求时还会多发一次预检请求)。目前,IE10+及主流浏览器都支持CORSCORS通信的关键是服务端实现支持CORS的接口。

CORS请求分为两种:简单请求非简单请求。浏览器对两种请求的处理不同。

同时满足以下两个条件的,就是简单请求:

  • 请求方法为 HEADGETPOST中任意一种;
  • 头信息中只包含AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type(值为application/x-www-form-urlencoded、multipart/form-data、text/plain)

不满足上述两个条件的,就是非简单请求。

2.1 简单请求 基本流程
当浏览器发现这次跨域AJAX请求为简单请求时,就自动在头信息之中,添加一个Origin字段,用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

2.2 非简单请求 常见非简单请求

  • put、delete方法的ajax请求
  • 请求字段为 json 格式(Content-type: application/json)的 post 请求
  • 带自定义头字段的ajax请求,假设带自定义字段 x-custom-header

对应解决方法(均为服务端设置):

  • access-control-allow-methods: PUT,DELETE
  • access-control-allow-header: Content-type
  • access-control-allow-header: x-custom-header

# 3. WebSocket

实现原理:WebSocket实现了浏览器与服务器的全双工通信,同时允许跨域通信,是seaver push技术的很好实现。

    // 前端代码
    var ws = new WebSocket("wss://echo.websocket.org");

    ws.onopen = function(evt) { 
        console.log("Connection open ..."); 
        ws.send("Hello WebSockets!");
    };

    ws.onmessage = function(evt) {
        console.log( "Received Message: " + evt.data);
        ws.close();
    };

    ws.onclose = function(evt) {
        console.log("Connection closed.");
    };   

WebSocket通信需要服务端配合实现。

# 4. document.domain + iframe

此方案仅限于主域名相同,子域名不同的跨域场景。eg: http://www.jerryzhang.com/a.html 与 http://child.jerryzhang.com/b.html 主域名都是 jerryzhang.com,子域名分别为 www 和 child。

实现原理:通过将两个页面的 document.domain 设置成相同的域名(只能设置为公共主域名),人为的实现同域,避免了跨域。

父页面:(http://www.jerryzhang.com/a.html)

    <iframe id="iframe" src="http://child.jerryzhang.com/b.html"></iframe>
    <script>
        document.domain = 'jerryzhang.com'  // 设置成相同的域名(只能设置为公共主域名)
        var fatherName = 'jerry'
        console.log(document.getElementById('iframe').contentWindow.childName)  // 'child'
    </script>

子页面:(http://child.jerryzhang.com/a.html)

    <script>
        document.domain = 'jerryzhang.com'  // 设置成相同的域名(只能设置为公共主域名)
        var childName = 'child'
        console.log(window.parent.fatherName)   // 'jerry'
    </script>

缺陷:

  • 仅适用于主域名相同,子域名不同的跨域场景。

# 5. location.hash + iframe

实现原理:父页面与 iframe 页面可以读写彼此的 url,而 url 中的 hash 不参与实际 http 请求,且 onhashchange 事件可以监听到 hash 的变化,所以可以将 hash 作为跨域通信的桥梁。

假设父页面为 http://baidu.com/a.html ,iframe 页面为 http://google.com/b.html

(1) 父页面向 iframe 页面传送数据

    // 父页面 a.html
    <iframe id="iframe" src="http://google.com/b.html"></iframe>
    <script>
        var iframe = document.getElementById('iframe')
        iframe.onload = function() {
            var data = '1010101'
            iframe.contentWindow.location.hash = data
        }
    </script>

    // iframe 页面 b.html
    <script>
        window.addEventListener('hashchange', (data) => {
            console.log(location.hash)  // '#1010101'
        })
    </script>

(2) iframe 页面向父页面传送数据

由于跨域情况下,IE、Chrome 不允许直接修改 parent.location.hash,所以需借助一个与父页面同域的隐藏 iframe 页面,作为中间过渡,实现从子向父的数据传递。

    // 父页面 a.html
    <iframe id="iframe" src="http://google.com/b.html"></iframe>
    <script>
        window.addEventListener('hashchange', (data) => {
            console.log(location.hash)  // '#1010101'
        })
    </script>

    // iframe 页面 b.html
    <script>
        const data = '1010101'
        try {
            parent.location.hash = data     // 非 IE、Chrome 可直接修改 parent.location.hash
        } catch(e) {
            // 创建与父页面 a.html 同域的隐藏的代理 iframe 页面 proxy.html
            var ifrproxy = document.createElement('iframe')
            ifrproxy.style.display = 'none'
            ifrproxy.src = 'http://baidu.com/proxy.html#data'   // proxy.html 作为代理页面,先接收子页面的数据,再传递给父页面
            docuemnt.body.appendChild(ifrproxy)
        }
    </script>

    // 隐藏的代理 iframe 页面 proxy.html,将 b.html 传递来的 data,再传给真正的父页面 a.html
    <script>
        parent.parent.location.hash = location.hash.substring(1)
    </script>

# 6. window.name + iframe

# 7. postMessage + iframe

实现原理:postMessage() 方法提供了一种受控的可以规避同源策略实现跨域通信的机制。IE10及以上

语法:

    targetWindow.postMessage(message, targetOrigin)     // 在目标页面`targetWindow`向目标源`targetOrigin`发送消息`message`

    `targetWindow` 目标页面的引用,例如 iframe 页面的 contentWindow 属性。
    `message` 要发送到目标源`targetOrigin`的消息。若为对象需序列化成字符串。
    `targetOrigin` 目标页面`targetWindow`对应的域名。

假设父页面为 http://baidu.com/a.html ,iframe 页面为 http://google.com/b.html

    // 父页面 a.html
    <iframe id="iframe" src="http://google.com/b.html"></iframe>
    <script>
        // 向 b.html 发送数据
        var targetWindow = document.getElementById('iframe')
        var targetOrigin = 'http://google.com'
        targetWindow.postMessage('Hello b.html!', targetOrigin)  

        // 接收 a.html 发来的数据
        window.addEventListener('message', (data) => {      // 监听 message 事件,获取数据
            console.log(data)  // 'Hello a.html!'
        })
    </script>

    // iframe 页面 b.html
    <script>
        window.addEventListener('message', (data) => {      // 监听 message 事件,获取数据
            console.log(data)  // 'Hello b.html!'

            // 向 a.html 发送数据
            var targetOrigin = 'http://baidu.com'
            window.parent.postMessage('Hello a.html!', targetOrigin)  
        })
    </script>

安全问题:如果不希望你的网站接收来自别的网站的数据(比如跨域攻击),请确保不要为 message 事件添加监听器。

# 8. nginx代理

# 9. nodejs中间件代理

最后更新时间: 4/27/2021, 7:11:19 PM