# 👉 跨域

浏览器本着安全的原则采用了同源策略,同源指两个 URL 的协议/主域名/子域名/端口都需要一致,不同域之间相互请求资源,就算作“跨域”。

同源才允许访问相同的 cookie、localStorage 和 发送 Ajax 请求。如果上面其中一点不同都会出现跨越问题。

非同源站点有这样一些限制:

1)不能读取和修改对方的 DOM;
2)不读访问对方的 Cookie、IndexDB 和 LocalStorage;
3)限制 XMLHttpRequest 请求。

当浏览器向目标 URI 发 Ajax 请求时,只要当前 URL 和目标 URL 不同源,则产生跨域,被称为跨域请求。

响应其实是成功到达客户端了,跨域请求的响应一般会被浏览器所拦截

# 那这个拦截是如何发生呢

基于浏览器的多进程架构,浏览器会将每个渲染进程(WebKit 渲染引擎和 V8 引擎都在渲染进程当中)装进了沙箱中。渲染进程是无法直接发送网络请求的,只能通过网络进程发送请求。

然后网络进程利用 Unix Domain Socket 套接字,配合事件驱动的高性能网络并发库 libevent 完成进程的 IPC 过程。网络进程收到数据传递给了浏览器主进程,主进程接收到后,才真正地发出相应的网络请求。

在服务端处理完数据后,将响应返回,主进程检查到跨域,且没有 cors 响应头,将响应体全部丢掉,并不会发送给渲染进程。这就达到了拦截数据的目的。

装进了沙箱中的原因:为了防止黑客通过脚本触碰到系统资源,浏览器将每一个渲染进程装进了沙箱,并且为了防止 CPU 芯片一直存在的 Spectre 和 Meltdown 漏洞,采取了站点隔离的手段,给每一个不同的站点(一级域名不同)分配了沙箱,互不干扰。

更多参考:

# 解决跨域的方式

# CORS

CORS 其实是 W3C 的一个标准,全称是跨域资源共享。要求服务器需要附加特定的响应头。

# 简单请求和复杂请求

对于浏览器的范围划分,满足以下条件的称为简单请求:

  1. 请求方法为:GET、POST、HEAD;
  2. 请求字段在对应值在特定范围:AcceptContent-TypeAccept-LanguageContent-Language,这几个字段取值只限于application/x-wwww-form-urlencodedtext/plainmultipart/form-data

除了以上条件,额外的请求都可以划分为复杂请求。

# CORS 原理

针对简单请求和复杂请求,有两种类型的处理方式。

# 针对简单请求

  1. 在简单请求发出去之前,浏览器会自动在请求头中添加一个Origin属性,表明请求来源;
  2. 服务端获取到请求时,也会在响应头中增加一个Access-Control-Allow-Origin;浏览器端获取到响应时,就会根据这个字段决定是否拦截这个响应。
  3. 同时,服务端也会有其他一些额外的字段来进行其他功能数据交互,如:
  • Access-Control-Allow-Credentials
    这个字段标示客户端是否允许发送 Cookie,浏览器默认改值为 false。若服务端需要获取到客户端的 Cookie 则需设置为 true,并且客户端需要设置 withCredentials为 true。

  • Access-Control-Expose-Headers
    这个字段相当于给 XMLHttpRequest 对象赋能,让它不仅可以拿到基本的 6 个响应头字段(Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma),还能拿到这个字段声明的响应头字段。 如Access-Control-Expose-Headers: aaa,此时前端可以通过 XMLHttpRequest.getResponseHeader('aaa') 拿到 aaa 这个字段的值

# 针对复杂请求

针对复杂请求会有两个阶段:预检请求和正式请求。(和简单请求的区别主要体现在:预检请求和响应字段)

  1. 在进行复杂请求时,会先发送一个OPTIONS预检请求;
    浏览器会自动在请求头中添加Origin属性和Host属性,同时也会增加以下两个请求字段:
    1)Access-Control-Request-Method:CORS 请求用到哪个 HTTP 方法
    2)Access-Control-Rquest-Headers:CORS 请求将要加上什么请求头

这个预检请求的请求行和请求体是下面这个格式:

OPTIONS / HTTP/1.1

Origin: 当前地址
Host: xxx.com

// 注意Method没有s
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header(一些自定义的头部字段)
  1. 服务端获取到预检请求时,会进行响应报文格式如下:
HTTP/1.1 200 OK

// 表示可以允许请求的源,可以填具体的源名,也可以填*表示允许任意源请求
Access-Control-Allow-Origin: *

// 表示可以允许请求的方法
Access-Control-Allow-Methods: GET, POST, PUT

// 表示可以允许发送的请求头部
Access-Control-Allow-Headers: X-Custom-Header

// 表示客户端是否可以发送Cookie
Access-Control-Allow-Credentials:true

// 表示同一请求的预检请求有效期,客户端在多长时间内可以不用再发送此请求预检请求(无法针对整个域或者模糊匹配 URL 做缓存)
Access-Control-Max-Age: 1728000

Content-Type: text/html; charset=utf-8

Content-Length: 0

Content-Encoding: gzip
  1. 客户端收到来自服务端对预检请求的响应后,会对真正要发送的请求进行检测。
    1)如果请求不满足响应头的条件,则触发 XMLHttpRequest 的 onerror 方法,当然后面真正的 CORS 请求也不会发出去了。
    2)如果满足条件,则像简单请求一样,发送真正的正式请求。其中,浏览器也会像简单请求那样子,自动加上 Origin 字段,服务端响应头返回 Access-Control-Allow-Origin。可以参考以上简单请求部分的内容。

# JSONP

虽然 xhr 对象遵循同源政策,但是 script 标签并没有限制,依然可以通过 GET 请求的方式,获取 src 的目标地址资源。因此可以借用 script 标签,实现跨域请求并获取响应(JSONP 原理)。

实现对 JSONP 封装:

function JSONP({ url, params, callbackName }) {
    callbackName = callbackName || (Math.random() + "").replace(".", "");

    const generateURL = () => {
        let queryStr = "";

        for (let key in params) {
            queryStr += `${key}=${params[key]}&`;
        }

        queryStr += `callback=${callbackName}`;

        return `${url}?${queryStr}`;
    };

    return new Promise((resolve, reject) => {
        // 初始化回调函数名称,若无就随机创建一个
        callbackName =
            callbackName ||
            Math.random()
                .toString()
                .replace(",", "");

        let scriptTag = document.createElement("script");
        scriptTag.src = generateURL();

        document.body.appendChild(scriptTag);

        // 绑定到 window 上,为了后面调用
        window[callbackName] = (data) => {
            resolve(data);

            // script 执行完了,成为无用元素,需要清除
            document.body.removeChild(scriptTag);
        };
    });
}

JSONP({
    url: "https://www.baidu.com/s",
    params: {
        wd: "122",
    },
}).then((data) => {
    // 拿到数据进行处理
    console.log(data); // 数据包
});

和 CORS 相比,JSONP 最大的优势在于兼容性好,IE 低版本不能使用 CORS 但可以使用 JSONP,缺点也很明显,请求方法单一,只支持 GET 请求。

# Nginx

# 正向代理

正向代理可以帮助客户端,访问客户端自己访问不到的服务器,然后将结果返回给客户端。

# 反向代理

反向代理可以帮助服务端,可以拿到客户端的请求,将请求转发给其他服务器。
主要的场景是维持服务器集群的负载均衡,换句话说,反向代理帮其它的服务器拿到请求,然后选择一个合适的服务器,将请求转交给它。

例子:比如客户端域名为client.com,服务端域名为server.com,客户端需要向服务端发送请求:

server{
  listen 80
  server_name client.com
  location /api {
    proxy_pass server.com;
  }
}

Nginx 相当于起了一个跳板机,这个跳板机的域名也是 client.com,让客户端首先访问 client.com/api,这当然没有跨域;
然后 Nginx 服务器作为反向代理,将请求转发给 server.com,当响应返回时又将响应给到客户端,这就完成整个跨域请求的过程。

##  更多

除了以上的三种方式,还有其他解决方案,比如 postMessage,当然 WebSocket 也是一种方式。

# 场景分析

# http://a.com 网站中嵌入 https://b.com 的 iframe

# https://b.com 网站中嵌入 http://a.com 的 iframe