同源策略与解决方法
1.浏览器的同源策略
1.1 同源策略
同源策略(same origin policy),一种安全策略,用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。
浏览器默认两个不同的源之间是可以互相访问资源和操作 DOM 的。两个不同的源之间若是想要访问资源或者操作 DOM,就会受到同源策略的制约。
1.2 同源
同源:URL 的协议、主机、端口号相同。
解释一下协议、主机、端口号:
- 协议:协议是定义了数据如何在计算机内和之间进行交换的规则的系统,例如 HTTP、HTTPS。
- 主机:是已连接到一个计算机网络的一台电子计算机或其他设备。网络主机可以向网络上的用户或其他节点提供信息资源、服务和应用。使用 TCP/IP 协议族参与网络的计算机也可称为 IP 主机。
- 端口:主机是计算机到计算机之间的通信,那么端口就是进程到进程之间的通信。
1.3 限制
同源策略的限制:
- DOM 访问限制:DOM 和 Js 对象无法获得。同源策略限制了网页脚本(如 JavaScript)访问其他源的 DOM。这意味着通过脚本无法直接访问跨源页面的 DOM 元素、属性或方法。(防止恶意网站从其他网站窃取敏感信息)
- Web 数据限制:Cookie、LocalStorage 和 IndexDB 无法读写。限制从其他源加载的 Web 数据(例如 XMLHttpRequest 或 Fetch API)。在同源策略下,XMLHttpRequest 或 Fetch 请求只能发送到与当前网页具有相同源的目标。(这有助于防止跨站点请求伪造(CSRF)等攻击)
- 网络通信限制:AJAX 请求不能发送,浏览器会阻止从一个源发出的请求获取来自其他源的响应。这样做是为了确保只有受信任的源能够与服务器进行通信,以避免恶意行为。
2. 跨域解决方案
2.1 ajax 跨域请求方案 jsonp
jsonp(JSON with Padding),是 JSON 的一种 “使用模式”,可以让网页跨域读取数据,其本质是利用 script 标签的开放策略,浏览器传递 callback 参数到后端,后端返回数据时会将 callback 参数作为函数名来包裹数据,从而浏览器就可以跨域请求数据并制定函数来自动处理返回数据。
demo:
var script = document.createElement('script');
script.type = 'text/javascript';
// 传参callback给后端,后端返回时执行这个在前端定义的回调函数
script.src = 'http://a.qq.com/index.php?callback=handleCallback';
document.head.appendChild(script);
// 回调执行函数
function handleCallback(res) {
alert(JSON.stringify(res));
}
优点:
- jsonp 兼容性强,适用于所有浏览器,尤其是 IE10 以下浏览器
缺点:
- 没有关于调用错误的处理
- 只支持 GET 请求,不支持 POST 以及大数据量的请求,也无法拿到相关的返回头,状态码等数据
- callback 参数恶意注入,可能会造成 xss 漏洞
- 无法设置资源访问权限
2.2 跨域资源共享(CORS)
浏览器限制
情景:跨站请求正常发送,但是结果被浏览器拦截。
原因:浏览器将不同域的内容隔离在不同的进程中,网络进程负责下载资源并将其送到渲染进程中,但由于跨域限制,某些资源可能被阻止加载到渲染进程。如果浏览器发现一个跨域响应包含了敏感数据,它可能会阻止脚本访问这些数据,即使网络进程已经获得了这些数据。CORB 的目标是在渲染之前尽早阻止恶意代码获取跨域数据。
CORS
跨源资源共享(Cross-Origin Resource Sharing,CORS)是一种允许在受控的条件下,不同源的网页能够请求和共享资源的机制。
CORS 整个通信过程都是浏览器自动完成,浏览器一旦发现 ajax 请求跨源,就会自动在头信息中增加 Origin 字段,用来说明本次请求来自哪个源(协议+域名+端口)。因此,实现 CORS 通信的关键是服务器,需要服务器配置 Access-Control-Allow-Origin 头信息。
基本思想:
- 服务器在响应中提供一个标头(HTTP 头),指示哪些源被允许访问资源。
- 浏览器在发起跨域请求时会先发送一个预检请求(OPTIONS 请求)到服务器,服务器通过设置适当的 CORS 标头来指定是否允许跨域请求,并指定允许的请求源、方法、标头等信息。
CORS 的请求根据是否会触发 OPTIONS 请求,分为两类:
- 简单请求:不触发 CORS 的请求
- 预检请求:在正式通信之前,增加一次 OPTIONS 查询请求。
简单请求
需要满足的条件:
- HTTP 方法限制:只能使用 GET、HEAD、POST 这三种 HTTP 方法之一。如果请求使用了其他 HTTP 方法,就不再被视为简单请求。
- 自定义标头限制:请求的 HTTP 标头只能是以下几种常见的标头:Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type(仅限于 application/x-www-form-urlencoded、multipart/form-data、text/plain)。HTML 头部 header field 字段:DPR、Download、Save-Data、Viewport-Width、WIdth。如果请求使用了其他标头,同样不再被视为简单请求。
- 请求中没有使用 ReadableStream 对象。
- 不使用自定义请求标头:请求不能包含用户自定义的标头。
- 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问
预检请求
- 预检请求:在正式通信之前,增加一次 OPTIONS 查询请求。服务器通过了 预检请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。
第一个类型为 preflight 就是预检请求:
查看预检请求的请求头数据:
OPTIONS 请求头中的特殊字段:
- ccess-Control-Request-Method:该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法。
- Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段。即告知服务器,实际请求将携带的自定义请求首部字段。
查看预检请求的响应头数据:
OPTIONS 响应头中的特殊字段:
- Access-Control-Allow-Credentials(可选):值是一个布尔值,表示是否允许发送 Cookie。默认情况下,Cookie 不包括在 CORS 请求之中。设为 true,即表示服务器明确许可,前端也需要设置 withCredentials,Cookie 可以包含在请求中,一起发给服务器。这个值也只能设为 true,如果服务器不要浏览器发送 Cookie,删除该字段即可。
- Access-Control-Expose-Headers(可选):CORS 请求时,XMLHttpRequest 对象的 getResponseHeader()方法只能拿到 6 个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在 Access-Control-Expose-Headers 里面指定。
- Access-Control-Allow-Methods:表明服务器允许客户端使用什么方法发起请求
- Access-Control-Allow-Origin:(必须):表示可以请求的数据,可以设置为* 符号,表示允许任意跨源请求。
- Access-Control-Allow-Headers:将实际请求所携带的首部字段告诉服务器
OPTIONS 请求的更多信息,可参考官方文档
优点与缺点
优点:
- 支持所有类型的 HTTP 请求,功能完善。
- 通过 onerror 事件监听进行调用错误处理;
- 通过 Access-Control-Allow-Origin 进行资源访问授权。
缺点:
- 目前主流浏览器(IE10 及以上)都支持 CORS,但 IE8 和 IE9 需要使用 XDomainRequest 对象进行兼容,IE7 及以下浏览器不支持。
2.3 服务器代理
后端进行代理中转请求至服务器端,然后将获取的数据返回给前端。
外网前端页面无法访问内网接口,配置代理接口允许前端页面访问,并中转内网接口,则外网前端页面可以跨域访问内网接口。
比如配置 Nginx,将接收到的请求转发到内网:
server{
#如果是静态文件,直接指向目录
location / {
root html;
index index.html index.htm;
}
# 如果是动态应用,用proxy_pass转发一下
location ~ ^/api/(.*?)$ {
proxy_pass http://127.0.0.1:8080/api/$1?$args;
}
}
优点:
- 前端无需进行任何改变
缺点:
- 后端需要一定工作量
2.4 前端跨域
前端跨域通信是指浏览器中两个不符合同源策略的前端页面进行通信。
2.4.1 document.domain+iframe
仅适用于主域相同,子域不同的前端通信跨域场景。
核心点:
- A 嵌套 B
- 将 document.domain 指向主域名
页面 A:
<!-- A页面 http://a.qq.com/a.html -->
<iframe id="iframe" src="http://b.qq.com/b.html"></iframe>
<script>
document.domain = 'qq.com';
var windowB = document.getElementById('iframe').contentWindow;
alert('B页面的user变量:' + windowB.user);
</script>
页面 B:
<!-- B页面 http://b.qq.com/b.html -->
<script>
document.domain = 'qq.com';
var user = 'saramliu';
</script>
2.4.2 location.hash+iframe
利用 url 的 hash 值改变但不刷新页面的特性,实现简单的前端跨域通信。
受到浏览器安全机制的限制,A 嵌套 B,A 可以修改 B 的 hash 值,但 B 不能修改 A 的 Hash 值,需要一个与 A 同源的页面来中转。
A 页面:
<!-- A页面 http://a.qq.com/a.html -->
<iframe id="iframe" src="http://b.qq1.com/b.html"></iframe>
<script>
// 监听c.html传来的hash值
window.onhashchange = function () {
alert('B页面传递数据:' + location.hash.substring(1));
};
</script>
B 页面:
<!-- B页面 http://b.qq1.com/b.html -->
<iframe id="iframe" src="http://a.qq.com/c.html"></iframe>
<script>
// 向c.html传递hash值
var iframe = document.getElementById('iframe');
setTimeout(function () {
iframe.src = iframe.src + '#user=saramliu';
}, 1000);
</script>
中转页面:
<!-- C页面 http://a.qq.com/c.html -->
<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 操作同域a.html的hash值,传递数据
window.parent.parent.location.hash = window.location.hash.substring(1);
};
</script>
优点:
- 可以解决主域不同的前端通信跨域问题。
- hash 改变,页面不会刷新。
缺点:
- 受部分浏览器安全机制限制,需要额外的同源中转页面,且中转页面需要 js 逻辑来修改 hash 值。
- 通信数据类型及长度均受限,且数据外显在 url 上,存在一定安全风险。
2.4.3 window.name+iframe
window.name 属性在不同页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
A 嵌套 B,B 将要传递的数据附加到 window.name 上,然后跳转到与 A 同域名的 C,A 和 C 满足同源策略,A 可与获取到 C 的 window.name。
A
<!-- A页面 http://a.qq.com/a.html -->
<iframe id="iframe" src="http://b.qq1.com/b.html"></iframe>
<script>
var state = 0;
var iframe = document.getElementById('iframe');
iframe.onload = function () {
if (state === 1) {
// 第2次onload成功后,读取同域window.name中数据
alert(iframe.contentWindow.name);
} else if (state === 0) {
// 第1次onload成功后
state = 1;
}
};
</script>
B:
<!-- B页面 http://b.qq1.com/b.html -->
<script>
window.name = '这里是B页面!';
window.location = 'http://a.qq.com/c.html'; // 跳转到与A同源的C
</script>
优点:
- 通信数据类型不受限,且长度可达 2MB。
缺点:
- 需要额外的同源中转页面,但中转页可以为空白页。
2.4.4 postMessage
postMessage 是 HTML5 XMLHttpRequest Level2 中的 API,是一种安全的跨域通信方法,且是为数不多可以跨域操作的 window 属性之一,它通常用于解决以下方面的问题:
- 页面和其打开的新窗口的数据传递。
- 多窗口之间消息传递。
- 页面与嵌套 iframe 消息传递。
A 获得 B 的 window 对象后,A 调用 postMessage 方法发送一个个 MessageEvent 消息。B 通过监听 message 事件即可获取 A 传递的数据。
A:
<iframe id="iframe" src="http://b.qq1.com/b.html"></iframe>
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function () {
var data = { meesage: '这里是A页面发的消息' };
var url = 'http://b.qq1.com/b.html'; // 向B页面发送消息
iframe.contentWindow.postMessage(JSON.stringify(data), url);
};
window.addEventListener('message', function (e) {
alert('B页面发来消息:' + JSON.parse(e.data));
});
</script>
B:
<!-- B页面 http://b.qq1.com/b.html -->
<script>
window.addEventListener(
'message',
function (e) {
alert('A页面发来消息:' + JSON.parse(e.data));
var data = { meesage: '这里是B页面发的消息' };
var url = 'http://a.qq.com/a.html';
window.parent.postMessage(JSON.stringify(data), url);
},
false
);
</script>
优点:
- 可以解决多种类型的前端跨域通信问题。
缺点:
- 兼容性方面相对差一点,IE8 及以下浏览器不支持该方法,IE9 只支持 postMessage 传递 string 类型的数据,而标准的 postMessage 消息数据可以是任何类型。
参考文档
- 同源策略(same origin policy)