跨域最优解

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

浏览器不支持跨域?

  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. 必须是以下三种方法
  • get
  • post
  • head(什么是head请求? -> 只返回响应头,不会返回响应内容,http1.0定义的方法,前端用的很少)
  1. 请求头只能包含以下字段
  • Accept

  • Accept-Language

  • Content-Language

  • Last-Event-ID

  • Content-Type:application/x-www-form-urlencoded multipart/form-data text/plain

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

服务器可以设置的header头

  • Access-Control-Allow-Origin: www.baidu.com: 允许百度这个域下的请求,可以配置多个域,以逗号隔开,还能设置为*(*代表所有域都能请求但是origin写*不能允许携带cookie凭证
  • Access-Control-Allow-Methods: POST,GET,PUT,DELETE: 允许哪些方法,以逗号隔开
  • Access-Control-Allow-Headers: name,token: 表示服务器支持name和token字段
  • Access-Control-Allow-Credentials: true: 允许cookie跨域
  • Access-Control-Max-Age: 6: 预检请求的有效期,单位为秒,相当于把预检请求缓存下来,下次直接发送正式请求,不用再去预检是否服务器支持该请求方法
  • Access-Control-Expose-Headers: token: 允许用js获取响应头里的token值
  • Access-Control-Request-Headers: token: 指定浏览发送请求时,需要带上的额外请求头

用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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容

  • 前端开发者丨http请求 https:www.rokub.com 前言见解有限, 如有描述不当之处, 请帮忙指出,...
    麋鹿_720a阅读 10,896评论 11 31
  • 题目1.什么是同源策略? 同源策略(Same origin Policy): 浏览器出于安全方面的考虑,只允许与本...
    FLYSASA阅读 1,715评论 0 6
  • 什么是跨域 跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实...
    HeroXin阅读 834评论 0 4
  • 什么是跨域 跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实...
    他方l阅读 1,062评论 0 2
  • 昨天凌晨飞机到乌鲁木齐,加上神州租车时晓光没带信用卡,费了一番周折转唔到我名下。路上还被真枪核弹的警察叔叔盘查,折...
    马丁Success阅读 385评论 1 2