同源策略实现跨域第二弹

313 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

上一期我们提到jsonp和cors两种方法来避开同源策略,实施跨域请求。本期继续讲解另外的跨域方法。

Nginx相信大家都很熟悉,很多时候我们用它来做反向代理。那么就用这一点,我们来解决跨域问题

nginx反向代理跨域

nginx配置解决iconfont跨域:浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。

location / {
  add_header Access-Control-Allow-Origin *;
}

nginx反向代理接口跨域:注意,跨域问题根源是同源策略。而同源策略仅仅是针对浏览器的安全策略。服务器端调用HTTP接口只是使用HTTP协议而已,不受同源策略影响,也就不存在跨域问题。实现思路:通过Nginx配置一个代理服务器域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域访问。

我们来看配置:

server {
    listen       81;
    server_name  www.domain1.com;

    location / {
        proxy_pass   http://www.yp2.com:8080;  #反向代理
        proxy_cookie_domain www.yp2.com www.yp1.com; #修改cookie里域名
        index  index.html index.htm;

        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.yp1.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}

nodejs中间件代理跨域

node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。

我们简单使用node + express + http-proxy-middleware搭建一个proxy服务器。

const express=require('express')
const app=express()
app.listen(5000)
const httpProxyMiddleware=require('http-proxy-middleware')
// 服务器代理  ---接口中间层 代理层
app.use('/api' ,httpProxyMiddleware.createProxyMiddleware({
    // 代理的地址
    target:'http://localhost:8989',
    // 默认false不修改。修改代理请求是他的主机名
    changeOrigin:true,
    // 修改响应头信息,实现跨域并允许带cookie
    onProxyResfunction(proxyRes, req, res) {
        res.header('Access-Control-Allow-Origin''http://localhost:5000');
        res.header('Access-Control-Allow-Credentials''true');
    },

    // 匹配规则
    pathRewrite:{
        // 访问路径 映射到 目标服务器中的路径
        '^/v1/api':'/'
    }
}))

考录到很多人使用vue,那么vue中实现开发环境的时的反向代理进行跨域,只需在项目根目录下面创建一个vue.config.js文件,写下如下代码:

module.exports={
    // 指定服务器模块
    devServer:{
        // 代理
        proxy:{
            '/v1/api':{
                // 目标地址
                target:'http://localhost:3000',
                changeOrigin:true,
                pathRewrite:{
                    '/v1/api':'/api'
                }
            }
        }
    }
}

还有一种不是很正规的方法,就是利用document.domain + iframe组合实现跨域。但是有个前提条件,就是这两个域名必须属于同一个二级域名!而且所用的协议,端口都要一致,否则无法利用document.domain进行跨域。这是因为Javascript出于对安全性的考虑,而禁止两个或者多个不同域的页面进行互相操作。相同域的页面则不受影响。

比如说域名为“es6.jikesuixiang.com”,domain可以设置为"jikesuixiang.com",如果是"es7.jikesuixiang.com"就会报错。

image.png

假设父窗口url为:father.baidu.com/a.html

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

子窗口url为:child.baidu.com/b.html

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

此外,还有一种location.hash + iframe跨域方法。其中hash 属性是一个可读可写的字符串,该字符串是 URL 的锚部分(从 # 号开始的部分)。大致原理就是:a想要与b跨域相互通信,通过中间页c来实现;三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。利用location.hash传值,创建定时器,hash的变化,执行相应的操作。下面我们来具体实现:

A域:a.html -> B域:b.html -> A域:c.html

a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。我们假设a页面url为:www.baidu1.com/a.html

<iframe id="iframe" src="http://www.baidu2.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>

b页面的url为:www.baidu2.com/b.html

<iframe id="iframe" src="http://www.baidu1.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>

c页面的url为:www.baidu1.com/c.html

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

这种方法可以解决域名完全不同的跨域,同时还可以实现双向通讯。但是location.hash会直接暴露在URL里,并且在一些浏览器里会产生历史记录,数据安全性不高也影响用户体验;另外由于URL大小的限制,支持传递的数据量也不大。

WebSocket协议跨域

前面说到的同源策略,其实本质还是限制了XMLHttpRequest对象。那我们可以这样考虑,不适用XMLHttpRequest对象,前面借助一些没有被限制的标签来跨域,比如script等,我们换个思路,直接使用另一种不受限制的协议,比如说:WebSocket协议。

WebSocket协议是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

Web浏览器和服务器都必须实现 WebSockets 协议来建立和维护连接。由于 WebSockets 连接长期存在,与典型的HTTP连接不同,对服务器有重要的影响。基于多线程或多进程的服务器无法适用于 WebSockets,因为它旨在打开连接,尽可能快地处理请求,然后关闭连接。任何实际的 WebSockets 服务器端实现都需要一个异步服务器。

前端代码设计:

<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io('http://www.baidu2.com:8080');

// 连接成功处理
socket.on('connect'function() {
    // 监听服务端消息
    socket.on('message'function(msg) {
        console.log('data from server: ---> ' + msg); 
    });

    // 监听服务端关闭
    socket.on('disconnect'function() { 
        console.log('Server socket has closed.'); 
    });
});

document.getElementsByTagName('input')[0].onblur = function() {
    socket.send(this.value);
};
</script>

我们依然采用Nodejs 做后端:

var http = require('http');
var socket = require('socket.io');

// 启http服务
var server = http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-type''text/html'
    });
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

// 监听socket连接
socket.listen(server).on('connection'function(client) {
    // 接收信息
    client.on('message'function(msg) {
        client.send('hello:' + msg);
        console.log('data from client: ---> ' + msg);
    });

    // 断开处理
    client.on('disconnect'function() {
        console.log('Client socket has closed.'); 
    });
});

OK,本期就写到这里,感谢阅读。