<!DOCTYPE html>
<title>Home</title>
</head>
<h1>Home</h1>
</div>
<iframe id="iframe" src="http://127.0.0.1:8082/crossOrigin/detail.html" height="200" width="300"></iframe>
<script>
;(function() {
var iframe = document.getElementById('iframe');
debugger
var title = iframe.contentWindow.document.title;
console.log(`title=${title}`);
})();
</script>
</body>
</html>
<!DOCTYPE html>
<title>Home</title>
</head>
<h1>Home</h1>
<button id="exchange">Set window.name</button>
<a href="http://127.0.0.1:8082/crossOrigin/detail.html">Detail</a>
</div>
<!-- <iframe id="iframe" src="http://127.0.0.1:8082/crossOrigin/detail.html" height="200" width="300"></iframe> -->
<script>
;(function() {
document.getElementById('exchange').onclick = () => {
window.name = `Home: ${Math.random()}`
})();
</script>
</body>
</html>
<!DOCTYPE html>
<title>Detail</title>
</head>
<h1>Detail</h1>
</div>
<script>
;(() => {
console.log(`detail: ${window.name}`);
})()
</script>
</body>
</html>
function isPrivateModel() {
var testKey = "TEST_PRIVATE_MODEL_KEY";
var valueExpire = "TEST_PRIVATE_MODEL_VALUE";
var valueActual;
var storage = window.localStorage;
try {
storage.setItem(testKey, valueExpire);
valueActual = storage.getItem(testKey);
storage.removeItem(testKey);
} catch(e){
// QuotaExceededError: DOM Exception 22
return true;
//UC隐私模式下testValue !== value
return valueActual !== valueExpire;
只能保存字符串数据;
一般是把对象转成JSON字符串保存。
文档和内嵌iframe
之前无法使用window.name
进行通信,因为两个文档的全局对象window
是不同的。
HTML5引入的API专门用于解决跨域文档之间的通讯,本质是跨域文档之间的window
对象之间通讯:
enables cross-origin communication between Window objects; e.g., between a page and a pop-up that it spawned, or between a page and an iframe embedded within it.
只适用于:
文档和文档内嵌iframe
;
HTMLIFrameElement.contentWindow
window.parent
文档和文档打开的文档。
window.open
window.opener
// 发消息
targetWindow.postMessage(message, targetOrigin, [transfer]);
// 收消息
targetWindow.addEventListener("message", (event) => {
// ...
}, false);
postMessage
是唯一可以跨域文档访问的API(必须得特殊处理,要不然怎么通讯呢)。
传递的数据利用的是 结构化克隆算法复制数据。
并不是任意数据都可以传递的,具体见Things that don't work with structured clone
DOM对象;
正则对象的lastIndex
属性。
还可以利用结构化克隆算法实现深拷贝
targetOrigin
指定目标window的origin
。
targetOrigin
除了可以是origin
外还可以是个URL,此时浏览器自动获取URL里的origin
(不是直接使用URL匹配)。
targetOrigin
默认值通配符"*",只有同域时使用通配符才能正常发消息;
跨域时只有当targetOrigin
指定的origin
和当目标window的origin
匹配时(大小写不敏感)才能发消息;
收消息的时候最好在事件处理函数里增加origin
白名单,即只处理白名单origin
发的消息。
window.addEventListener("message", (event) => {
if (event.origin !== "http://example.org:8080")
return;
// ...
}, false);
代表一个能在不同可执行上下文之间,列如主线程和Worker
之间,相互传递的对象。
主要用于管道通讯中转移MessagePort
等。
比较重要的属性:
origin
source
消息发送者对象。可以是window, ServiceWorder, MessagePort。
ports
MessageChannel
是Window.postMessage()
背后的原理。
利用MessageChannel
可以自定义管道通讯。
MessagePort
必须调用start
方法才能发消息;
通过MessagePort.onmessage
绑定事件时会内部调用start
方法。利用EventTarget.addEventListener
绑定事件需要手动调用start
方法。
window.postMessage
内部也利用MessageChannel
通讯,不过增加了origin
的判断。
window.postMessage
和MessageChannel
除了用于管道通讯外,还有一些其他使用场景。
作为setImmediate
的polyfill
四、客户端和跨域服务端通讯
4.1 jsonp
4.3 CORS
一、引入背景
浏览器端JS中的http请求(XMLHttpRequest
/fetch
)受同源策略限制。但是这也导致有些合理的请求也被限制了。W3C提出了新的标准CORS来解决这个问题。
CORS机制让服务端决定是否准许跨域请求(当然了服务端也要承担确保安全的职责)。
除了XMLHttpRequest
/fetch
还有其他资源请求可以使用CORS:
Web Fonts (for cross-domain font usage in @font-face within CSS)
WebGL textures
Images/video frames drawn to a canvas using drawImage()
CSS Shapes from images
二、CORS原理
21 浏览器和服务端的谈判
服务端:hi,我说你管的也太多了!我认为request A是安全的,你怎么不发给我?
浏览器:我怎么知道request A是安全。为了安全起见,我不能发给你。
服务端:瞎子都能看出来reques是安全的。你个SB。
浏览器:你才SB
服务端:你SB
......
浏览器:咱天天这样吵也不是事啊。咱们各退一步。
服务端:怎么?
浏览器:这样吧,如果是跨域请求,我先咨询下你,如果你觉得请求安全,我再把真实请求发给你。(
Origin
,
Access-Control-Allow-Origin
)
服务器:恩,好吧。不过你每次都先咨询我,对我的性能会造成影响啊,再说了有些请求不存在安全问题。
浏览器:也是啊。这样吧,对于那些
安全的请求 ->戳<-,我直接发给你。
服务器:这个定义确实OK,但也太苛刻了,实际应用中很少遇到啊,这样对性能的提升没有实际解决。
浏览器:但是
确保安全是我底线。这个没得让步。
服务器:要不这样,你把预检的结果缓存一段时间,在缓存时间内不用再发送预检请求。
浏览器:好想法,就这样干。不过你得告诉我缓存多久。
服务器:不过你记得把
Cookie
带给我,要不然我就变成瞎子了(
Access-Control-Max-Age
)。
浏览器:
Cookie
太私密了,我不能随便给你。让小主(前端开发)自己决定吧,小主命令我携带
Cookie
,我就携带。
服务器:可以的,不过万一的你的小主是个坏人怎么办?
浏览器:.....,这样把你也告诉我你的小主(后端开发)是否需要
Cookie
。两位小主都明确需要
Cookie
时我再携带Cookie.(
Access-Control-Allow-Credentials
)。
服务器:好吧。毕竟只有小主们知道他们是否真的需要
Cookie
。
浏览器:不过我得提醒你,预检请求我是不会携带
Cookie
的。
服务器:好吧,毕竟*确保安全也是我底线**。
2.2 简单请求
不会对服务端数据产生副作用的HTTP请求视为简单请求。具体规则:符合一定条件的请求
2.2.1 简单请求处理流程
浏览器会直接发生真实请求。具体步骤:
浏览器:在请求头部中添加Origin
头,如果XMLHttpRequest
对象的withCredentials
属性为true
, 把请求域的cookie信息添加到请求头中。
服务器:在响应头部中添加Access-Control-Allow-Origin
头
浏览器:读取响应的Access-Control-Allow-Origin
头取值。如果为"*"或者和Origin
取值相等,则通过,否则报错XMLHttpRequest
对象onError
捕获。
2.2.2 携带Cookie
跨域请求默认不携带Cookie
(HTTP认证信息),需要开发显示的告诉浏览器传递;
如设置XMLHttpRequest
的withCredentials=true
如果浏览器要携带Cookie
,则响应必须携带``Access-Control-Allow-Credentials: trueheader,并且
Access-Control-Allow-Origin`取值不能是通配符"*",必须是指定的源 。
Access to XMLHttpRequest at 'http://localhost:3000/' from origin 'http://localhost:8082' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
是否携带Cookie是由前端开发决定的,服务端只是决定是否需要Cookie
;
其实请求里已经携带Cookie
了,后端也可以取到Cookie
,只不过客户端无法获取到返回值。
如果响应有Set-Cookie
header也是被忽略的。
2.2.3 总结
服务端处理请求时,如果请求源是在白名单中,则应该只返回该源。不要返回"*"。
浏览器在匹配Origin
和Access-Control-Allow-Origin
时,只是简单的字符串匹配,大小写是敏感的。
感觉这个有点坑,比较URL是大小不敏感的。估计是浏览器是明确开发明白自己做的事情。
2.3 预检(Preflight)
如果跨域请求不是简单请求,则浏览器先给服务端发送个OPTIONS
请求用于预检。由服务端告诉浏览器是否准许真实请求跨域,如果准许则浏览器再发送真实的请求(走简单请求的流程),否则报错。这个过程就是预检过程。
2.3.1 预检请求携带的内容
预检请求必须是个简单请求;
预检请求不能携带数据(HTTP Body),也不能携带Cookie
等认证信息;
预检请求需要携带真实请求的信息:
可能引发副作用的Http Method
可能引发副作用的Http Headers
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.withCredentials = true;
xhr.setRequestHeader('Content-Type', 'text/html');
xhr.setRequestHeader('x-page-id', '123456');
xhr.onreadystatechange = function() {
console.log(`xhr.readyState=${xhr.readyState}`)
xhr.send(url);
预检请求:
OPTIONS http://localhost:3000/ HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Access-Control-Request-Method: GET
Access-Control-Request-Headers: content-type,x-page-id
Origin: http://localhost:8082
预检过程不需要开发做任何事情:
Access-Control-Request-Method
和Access-Control-Request-Headers
的添加已经取值都是由浏览器自行完成。不准许开发介入。
2.3.2 预检请求的响应
浏览器只会认CORS相关的headers,响应的其他数据都会被忽。所以对于预检请求的响应最好不要携带:
Cookie;
响应Body。
2.3.3 真实请求
预检通过后浏览器就向服务端发送真实请求了。
和预检请求区别
真实请求的request不携带Access-Control-Request-Method
和Access-Control-Request-Headers
头部了;
真实请求的request可以携带Cookie
和数据了;
真实请求的response不携带Access-Control-Allow-Method
和Access-Control-Allow-Headers
头部。
和简单请求处理区别
没有区别,处理逻辑是一样的。
这也要求真实请求的响应也必须携带Access-Control-Allow-Origin
,并且如要带Cookie
也得携带Access-Control-Allow-Credentials: true
头部等,以cors源码:
2.4 request headers
1. Origin
:真实请求的源。
跨域的原因就是Origin
的不同,所以一定要携带Origin
信息的。
为啥不叫Access-Control-Request-Origin
?Origin
除了用于CORS
外还有其他用处?
2. Access-Control-Request-Headers
代表真实请求用户设置的headers(比如自定义的头部),多个用逗号隔开。
3. Access-Control-Request-Method
:
预检请求是OPTIONS,浏览器利用Access-Control-Request-Method
上送真实请求的Method。
上面三个header都是浏览器自动检测处理,无需前端开发手动设置。
本质上开发也不能设置,防止欺骗服务端。
2.5 response headers
1. Access-Control-Allow-Origin
准许请求的源
2. Access-Control-Allow-Headers
准许请求的自定义头部名称,多个用逗号隔开;
大小写不敏感。
3. Access-Control-Allow-Methods
准许请求的method,多个用逗号隔开;
大小写敏感(全大写)。
4. Access-Control-Allow-Credentials
表示是否准许真实请求发送Cookie信息,true
表示准许发送Cookie信息(至于发不发要看客户端),false
表示不准发送Cookie,如果客户端打算发送,则报错。
5. Access-Control-Max-Age
指定浏览器缓存预检请求的时间(单位s)。
这个时间不是任意设置的,每个浏览器都有最大缓存时间,并且不同浏览器还不相同(如Chromium 最大5min)。
浏览器也有默认的缓存时间,并且不同浏览器还不相同(如Chromium 默认5s)。所以大部分情况下可以不显式的设置这个值。
但是要留意个问题,浏览器缓存预检请求时以什么标准判断两个预检请求是否相同呢?
Origin
, Access-Control-Request-Headers
, Access-Control-Request-Method
三个头部相同的预检请求视为相同的预检。如果被缓存过,则在缓存时间内不会发送预检。
6. Access-Control-Expose-Headers
默认情况下跨域可以获取到跨域响应的headers只有Content-Type
和Content-Length
。服务端可以利用Access-Control-Expose-Headers
头部指定哪些headers可以被客户端访问。
// 服务端:
res.setHeader('x-page-id', 'abc')
res.setHeader('x-pagetrace', 'hello')
res.setHeader('Access-Control-Expose-Headers', 'x-page-id');
在/客户端xhr.getAllResponseHeaders()
返回的值:
"content-length: 11
content-type: text/html; charset=utf-8
x-page-id: abc
无法获取到x-pagetrace
。
2.6 CORS流程图
从图中注意几点:
预检请求就是比简单请求多了一步预检过程,预检通过后发送的真实请求是走真实请求的逻辑;
Access-Control-Allow-Orgin
, Access-Control-Allow-Credentials
可能会被判定两次(预检请求,真实请求)。
2.7 优缺点
解决XMLHttpRequest跨域请求的最终方案,可以支持各种类型的请求。
兼容性不好,有些浏览器不支持CORS机制(见MDN,PC&Mobile)。
三、CORS-浏览器
3.1 浏览器做的事情
在CORS机制里大部分事情是浏览器自动处理的,
是否跨域检查
是否需要预检
发生预检请求,CORS相关Header信息
检查预检请求
发生真实请求
3.2 需要前端开发做的事情
在CORS机制里大部分事情是浏览器自动处理的,只有一件事情需要开发辅助处理,即是否需要携带身份凭证(cookie
,HTTP 认证信息发送身份凭证)。
四、CORS-服务端
要实现CORS机制离不开服务端的配合。为了更好的实现支持CORS服务接口,需要注意** 预检request会请求服务两次**,在处理预检过程中不要做真实请求的逻辑处理。
// 这个就不是很好的服务接口代码(预检请求中才处理的真实逻辑)
public string CORS_Preflight(int accessControl)
this.Response.AddHeader("Access-Control-Allow-Origin", "http://qyao.com");
this.Response.AddHeader("Access-Control-Allow-Credentials", "true");
this.Response.AddHeader("Access-Control-Allow-Headers", "X-PINGOTHer");
this.Response.AddHeader("Access-Control-Allow-Methods", "POST");
this.Response.AddHeader("Access-Control-Max-Age", (5 * 60).ToString());
string result = "<p>Hello</p>";
return result;
改成这样:
public string CORS_Preflight(int accessControl)
string result = string.Empty;
this.Response.AddHeader("Access-Control-Allow-Origin", "http://qyao.com");
this.Response.AddHeader("Access-Control-Allow-Credentials", "true");
if (this.Request.HttpMethod == "OPTIONS") // 预检请求
this.Response.AddHeader("Access-Control-Allow-Headers", "X-PINGOTHer");
this.Response.AddHeader("Access-Control-Allow-Methods", "POST");
this.Response.AddHeader("Access-Control-Max-Age", (5*60).ToString());
result = "<p>Hello</p>"; // 真实请求逻辑
return result;
内部依赖npm vary,HTTP Vary
这么重要吗?
重要啊,会影响客户端缓存决策,见:
HTTP 协议中 Vary 的一些研究
HTTP请求的响应头部Vary的理解
CORS里关于Access-Control-Allow-Origin
也有段描述:
如果服务端指定了具体的域名而非“*”,那么响应首部中的 Vary 字段的值必须包含 Origin。这将告诉客户端:服务器对不同的源站返回不同的内容
默认情况不设置Access-Control-Max-Age
, 即采用浏览器默认的。
MDN Cross-Origin Resource Sharing (CORS)