7种跨域方案操练起来

1,539 阅读5分钟

在不了解同源策略与跨域之前,总会遇到这样一个场景。在web端与后台调接口时经常会发现下图这样的错误,但用 POSTMAN 确可以正常访问接口,不免与后端伙伴一顿唇枪舌战分析问题原因。这是因为触发了浏览器的\color{red}{同源策略}。Access-Control-Allow-Origin 响应头指定了该响应的资源是否被允许与给定的 origin 共享。

同源策略限制以下几种行为:

  • 1.Cookie、LocalStorage 和 IndexDB 无法读取
  • 2.DOM 和 Js对象无法获得
  • 3.AJAX 请求不能发送
//服务端代码
const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);
//浏览器端代码
let xhr = new XMLHttpRequest()
xhr.open("GET", "http://localhost:3000/api")
xhr.onreadystatechange = function(response) {
    console.log(response)
}
xhr.send()

同源策略

MDN 中的定义,同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。通俗的说 浏览器 端不可以直接访问同源网站。而同源呢指的是 域名、 协议、 端口 都相同。

域跨起来

今天主要总结一下跨域的方法:

  • 1.jsonp
  • 2.cors
  • 3.代理
  • 4.domain + iframe
  • 5.location.hash + iframe跨域
  • 6.window.name + iframe跨域
  • 7.postMessage跨域

1.jsonp

jsonp 是利用 script 标签具有跨域请求资源的能力。

  • 浏览器端生成 script 标签资源 src 指向目标接口。
  • 服务端将响应数据包装成一个 JS function,并调用 function 将响应数据回调出去。
// 客户端代码
let url = "http://localhost:3000/api";
function cb(res){
    console.log(res)    //响应数据
}
function jsonp(url){
    let script = document.createElement("script");
    script.src = url;
    document.body.appendChild(script)
}
jsonp(url)
//服务端代码
app.use(async ctx => {
  ctx.body = `function fn(cb){ cb("Hello World") } fn(cb)`;
});

由于 jsonp 是依赖标签加载资源的能力在老式浏览器有着较好的兼容性,浏览器端没有办法设置 请求头、请求方法 ... ,所以项目完全用 jsonp 来实现跨域不可能的。

2.CORS ( Cross-origin resource sharing )

CORS接收到此次请求后 , 首先会判断Origin是否在允许源(由服务端决定)范围之内,如果验证通过,服务端会在Response Header 添加 Access-Control-Allow-Origin、Access-Control-Allow-Credentials等字段。

//服务端代码
const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
    ctx.body = 'Hello World'
    ctx.set("Access-Control-Allow-Origin","http://localhost:8000")
});

app.listen(3000);

3.代理

既然跨域是由于浏览器的同源策略影响,那么浏览器可以请求当前域的服务通过代理去访问服务端接口,问题迎刃而解。

//浏览器端,当前域名http://localhost:8000/api
let xhr = new XMLHttpRequest()
xhr.open("GET", "http://localhost:8000/api")
xhr.onreadystatechange = function(response) {
    console.log(response)
}
xhr.send()
//前端静态资源服务
const Koa = require('koa');
const fs = require("fs");
const path = require("path");
const http = require("http");
const app = new Koa();

app.use(async ctx => {
 let apiReg = /^\/api/;
  if( apiReg.test( ctx.request.url ) ){
    let text = await new Promise((resolve, reject) => {
        http.get({
            hostname: 'localhost',
            port: 3000,
            path: ctx.request.url,
          }, (res) => {
            let buf = [];
            res.on('data', (data)=> {
                buf.push(data)
            })
            res.on('end', () => {
                let text = buf.toString();
                resolve(text)
            })
          });
    })
    ctx.body = text;
    
  }else{
    let html = fs.readFileSync( path.resolve(__dirname, "./index.html"),{encoding: 'utf8' } )
    ctx.body = html;
  }
});

app.listen(8000);

4.domain + iframe

此方案仅限主域相同,子域不同的跨域应用场景。 实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
1.父窗口:(www.domain.com/a.html)

<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
    document.domain = 'domain.com';
    var user = 'admin';
</script>

2.子窗口:(child.domain.com/b.html)

<script>
    document.domain = 'domain.com';
    // 获取父窗口中变量
    alert('get js data from parent ---> ' + window.parent.user);
</script>

5.location.hash + iframe跨域

实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。

具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。 1.a.html:(www.domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 向b.html传hash值
    setTimeout(function() {
        iframe.src = iframe.src + '#user=admin';
    }, 1000);
    
    // 开放给同域c.html的回调方法
    function onCallback(res) {
        alert('data from c.html ---> ' + res);
    }
</script>

2.b.html:(www.domain2.com/b.html)

<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 监听a.html传来的hash值,再传给c.html
    window.onhashchange = function () {
        iframe.src = iframe.src + location.hash;
    };
</script>

3.c.html:(www.domain1.com/c.html)

<script>
    // 监听b.html传来的hash值
    window.onhashchange = function () {
        // 再通过操作同域a.html的js回调,将结果传回
        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
    };
</script>

6.window.name + iframe跨域

ifreame 的 window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

  1. a.html:(www.domain1.com/a.html)
var proxy = function(url, callback) {
    var state = 0;
    var iframe = document.createElement('iframe');

    // 加载跨域页面
    iframe.src = url;

    // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
    iframe.onload = function() {
        if (state === 1) {
            // 第2次onload(同域proxy页)成功后,读取同域window.name中数据
            callback(iframe.contentWindow.name);
            destoryFrame();

        } else if (state === 0) {
            // 第1次onload(跨域页)成功后,切换到同域代理页面
            iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
            state = 1;
        }
    };

    document.body.appendChild(iframe);

    // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
    function destoryFrame() {
        iframe.contentWindow.document.write('');
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
    }
};

// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
    alert(data);
});
  1. proxy.html:(www.domain1.com/proxy....
    中间代理页,与a.html同域,内容为空即可。
  2. b.html:(www.domain2.com/b.html)
<script>
    window.name = 'This is domain2 data!';
</script>

总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

7.postMessage

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:

  1. 页面和其打开的新窗口的数据传递
  2. 多窗口之间消息传递
  3. 页面与嵌套的iframe消息传递
  4. 上面三个场景的跨域数据传递
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>       
    var iframe = document.getElementById('iframe');
    iframe.onload = function() {
        var data = {
            name: 'aym'
        };
        // 向domain2传送跨域数据
        iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
    };

    // 接受domain2返回数据
    window.addEventListener('message', function(e) {
        alert('data from domain2 ---> ' + e.data);
    }, false);
</script>

b.html:(www.domain2.com/b.html)

<script> 
    // 接收domain1的数据
    window.addEventListener('message', function(e) {
        alert('data from domain1 ---> ' + e.data);

        var data = JSON.parse(e.data);
        if (data) {
            data.number = 16;

            // 处理后再发回domain1
            window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
        }
    }, false);
</script>

参考

  • 前端常见跨域解决方案(全) --安静de沉淀