添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement . We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account
  • 跨文档DOM API访问
    JS中可以通过 iframe.contentWindow , window.open , window.opener , window.parent 等API进行文档间的交互。但这只限于同源文档之间,非同源之间不能交互。
  • 跨文档数据访问
    除了DOM API外,还有其他 localStorage sessionStorage 等本地缓存数据也受同源限制。
    本质上 跨文档DOM API访问目的是为了数据交换,所以也属于跨文档数据访问
  • 页面URL: http://localhost:8082/crossOrigin/home.html

    <!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>

    跨域情况下除了 postMessage 函数外,不可以访问 contentWindow 对象的任何属性。

    3.2 解决方案 window.name

    可用于页面跳转时的跨文档数据通信。
    Home页面

    <!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>

    Detail页面:

    <!DOCTYPE html>
            <title>Detail</title>
        </head>
                <h1>Detail</h1>            
            </div>
            <script>
                ;(() => {
                    console.log(`detail: ${window.name}`);
                })()
            </script>
        </body>
    </html>
  • window.name 可以保存大数据;
    之前看有框架在浏览器无痕模式下把本地缓存数据(MemoryStorage)都保存在 window.name 里。
  • 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是不同的

    3.3 终极解决方案Window.postMessage()

    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(必须得特殊处理,要不然怎么通讯呢)。
  • message & 结构化克隆算法

  • 传递的数据利用的是 结构化克隆算法复制数据。
    并不是任意数据都可以传递的,具体见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);

    Transferable

    代表一个能在不同可执行上下文之间,列如主线程和Worker之间,相互传递的对象。
    主要用于管道通讯中转移MessagePort等。

    MessageEvent

    比较重要的属性:

  • origin
  • source
    消息发送者对象。可以是window, ServiceWorder, MessagePort。
  • ports
  • MessageChannel

    MessageChannelWindow.postMessage()背后的原理。
    利用MessageChannel可以自定义管道通讯。

    MessagePort必须调用start方法才能发消息;
    通过MessagePort.onmessage绑定事件时会内部调用start方法。利用EventTarget.addEventListener绑定事件需要手动调用start方法。

    window.postMessage内部也利用MessageChannel通讯,不过增加了origin的判断。

    window.postMessageMessageChannel除了用于管道通讯外,还有一些其他使用场景。

  • 作为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认证信息),需要开发显示的告诉浏览器传递;
    如设置XMLHttpRequestwithCredentials=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-Cookieheader也是被忽略的。
  • 2.2.3 总结

  • 服务端处理请求时,如果请求源是在白名单中,则应该只返回该源。不要返回"*"。
  • 浏览器在匹配OriginAccess-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-MethodAccess-Control-Request-Headers的添加已经取值都是由浏览器自行完成。不准许开发介入。

    2.3.2 预检请求的响应

    浏览器只会认CORS相关的headers,响应的其他数据都会被忽。所以对于预检请求的响应最好不要携带:

  • Cookie;
  • 响应Body。
  • 2.3.3 真实请求

    预检通过后浏览器就向服务端发送真实请求了。

    和预检请求区别

  • 真实请求的request不携带Access-Control-Request-MethodAccess-Control-Request-Headers头部了;
  • 真实请求的request可以携带Cookie和数据了;
  • 真实请求的response不携带Access-Control-Allow-MethodAccess-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-OriginOrigin除了用于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-TypeContent-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;
    

    4.2 npm cors middleware分析

  • 内部依赖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)
  •