作者:campcc
来源:
今天我们来聊一个老生常谈的话题,跨域!又是跨域,烦不烦 ?网上跨域的文章那么多,跨的我眼睛都疲劳了,不看了不看了别走...我尽量用最简单的方式将常见的几种跨域解决方案给大家阐释清楚,相信认真看完本文,以后不管是作为受试者还是面试官,对于这块的知识都能够游刃有余。
什么是“跨源”
不是讲跨域吗 ?怎么又来个“跨源” ?字都能打错的 ?...稍安勿躁,其实我们平常说的跨域是一种狭义的请求场景,简单来说,就是“跨“过浏览器的同源策略[1]去请求资“源”,所以我们叫它“跨源”也没啥问题。那么,跨源,源是什么?浏览器的同源策略什么是同源?协议,域名,端口都相同就是同源干巴巴的,能不能举个栗子?栗子:),有的有的:
Cookie,LocalStorage,IndexDB 等存储性内容无法读取
DOM 节点无法访问
Ajax 请求发出去了,但是响应被浏览器拦截了
我就想请求个东西,至于吗,为什么要搞个这么个东西限制我?基于安全考虑,没有它,你可能会遇到:
Cookie劫持,被恶意网站窃取数据
更容易受到 XSS,CSRF 攻击
无法隔离潜在恶意文件
... ...
如果您正在学习Spring Boot,推荐一个连载多年还在继续更新的免费教程:
所以,得有。正是因为浏览器同源策略的存在,你的 Ajax 请求有可能在发出去后就被拦截了,它还会给你报个错:
这种发出去拿不到响应的感受,就像你在网上冲浪时,被一股神秘的东方力量限制了一样:
非常难受,所以,我们接下来就来看看怎么用科学的方法上网(啊呸,科学的方法解决跨域的问题)。
JSONP
这玩意儿就是利用了标签的src 属性没有跨域限制的漏洞,让我们可以得到从其他来源动态产生的 JSON 数据。为什么叫 JSONP ?JSONP 是 JSON with Padding 的缩写,额,至于为什么叫这个名字,我网上找了下也没个标准的解释,还望评论区的各位老哥知道的赶紧告诉我: )怎么实现 ?具体实现思路大致分为以下步骤:
本站的脚本创建一个 元素,src 地址指向跨域请求数据的服务器
提供一个回调函数来接受数据,函数名可以通过地址参数传递进行约定
服务器收到请求后,返回一个包装了 JSON 数据的响应字符串,类似这样:callback({...})
如果您正在学习Spring Boot,推荐一个连载多年还在继续更新的免费教程:
浏览器接受响应后就会去执行回调函数 callback,传递解析后的 JSON 对象作为参数,这样我们就可以在 callback 里处理数据了。实际开发中,会遇到回调函数名相同的情况,可以简单封装一个 JSONP 函数:
我们用Promise 封装了请求,使异步回调更加优雅,但是别看楼上的洋洋洒洒写了一大段,其实本质上就是:
CORS
CORS[4](Cross-Origin Resource Sharing)的全称叫跨域资源共享,名称好高大上,别怕,这玩意儿其实就是一种机制。浏览器不是有同源策略呐,这东西好是好,但是对于开发人员来说就不怎么友好了,因为我们可能经常需要发起一个跨域 HTTP 请求。我们之前说过,跨域的请求其实是发出去了的,只不过被浏览器给拦截了,因为不安全,说直白点儿就是,你想要从服务器哪儿拿个东西,但是没有经过人家允许啊。所以怎么样才安全 ?服务器允许了不就安全了,这就是 CORS 实现的原理:使用额外的 HTTP 头来告诉浏览器,让运行在某一个 origin 上的 Web 应用允许访问来自不同源服务器上的指定的资源。
兼容性
实现原理
如果您正在学习Spring Boot,推荐一个连载多年还在继续更新的免费教程:
CORS 需要浏览器和服务器同时支持,整个 CORS 的通信过程,都是浏览器自动完成。怎么个自动法?简单来说,浏览器一旦发现请求是一个跨域请求,首先会判断请求的类型,如果是简单请求,会在请求头中增加一个Origin字段,表示这次请求是来自哪一个源。而服务器接受到请求后,会返回一个响应,响应头中会包含一个叫Access-Control-Allow-Origin的字段,它的值要么包含由 Origin 首部字段所指明的域名,要么是一个 "*",表示接受任意域名的请求。如果响应头中没有这个字段,就说明当前源不在服务器的许可范围内,浏览器就会报错:
如果是非简单请求,会在正式通信之前,发送一个预检请求(preflight),目的在于询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段,只有得到肯定答复,浏览器才会发出正式的请求,否则就报错。你可能发现我们在日常的开发中,会看到很多使用 OPTION 方法发起的请求,它其实就是一个预检请求:
那么到底哪些是简单请求,哪些是非简单请求 ?
请求类型
不会触发 CORS 预检的,就是简单请求。哪些请求不会触发预检 ?使用以下方法之一:GET, HEAD, POST,并且Content-Type的值仅限于下列三者之一:
text/plain
multipart/form-data
application/x-www-form-urlencoded
相反,不符合上述条件的就是非简单请求啦。所以,实现 CORS 的关键是服务器,只要服务器实现了 CORS 的相关接口,就可以实现跨域。CORS 与 JSONP相比,优势是支持所有的请求方法,缺点是兼容性上较 JSONP 差。除了 JSONP 和 CORS 外,还有一种常用的跨域解决方案:PostMessage,它更多地用于窗口间的消息传递。
PostMessage
PostMessage 是 Html5 XMLHttpRequest Level 2 中的 API,它可以实现跨文档通信(Cross-document messaging)。兼容性上,IE8+,Chrome,Firfox 等主流浏览器都支持,可以放心用,如何理解跨文档通信?你可以类比设计模式中的发布-订阅模式,在这里,一个窗口发送消息,另一个窗口接受消息,之所以说类似发布-订阅模式,而不是观察者模式,是因为这里两个窗口间没有直接通信,而是通过浏览器这个第三方平台。
postMessage 方法接收三个参数,要发送的消息,接收消息的源和一个可选的 Transferable 对象,如何接收消息 ?
Websocket
Websocket 是 HTML5 的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。什么是全双工通信 ?简单来说,就是在建立连接之后,server 与 client 都能主动向对方发送或接收数据。原生的 WebSocket API 使用起来不太方便,我们一般会选择自己封装一个 Websocket(嗯,我们团队也自己封了一个 : ))或者使用已有的第三方库,我们这里以第三方库ws[7] 为例:
需要注意的是,Websocket 属于长连接,在一个页面建立多个 Websocket 连接可能会导致性能问题。
如果您正在学习Spring Boot,推荐一个连载多年还在继续更新的免费教程:
Nginx 反向代理
我们知道同源策略限制的是:浏览器向服务器发送跨域请求需要遵循的标准,那如果是服务器向服务器发送跨域请求呢?答案当然是,不受浏览器的同源策略限制。利用这个思路,我们就可以搭建一个代理服务器,接受客户端请求,然后将请求转发给服务器,拿到响应后,再将响应转发给客户端:
这就是 Nginx 反向代理的原理,只需要简单配置就可以实现跨域:
Node 中间件代理
另外,如果您正在学习Spring Cloud,推荐一个连载多年还在继续更新的免费教程:
实现的原理和我们前文提到的代理服务器原理如出一辙,只不过这里使用了 Node 中间件做为代理。需要注意的是,浏览器向代理服务器请求时仍然遵循同源策略,别忘了在 Node 层通过 CORS 做跨域处理:
document.domain
另外,如果您正在学习Spring Cloud,推荐一个连载多年还在继续更新的免费教程:
二级域名相同的情况下,设置 document.domain 就可以实现跨域。什么是二级域名 ?a.test.com 和 b.test.com 就属于二级域名,它们都是 test.com 的子域。如何实现跨域 ?
总结
当然,除了上述的方案外,比较Hack的还有:window.name, location.hash,但是这些跨域的方式现在我们已经不推荐了,为什么 ?因为相比之下有更加安全和强大的 PostMessage 作为替代。跨域的方案其实有很多,总结下来:
CORS 支持所有的 HTTP 请求,是跨域最主流的方案
JSONP 只支持 GET 请求,但是可以兼容老式浏览器
Node 中间件和 Nginx 反向代理都是利用了服务器对服务器没有同源策略限制
Websocket 也是一种跨域的解决方案
PostMessage 可以实现跨文档通信,更多地用于窗口通信
document.domain, window.name, location.hash 逐渐淡出历史舞台,作为替代 PostMessage 是一种不错的方案
写在最后
本文首发于我的博客[8],才疏学浅,难免有错误,文章有误之处还望不吝指正!如果有疑问或者发现错误,可以在相应的 issues 进行提问或勘误如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励(完)
参考资料
[1] [2] [3]?editors=1000 [4] [5]#search=CORS [6]#heading-9 [7] [8]
往期推荐
Elasticsearch 快速检索的秘诀
同事把实数作为 HashMap 的key,领导心态崩了
SpringBoot 实现万能文件在线预览
开源分布式定时任务框架技术选型
基于Elasticsearch 实现站内全文搜索
2.3W字,这可能是把Nginx讲得最全面的一篇文章了,建议收藏备用
Java 阻塞队列实现原理分析
回复【干货】获取精选干货视频教程
回复【加群】加入疑难问题攻坚交流群
回复【mat】获取内存溢出问题分析详细文档教程
回复【赚钱】获取用java写一个能赚钱的机器人
回复【副业】获取程序员副业攻略一份
好文请点赞+分享