前言:跨域涉及内容较多,也是面试必备知识。本文希望将跨域的技术点都串联起来,形成一个知识体系。
1.什么是跨域?
跨域定义:协议(http和https)、域名、端口号任意一个不同,都属于跨域。
构成:<scheme>://<netloc>:<port>/<path>?<query>#<fragment>
解释:协议://域名:端口号/路径
https://www.foo.com/page?name=123#card
:不同源,协议不同
http://www.foo.com:8081/page?name=123#card
:不同源,端口号不同
http://foo.com/page?name=123#card
:不同源,域名不同
http://aaa.foo.com/page?name=123#card
:不同源,域名不同
http://www.aaa.com/page?name=123#card
:不同源,域名不同http://www.foo.cn/page?name=123#card
:不同源,域名不同
2.为什么要设置跨域?
因为跨域的存在,我们需要额外配置很多信息,比如http
请求中的headers
,尤其是做低版本浏览器(如IE8、IE9)兼容时,跨域是一件很麻烦的事情。所以当初的设计者为什么要设置跨域呢?
以下摘抄自MDN:
出于安全原因,浏览器限制了从脚本启动的跨域HTTP请求。
那如果没有跨域的话,会导致什么问题呢?
-
DOM同源策略:如果iframe之间可以跨域访问,会出现什么问题呢?
- 做一个假网站aaa.com,用iframe嵌套一个银行网站
https://www.bank.com
- 将
iframe
最大化,做成和银行网站一样的样式 - 用户点进假网站
aaa.com
后,输入用户名、密码 - 假网站就可以跨域访问到
https://www.bank.com
的input
输入框,拿到用户名和密码,个人信息就完全被泄露了
- 做一个假网站aaa.com,用iframe嵌套一个银行网站
-
AJAX请求同源策略:如果所有请求之间,没有同源策略限制,会发生什么?
- 用户登录某银行网站
http://www.aaa.com
后,网站A会向用户的cookie添加一个唯一id标识 - 然后,用户又打开了一个网站B
http://www.bbb.com
,执行了网站里的恶意代码,向网站A发起了ajax
请求,请求会默认将用户的cookie
带过去 - 此时A网站核实用户信息无误,
response
返回用户数据,数据就会被泄露
- 用户登录某银行网站
现在就能理解MDN中的那句话了:同源策略让用户信息更安全。
非同源情况下,会有三种行为受到限制:
AJAX
请求
DOM
访问
cookie、localStorage、IndexDB
无法获取
AJAX请求的跨域解决方案
- 图片资源
- 图片资源不受跨域限制。
- 方法:监听onload和onerror,确定是否接收到了响应。
- 实现:动态创建
<img>
,在onload和onerror方法中监听数据,判断处理请求是否成功 - 用途:跟踪用户点击页面或动态广告曝光次数。但只能发送get请求,无法访问服务器的responseText,只能用于服务器与浏览器的单向通信。
- JSONP
- 原理:script和img元素,都能不受限制的从其他域加载资源。
- 实现:动态创建
<script>
标签,添加src
属性url?callback=funcName
- 服务器端收到请求后, 会把数据放在
callback
函数的参数位置中返回,由于是<script>
请求的脚本,因此可以直接运行。 - 返回参数的JSON数据被视为JavaScript对象,而不是字符串,因此避免了使用JSON.parse的步骤。
- 缺点:从其他域加载代码,可能会造成安全问题,无法确定请求是否失败。 且只能发送
GET
请求。
// 动态创建<script>标签
function handleCallback(res) {
console.log(res);
}
var script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.src = 'url?callback=handleCallback';
document.body.appendChild(script);
- CORS
- 解决跨域AJAX请求的根本方法。需要浏览器和服务器同时支持,IE>=10版本。
- 原理:用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求响应的结构。给一个请求附加一个额外的origin头部,服务器返回的Access-Control-Allow-Origin头部中是否包含请求域或为
*
,则请求允许,否则不允许。 - 备注:跨域请求和响应都不包含cookie。
- 优点:可以发送任何形式的请求,GET \ POST等。而JSONP只能发送GET请求。相比XMLHttpRequest请求,CORS通信与同源的AJAX通信没有差别,代码完全一样。
- CORS通信分为简单请求和复杂请求
- 简单请求
- 仅限:HEAD \ GET \ POST方法
- 只包含以下头信息的请求:
ACCEPT 、 ACCEPT-Language 、Content-Language 、 Last-Event-ID
Content-Tyype
只能是以下几种类型:application/x-www/form-urlencoded \ multipart/form-datat\ text/plain
- 简单的跨域AJAX请求,仅仅在头信息部分添加一个origin
- 若回应的源信息,没有包含Access-Control-Allow-Origin字段,则说明请求出错。
- 若请求的源,在服务器的允许范围内,返回的信息会增加几个头字段
- Access-Control-Allow-Origin: url
- Access-Control-Allow-Credenttials: true /false表示cookie可以包含在请求中,一起发送给服务器
- Access-Control-Expose-Headers: 使CORS请求时,可以通过getResponseHeader()方法拿到的字段
- 若要在请求时发送cookie和HTTP认证信息,需要客户端和服务器端都同意
- 服务器端,Access-Control-Allow-Credenttials: true
- 客户端:xhr.withCredentials = true。
- 若要发送cookie,Access-Control-Allow-Origin不能设置为*,必须指定明确的、与请求网页一致的域名
- 非简单请求
- 请求方法为:
PUT/DELETE/PATCH
,Content-Type
不属于下面之一的application/x-www/form-urlencoded \ multipart/form-datat\ text/plain
请求, - 在正常的HTTP请求之前,添加一个“预检查”请求
- 预检查请求,方法是OPTIONS,询问服务器,当前域是否在服务器的许可名单之中,以及可以使用那些HTTP动词和头部字段信息,
- Access-Control-Request-Method
- 浏览器的CORS请求会用到哪些方法
- Access-Control-Request-Headers
- 指定浏览器的CORS请求会发送的额外的头信息字段
- 预检请求成功以后,会回应一串信息,包含CORS相关的头部信息。
- 如果不被允许,会返回一个正常的HTTP回应,并且不包含任何的头部信息。触发AJAX请求的onError()函数
Access-Control-Max-Age
:本次预检查请求的有效期。可用来缓存OPTIONS请求,单位是秒,如果为-1,则表示禁用缓存。
- 请求方法为:
- CORS的兼容性
- IE10+实现CORS是使用
XMLHttpRequest
,但IE8、IE9
实现CORS
,是使用XDomainRequest
。 XDomainRequest
只支持get和post请求,不支持携带cookie,不允许设置自定义headers
(不管是否跨域!),response
中没有status code
。Content-type
只能是text/plain
,不支持常用的application/json
!!!
- IE10+实现CORS是使用
DOM访问的跨域解决方案
- DOM跨域:比如网页中嵌入的Iframe,无法通过document.getElementById(),得到Iframe中的DOM。
- 若两个窗口一级域名相同,二级域名不同时,可以统一设置document.domain属性,让两个网站同源。
- 完全不同源的网站--解决办法:
- 片段识别符(fragment identifier)--#
- 改变url #后面的值不会刷新页面,可以利用hashchange监听事件
- 父窗口可以把信息写入子窗口的片段识别符,子窗口利用onhashchange监听变化
- window.name
- 浏览器窗口下的window.name--可以由同一个窗口下的所有网页读取,无论是否同源
- 但必须监听子窗口window.name属性的变化,会影响性能
- 跨文档通信API(Cross-document messaging)
- HTML5新引入的API---PostMessage:可用于web worker 通信,实现主线程和子线程的通信;也允许来自不同源的脚本,采用异步方式通信;可实现跨文档、多窗口、跨域消息通信。
- 也可以利用postMessage,监听其他窗口的localStorage
- 提供方法:
postMessage(data, origin)
发送消息,监听message
事件
window.addEventListener(‘message’, function(e) { console.log(e); // e.data:接收到的数据 // e.souce:发送消息的窗口 // e.origin:消息发向的网址 * }, false)
- 片段识别符(fragment identifier)--#
cookie跨域的解决方案
- 一般用于保存用户登录状态,非同源网页不能共享
- 两个网页一级域名相同,二级域名不同时,可通过设置相同的document.domain,共享cookie。
- 另外服务器在Set-cookie时,可以直接设置domain为一级域名 domain=.example.com,这样二级和三级域名,不用做任何设置,都可以读取这个cookie。
localStorage、sessionStorage跨域的解决方案
localStorage
:生命周期是永久,进程被kill
后,缓存依然存在,除非手动清除缓存。大小为5MB
,仅在客户端被保存,不参与和服务端通信。- 跨域:只要不同源就不能共享
localStorage
的数据。 - 跨域解决方法:
postMessage()
通信、二级域名共享
- 跨域:只要不同源就不能共享
- localStorage跨域——二级域名共享解决办法
- 假设顶级域名为
abc.com
,二级域名为www.abc.com
,还有其他n级域名或者多个二级域名。 - 实现方法:输出一个隐藏的
iframe
加载一个顶级域名的代理页面,统一在顶级域名设置localStorage
,其他二级域名或者n级域名需要读取或者设置localStorage
时通过此代理ifarme
来执行操作。 - 核心:
iframe
和当前页面都需要设置document.domain='abc.com'
,这样就可以相互操作对方了。
- 假设顶级域名为
sessionStorage
:关闭页面或关闭浏览器后,缓存消失。大小约为5MB,仅在客户端被保存,不参与和服务端通信。- 不同源,或同源但不同页面间无法共享
sessionStorage
的信息。 - 如果一个页面包含多个iframe且他们属于同源页面,那么他们之间是可以共享
sessionStorage
的。 - 跨域解决方法:
sessionStorage
生命周期比较特殊,需要用html5
的的postMessage
来实现,无法使用document.domain
。
- 不同源,或同源但不同页面间无法共享