后端之美-ASP.net

跨域最优解

2018-07-25  本文已影响109人  lihuanji

项目中和面试经常遇到跨域问题,老生常谈的问题,今天就来讲讲跨域:

浏览器不支持跨域?

  1. 用户登录A网站,在cookie里面存入了登录信息,用户再登录B网站,如果没有同源策略,B网站能够轻易拿到用户A网站信息,去伪造用户向A网站发送信息。
  2. DOM元素,iframe嵌入页面,如果没有同源策略,嵌入一个淘宝页面,伪造成淘宝钓鱼网站,用户输入账户密码可以通过dom操作拿到账户信息。
  3. ajax,LocalStorage也不支持跨域,不能随意拿取信息。

总的来说浏览器不支持跨域是为了安全,但在项目中有些情况下需要去跨域。

先讲讲什么是同源策略

3个都一样为同域,其中某一个不一样那么就跨域了。

列如你的网站域名是http://www.xxx.com

但是在浏览器中link标签(加载css) img(加载图片) script(加载js) 标签不受同源策略,可以随便跨域,去加载其他域下面的资源。

jsonp

由于script标签不受同源策略,可以用script去跨域,原理就是创建一个script标签,src地址去引入其他域下的js文件并且带入参数和回调函数,js文件返回一个执行函数,去执行window下的回调函数。

例如现在要向www.xxx.com去请求数据,首先我们在window下声明一个函数a,然后创建一个script标签<script src="www.xxx.com?params=xxx&cb=a"></script>,params代表请求参数,cb指定成功后需要调用的回调函数,这个请求返回一个js文件内容a({data: 'xxx'}),执行了最开始在window下声明的函数a并且传入我们需要的数据。

下面来看看,如果封装一个简单的jsonp函数

    function jsonp({ url, params }) {
        return new Promise((resolve, reject) => {
            // 创建script标签
            const script = document.createElement('script');
            
            // window下声明回调函数
            window.cb = function (data) {
                resolve(data);
                document.body.removeChild(script);
            }
            
            const arr = []
            
            // 拼接参数
            for (let key in params) {
                arr.push(`${key}=${params[key]}`);
            }
            
            script.src = `${url}?${arr.join('&')&cb=cb}`
            
            // 加载文件
            document.body.appendChild(script);
        })
    }
    
    
    jsonp({
        url: 'www.xxx.com/xxx',
        params: {xx: xx},
    }).then((data) => {
        // data数据
    })

后端实现

const express = require('express');
const app = express();

app.get('/xxx', function(req, res) {
    const { xx, cb } = req.query;
    
    // 返回cb(xxx)
    res.end(`${cb}(获取到数据${xx})`);
})

app.listen(80)

jsonp缺点: 只支持get请求并且不安全,如果加载第三方返回script标签,会出现恶意攻击(xss)。

cors

解决jsonp缺点 支持get post put delet请求,由服务端控制,安全性高,前端正常发送ajax请求,项目中最常用的方式。

上面说了同源策略是浏览器的行为,其实我们的请求能够到达服务器,只是浏览器给屏蔽掉了数据,cors就是利用http header头告诉浏览器一些信息,浏览器放开同源策略。

简单请求

  1. 必须是以下三种方法
  1. 请求头只能包含以下字段

如果不满足上面的简单请求,那么就是非简单请求,非简单请求的时候浏览器会首先发出一个预检请求OPTIONS到服务器,把将要发送的请求方法,请求头给服务器,如果服务器返回成功,那么浏览器才会发出正式的请求,否则报错。

服务器可以设置的header头

用express简单实现一个cors跨域服务端

    const express = require('express');
    const app = express();
    
    app.use((req, res, next) => {
        
        res.setHeader('Access-Control-Allow-Origin', 'http://localhost:8080');
        res.setHeader('Access-Control-Allow-Methods', 'POST,GET,PUT,DELETE');
        res.setHeader('Access-Control-Allow-Headers', 'name,token');
        res.setHeader('Access-Control-Allow-Credentials', true);
        res.setHeader('Access-Control-Max-Age', 6);
        res.setHeader('Access-Control-Expose-Headers', 'token');
        res.setHeader('Access-Control-Request-Headers', 'token');
        
        // 如果为预检请求,直接返回同意,避免浪费之后处理资源
        if (req.method === 'OPTIONS') {
            res.end();
        }
        
        next();
    });
    
    app.put('/uset', (req, res) => {
        res.end('yes');
    });

postMessage

除了与服务器通信需要跨域,有时候会存在iframe加载跨域,window.open()一个标签页跨域通信,html5为了解决这个问题新增了postMessage方法。

postMessage(需要传递的参数, 目标源)

iframe:

localhost:3000 下 a.html

<iframe 
    src="http://localhost:4000/b.html"
    id="frame"
    onload="load()"
></iframe>
<script>
    function load () {
        const i = document.getElementById('frame')
        i.contentWindow.postMessage('send', 'http://localhost:4000/')
    }
    
    window.addEventListener('message', function(e) {
        console.log(e.data) // 接收到ok
    })
</script>

localhost:5000 下 b.html
<script>
 window.addEventListener('message', function(e) {
     console.log(e.data) 接收到send
     
     e.source.postMessage('ok', e.origin)
 }, false)
</script>

注意: 一定要在onload结束之后再发送postMessage,否则子页面接收不到消息

window.open()

localhost:3000 下 a.html
<input type="button" value="打开窗口" onclick="open_new()">
<script>
    function open_new() {
        const newWindow = window.open('http://localhost:4000/b.html');
        
        // 跨域无法监听onload 如果同域下跨域使用
        newWindow.onload = function () {
            newWindow.postMessage('open', 'http://localhost:4000/')
        }

        setTimeout(() => {
            newWindow.postMessage('open', 'http://localhost:4000/')
        }, 1000);
    }

    window.addEventListener('message', function(e) {
        console.log(e.data)
    })
</script>

localhost:5000 下 b.html
<script>
window.addEventListener('message', function(e){
    console.log(e)
}, false);
        
window.opener.postMessage('Nice to see you', 'http://localhost:3000/');
</script>

window.open()如果是跨域,则无法监听onload事件

document.domain

该方法存在一定的限制条件,必须是2个网页的一级域名相同,用法也很简单,可以使2个页面共享cookie。

例如: a.xxx.comb.xxx.com:

同时设置docment.domain = 'xxx.com'

服务器设置cookie时也设置到xxx.com

这样的话,在这个一级域名下的所有二三级域名,都可以互通cookie

window.name

利用window.name改变网页地址,该值不变的特点,可以做到跨域。

a网站iframe加载b网站,b网站把需要传输的数据放入window.name中,然后重定向到a网站下同域的网址,这时a网站可以顺利的拿到window.name属性。

localhost: 3000 下的a.html

<iframe id="fra" src="http://localhost:4000/b.html" onload="load()"></iframe>
<script>
    function load(){
        const f = document.getElementById('fra');

        console.log(f.contentWindow.name);
    }
</script>

localhost: 4000 下的b.html
<script>
    window.name = 'hello'

    window.location = 'http://localhost:3000/c.html'
</script>

localhost: 3000 下的c.html
无需任何代码,建一个空的html文件

location.hash

hash值得变化不会导致页面刷新,通过a页面改变b页面hash值,b页面不能直接通过parent去修改a页面的hash值,需要通过加载一个a域下的代理iframe修改,从而实现跨域通信。

localhost: 3000 下的a.html

<iframe id="fra" src="http://localhost:4000/b.html" onload="load()"></iframe>
<script>
    function load(){
        const f = document.getElementById('fra');
        f.src = 'http://localhost:4000/b.html#hello'
    }
    window.addEventListener('hashchange', function() {
        console.log(window.location.hash)
    })
</script>

localhost: 4000 下的b.html

<script>
    addEventListener('hashchange', function() {
        console.log(window.location.hash)
        
        const ifr = document.createElement('iframe');
        ifr.style.display = 'none';
        ifr.src = 'http://localhost:3000/c.html#yes';
        document.body.appendChild(ifr);
    })
</script>

localhost: 3000 下的c.html
<script>
    parent.parent.location.hash = window.location.hash.substring(1);
</script>

other

以上就是项目中最常用到的跨域方式,还有一种与服务器通信方式websocket不受浏览器同源策略。平常在开发中经常用到的webpack-dev-server,nginx,http-server...利用的是代理去请求。

先是本地起了一个代理服务器,前端发送http到代理服务器,由代理服务器请求后端的接口,跨域是浏览器的行为,2个服务器之间是没有同源策略的,所有代理服务器拿到数据后,再返回给前端。代理服务器和前端同源。

image
上一篇 下一篇

猜你喜欢

热点阅读