note icon indicating copy to clipboard operation
note copied to clipboard

跨域总结

Open yaofly2012 opened this issue 5 years ago • 2 comments

一、何为跨域

源(Origin):协议+主域+端口; 同源(Same Origin):协议,域名,端口三者都相同视为同源; 跨域:是指资源的源和文档的源不同。

二、受跨域影响资源

2.1 不受同源策略限制的

  1. 跨域资源写入:
  • <script>
  • <link>
  • <iframe>
  • <img>
  • <video>
  1. Form表单提交。

2.2 受同源策略限制范围

  1. 跨域脚本API访问(不同源的DOM操作);

  2. 跨域数据存储访问:Cookie,Storage,(?IndexedDB,ApplicationCache,CacheStorage);

  3. JS Http请求(XMLHttpRequest, fetch);

  4. @font-face; 字体是版权比较敏感的资源。 image

  5. Images/video frames drawn to a canvas using drawImage(). 只展示跨域图片视频OK,但是不能操作获取数据内容(drawImage()下一步就可以getImageData())。

  6. WebGL textures.

  7. CSS Shapes from images.

总体分为

  1. 客户端跨域文档之间通讯;
  2. 客户端和跨域服务端通讯。

三、客户端跨域文档之间通讯

3.1 跨文档数据访问

  1. 跨文档DOM API访问 JS中可以通过iframe.contentWindow, window.open, window.opener, window.parent等API进行文档间的交互。但这只限于同源文档之间,非同源之间不能交互。
  2. 跨文档数据访问 除了DOM API外,还有其他localStoragesessionStorage等本地缓存数据也受同源限制。 本质上跨文档DOM API访问目的是为了数据交换,所以也属于跨文档数据访问

页面URL:http://localhost:8082/crossOrigin/home.html

<!DOCTYPE html>
<html>
    <head>
        <title>Home</title>
    </head>
    <body>
        <div>
            <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>

image

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

3.2 解决方案window.name

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

<!DOCTYPE html>
<html>
    <head>
        <title>Home</title>
    </head>
    <body>
        <div>
            <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>
<html>
    <head>
        <title>Detail</title>
    </head>
    <body>
        <div>
            <h1>Detail</h1>            
        </div>
        <script>
            ;(() => {
                console.log(`detail: ${window.name}`);
            })()
        </script>
    </body>
</html>
  1. 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;  
}
  1. 只能保存字符串数据; 一般是把对象转成JSON字符串保存。

  2. 文档和内嵌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.

只适用于:

  1. 文档和文档内嵌iframe
  • HTMLIFrameElement.contentWindow
  • window.parent
  1. 文档和文档打开的文档。

语法

// 发消息
targetWindow.postMessage(message, targetOrigin, [transfer]);

// 收消息
targetWindow.addEventListener("message", (event) => {
  // ...
}, false);
  1. postMessage是唯一可以跨域文档访问的API(必须得特殊处理,要不然怎么通讯呢)。

message & 结构化克隆算法

  1. 传递的数据利用的是 结构化克隆算法复制数据。 并不是任意数据都可以传递的,具体见Things that don't work with structured clone
  • 函数;
  • DOM对象;
  • 正则对象的lastIndex属性。
  1. 还可以利用结构化克隆算法实现深拷贝

targetOrigin

指定目标window的origin

  1. targetOrigin除了可以是origin外还可以是个URL,此时浏览器自动获取URL里的origin(不是直接使用URL匹配)。
  2. targetOrigin默认值通配符"*",只有同域时使用通配符才能正常发消息;
  3. 跨域时只有当targetOrigin指定的origin和当目标window的origin匹配时(大小写不敏感)才能发消息;
  4. 收消息的时候最好在事件处理函数里增加origin白名单,即只处理白名单origin发的消息。
window.addEventListener("message", (event) => {
  if (event.origin !== "http://example.org:8080")
    return;

  // ...
}, false);

Transferable

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

MessageEvent

比较重要的属性:

  1. data
  2. origin
  3. source 消息发送者对象。可以是window, ServiceWorder, MessagePort。
  4. ports

MessageChannel

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

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

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

其他应用

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

  1. 作为setImmediate的polyfill

四、客户端和跨域服务端通讯

4.1 jsonp

4.3 CORS

yaofly2012 avatar Nov 15 '20 16:11 yaofly2012

CORS

词条:

  1. 跨源资源共享:Cross-Origin Resource Sharing
  2. 访问控制:Access Control,后面会看到以Access-Control前缀的头部字段。

一、引入背景

浏览器端JS中的http请求(XMLHttpRequest/fetch)受同源策略限制。但是这也导致有些合理的请求也被限制了。W3C提出了新的标准CORS来解决这个问题。 CORS机制让服务端决定是否准许跨域请求(当然了服务端也要承担确保安全的职责)。

除了XMLHttpRequest/fetch还有其他资源请求可以使用CORS:

  1. Web Fonts (for cross-domain font usage in @font-face within CSS)
  2. WebGL textures
  3. Images/video frames drawn to a canvas using drawImage()
  4. 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 简单请求处理流程

浏览器会直接发生真实请求。具体步骤:

  1. 浏览器:在请求头部中添加Origin头,如果XMLHttpRequest对象的withCredentials属性为true, 把请求域的cookie信息添加到请求头中。
  2. 服务器:在响应头部中添加Access-Control-Allow-Origin
  3. 浏览器:读取响应的Access-Control-Allow-Origin头取值。如果为"*"或者和Origin取值相等,则通过,否则报错XMLHttpRequest对象onError捕获。

2.2.2 携带Cookie

  1. 跨域请求默认不携带Cookie(HTTP认证信息),需要开发显示的告诉浏览器传递; 如设置XMLHttpRequestwithCredentials=true

  2. 如果浏览器要携带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 总结

  1. 服务端处理请求时,如果请求源是在白名单中,则应该只返回该源。不要返回"*"。
  2. 浏览器在匹配OriginAccess-Control-Allow-Origin时,只是简单的字符串匹配,大小写是敏感的image 感觉这个有点坑,比较URL是大小不敏感的。估计是浏览器是明确开发明白自己做的事情。

2.3 预检(Preflight)

如果跨域请求不是简单请求,则浏览器先给服务端发送个OPTIONS请求用于预检。由服务端告诉浏览器是否准许真实请求跨域,如果准许则浏览器再发送真实的请求(走简单请求的流程),否则报错。这个过程就是预检过程。

2.3.1 预检请求携带的内容

  1. 预检请求必须是个简单请求;
  2. 预检请求不能携带数据(HTTP Body),也不能携带Cookie等认证信息;
  3. 预检请求需要携带真实请求的信息:
  • 可能引发副作用的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,响应的其他数据都会被忽。所以对于预检请求的响应最好不要携带:

  1. Cookie;
  2. 响应Body。

2.3.3 真实请求

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

和预检请求区别

  1. 真实请求的request不携带Access-Control-Request-MethodAccess-Control-Request-Headers头部了;
  2. 真实请求的request可以携带Cookie和数据了;
  3. 真实请求的response不携带Access-Control-Allow-MethodAccess-Control-Allow-Headers头部。

和简单请求处理区别

没有区别,处理逻辑是一样的。 这也要求真实请求的响应也必须携带Access-Control-Allow-Origin,并且如要带Cookie也得携带Access-Control-Allow-Credentials: true头部等,以cors源码image

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

  1. 准许请求的自定义头部名称,多个用逗号隔开;
  2. 大小写不敏感。

3. Access-Control-Allow-Methods

  1. 准许请求的method,多个用逗号隔开;
  2. 大小写敏感(全大写)。

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流程图

image

从图中注意几点:

  1. 预检请求就是比简单请求多了一步预检过程,预检通过后发送的真实请求是走真实请求的逻辑;
  2. Access-Control-Allow-Orgin, Access-Control-Allow-Credentials可能会被判定两次(预检请求,真实请求)。

2.7 优缺点

优点:

  1. 解决XMLHttpRequest跨域请求的最终方案,可以支持各种类型的请求。

缺点

  1. 兼容性不好,有些浏览器不支持CORS机制(见MDN,PC&Mobile)。

三、CORS-浏览器

3.1 浏览器做的事情

在CORS机制里大部分事情是浏览器自动处理的,

  1. 是否跨域检查
  2. 是否需要预检
  3. 发生预检请求,CORS相关Header信息
  4. 检查预检请求
  5. 发生真实请求

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());
    }
    else
    {
        result = "<p>Hello</p>"; // 真实请求逻辑
    }

    return result;
}

4.2 npm cors middleware分析

  1. 内部依赖npm vary,HTTP Vary这么重要吗? 重要啊,会影响客户端缓存决策,见:
  1. CORS里关于Access-Control-Allow-Origin也有段描述:

如果服务端指定了具体的域名而非“*”,那么响应首部中的 Vary 字段的值必须包含 Origin。这将告诉客户端:服务器对不同的源站返回不同的内容

  1. 默认情况不设置Access-Control-Max-Age, 即采用浏览器默认的。

参考

  1. MDN Cross-Origin Resource Sharing (CORS)

yaofly2012 avatar Nov 18 '20 13:11 yaofly2012