Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

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

跨域总结 #199

Open
yaofly2012 opened this issue Nov 15, 2020 · 2 comments
Open

跨域总结 #199

yaofly2012 opened this issue Nov 15, 2020 · 2 comments

Comments

@yaofly2012
Copy link
Owner

yaofly2012 commented Nov 15, 2020

一、何为跨域

源(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
Copy link
Owner Author

yaofly2012 commented Nov 18, 2020

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
Copy link
Owner Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant