跨域解决方案及实现

摘要:

身为一个前端,每当别人提起跨域,我的心里还真是虚,当面试官问我了解跨域吗,“了解”,解决方案呢,“jsonp或者iframe再或者代理”,好吧这个问题基本凉透了。日常的工作中,我们很少有机会去涉及配置跨域问题或者说基本没有,工作三年时间,干过产品也干过项目,整体架构都是大佬们配置好的,于是我就开始卡卡累业务流程了,很多问题其实平时都听过,也知道个大概,但是具体知识点其实很模糊,今天就记录一下什么是跨域,跨域的由来,以及什么是同源策略,为什么有这个东西存在,具体的解决方案是什么。

什么是跨域:

跨域字面意思不是同一个域,一个域去请求(获取)另外一个域下的资源,就是跨域。

跨域的由来:

听到跨域,我们总能跟一个词叫同源策略联系到一起,因为同源策略才会出现的跨域的情况,那么什么是同源策略。

同源策略:

同元策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制(官方答案,详见:https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy)。啥意思嘞,就是不是一个域的不让互相请求,首先这是浏览器的一种机制,其次其实是可以请求的,但是返回结果被浏览器拦截了。浏览器从哪些方面做的这个同源策略呢。

(1)针对接口请求(2)针对dom查询,如果没有同源策略会怎么样呢。

没有同源策略限制的接口请求:

首先了解下http是无状态协议,每次请求服务器的时候需要标识,不然服务器不认啊,这就是cookie了,这个cookie当然是服务器颁发并在响应头Set-Cookie字段返回给浏览器的,再次发送的时候回带着这个cookie,服务器见到这个cookie就如同见到你这个人,知道两次都是你干的。如果这时候再放问另一个站点呢,浏览器会带着cookie一起访问,这时候你不注册,站点也有你的身份了,这个站点如果用着你的cookie干点别的事儿呢,这就很尴尬了。这种情况也叫CSFR攻击方式

没有同源策略限制的dom查询:

1 <iframe name='site_a' src='www.a.com'></iframe>
2 <script>
3     var iframe = window.frames['site_a'];
4     var input = iframe.document.getElementById('input');
5     console.log(`input内容:${input}`);
6 </script>

这个时候我们可以拿到site_a的想要的页面信息,如果这个嵌套页面是一个银行网站呢,是不是你的信息就泄露了呢~ 这就是出现浏览器同源策略的原因,那么如何解决跨域问题呢?

同源策略具体方案:

服务器api响应允许跨域请求(CORS)

脚本请求设置

PostClick.onclick = function () {
            let option = {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    // 'Content-Type': 'application/json',
                    'Accept': 'application/json'
                },
                cache: 'no-cache',
                mode: "cors",
                // body: JSON.stringify({})
            },
                url = `http://localhost:1111/httppost`;
            fetch(url, option)
                .then(res => {
                    return res.json();
                })
                .then(res => {
                    console.log(res);
                })
                .catch(e => {
                    console.log(e);
                })
        }

这里主要针对mode进行设置,将mode设置为允许跨域访问

服务器api设置

router.post("/httppost", async ctx => {
  ctx.set("Access-Control-Allow-Origin", ctx.headers.origin);
  ctx.set("Access-Control-Allow-Headers", "content-type");
  ctx.set("Access-Control-Allow-Methods", "OPTIONS,GET,HEAD,PUT,DELETES,PATCH");
  ctx.response.status = 200;
  ctx.response.type = "json";
  ctx.response.body = {
    message: "http post data."
  };
});

这种处理方式仅仅可以针对一些特殊得api做处理,使用场景并不多。

jsonp

脚本代码展示

var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'http://127.0.0.1:1111/jsonp?callback=handleCallback';
document.head.appendChild(script);
function handleCallback(res) {
    console.log(JSON.stringify(res));
}

服务端需要配合jsonp

router.get('/jsonp', async ctx => {
  var data = { 'data': 'post data' };
  ctx.response.body = ctx.query.callback + '(' + JSON.stringify(data) + ');';
})

jsonp的处理方式仅仅支持http的get请求,这种方式已经很老了,已经不怎么使用了,在一些老的项目中我们偶尔会看到这种处理方式。

Nginx反向代理

脚本代码

NginxClick.onclick = function () {
            let option = {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    // 'Content-Type': 'application/json',
                    'Accept': 'application/json'
                },
                cache: 'no-cache',
                mode: "no-cors",
                // body: JSON.stringify({})
            },
                url = `http://localhost/nginxrequest`;
            fetch(url, option)
                .then(res => {
                    return res.json();
                })
                .then(res => {
                    console.log(res);
                })
                .catch(e => {
                    console.log(e);
                })
        }

后台代码

router.post('/nginxrequest', async ctx => {
  ctx.response.status = 200;
  ctx.response.type = "json";
  ctx.response.body = {
    message: "http post data."
  };
})

nginx配置

upstream node_server {   
  server 127.0.0.1:1111;
}


server {
    listen       80;
    server_name  localhost;

    location / {
        root   D:/code/HTTPCORS;
        index  index.html index.htm;
    }

    location /nginxrequest {
        proxy_pass http://node_server;
    }

    error_page   500 502 503 504  /50x.html;
    
    location = /50x.html {
        root   html;
    }
}

nginx解决跨域问题是通过代理的方式解决,当页面发送请求时,还是再同一域下发送,但是到了web server的时候转发到代理服务器上,这就是nginx反向代理的实现方式,同时也避免了跨域请求的问题。

HTTP代理

服务器代码

const Koa = require("koa");
const path = require("path");
const static = require('koa-static');
const config = require("../config/config");
const proxy = require('http-proxy-middleware');
const k2c = require('koa-connect');

let app = new Koa();
app.use(static(path.join(__dirname, "../public/")));
/**
 * 代理转发目标Url
 */
let targetUrl = `http://${config.apiHost}:${config.apiPort}`;
/**
 * 代理转发
 */
app.use(async (ctx, next) => {
  if(ctx.url.startsWith('/api')){
    ctx.respond = false;
    await k2c(proxy({
      target: targetUrl
    }))(ctx, next);
  }else{
    await next();
  }
})

这里我把自己的服务器代码复制过来了,通过httpproxy转发请求,本质上跟nginx反向代理是一样的,只不过没有通过web server进行转发,而是通过后台进行转发,有兴趣的自己看下httpproxy的源码吧~

以上就是针对服务器级别的跨域解决方案

DOM层面的跨域解决方案如下

iframe跨域(网上找的)

分别配置两个html页面跟一个代理服务页面

server {
        listen       2020;
        server_name  localhost;

        location / {
            root   D:/code/HTTPCORS/htmlA;
            index  index.html index.htm;
        }

        location /proxy {
            alias  D:/code/HTTPCORS/htmlC;
            index  index.html index.htm;
        }
    }

    server {
        listen       2021;
        server_name  localhost;

        location / {
            root   D:/code/HTTPCORS/htmlB;
            index  index.html index.htm;
        }
    }

    server {
        listen       2022;
        server_name  localhost;

        location / {
            root   D:/code/HTTPCORS/htmlC;
            index  index.html index.htm;
        }
    }

htmlA脚本

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://localhost:2020/proxy/';
                    state = 1;
                }
                callback(iframe.contentWindow.name);
            };

            document.body.appendChild(iframe);

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

        // 请求跨域b页面数据
        proxy('http://localhost:2021/', function (data) {
            alert(data);
        });

htmlB脚本

   <script type="text/javascript">
        window.name = '我是htmlB';
    </script>

htmlC作为代理页面与htmlA为同一域下,localhost:2020/proxy/

其他与iframe跨域相关有iframe与location.hash跟iframe与document.domain,由于firame不常用,本人工作也基本没有涉及,这里就做总结了。

postMessage(H5的API)

htmlA

<iframe id="iframe" src="http://localhost:2021/index.html" style="display:none;"></iframe>
<script>       
    var iframe = document.getElementById('iframe');
    iframe.onload = function() {// 向htmlB传送跨域数据
        iframe.contentWindow.postMessage(JSON.stringify({name:'dqhan'}), 'http://localhost:2021');
    };

    // 接受htmlB返回数据
    window.addEventListener('message', function(e) {
        console.log(e.data);
    }, false);
</script>

htmlB

<script>
    // 接收htmlA的数据
    window.addEventListener('message', function(e) {var data = JSON.parse(e.data);
        if (data) {
            data.age = 18;
            // 处理后再发回htmlA
            window.parent.postMessage(JSON.stringify(data), 'http://localhost:2020/');
        }
    }, false);
</script>

该方式可以应用于多窗口之间通信,iframe页面嵌套跨域获取脚本内容,由于现在html5已经普及,可以完全替代document.domain、localtion.hash、window.name三种方式。

 demo代码地址:https://github.com/Dqhan/HTTPCORS

原文地址:https://www.cnblogs.com/moran1992/p/10472051.html