# 👉 跨域
浏览器本着安全的原则采用了同源策略,同源指两个 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 的一个标准,全称是跨域资源共享。要求服务器需要附加特定的响应头。
# 简单请求和复杂请求
对于浏览器的范围划分,满足以下条件的称为简单请求:
- 请求方法为:GET、POST、HEAD;
- 请求字段在对应值在特定范围:
Accept
、Content-Type
、Accept-Language
、Content-Language
,这几个字段取值只限于application/x-wwww-form-urlencoded
、text/plain
、multipart/form-data
;
除了以上条件,额外的请求都可以划分为复杂请求。
# CORS 原理
针对简单请求和复杂请求,有两种类型的处理方式。
# 针对简单请求
- 在简单请求发出去之前,浏览器会自动在请求头中添加一个
Origin
属性,表明请求来源; - 服务端获取到请求时,也会在响应头中增加一个
Access-Control-Allow-Origin
;浏览器端获取到响应时,就会根据这个字段决定是否拦截这个响应。 - 同时,服务端也会有其他一些额外的字段来进行其他功能数据交互,如:
Access-Control-Allow-Credentials
:
这个字段标示客户端是否允许发送 Cookie,浏览器默认改值为 false。若服务端需要获取到客户端的 Cookie 则需设置为 true,并且客户端需要设置withCredentials
为 true。Access-Control-Expose-Headers
:
这个字段相当于给 XMLHttpRequest 对象赋能,让它不仅可以拿到基本的 6 个响应头字段(Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
和Pragma
),还能拿到这个字段声明的响应头字段。 如Access-Control-Expose-Headers: aaa
,此时前端可以通过 XMLHttpRequest.getResponseHeader('aaa') 拿到 aaa 这个字段的值
# 针对复杂请求
针对复杂请求会有两个阶段:预检请求和正式请求。(和简单请求的区别主要体现在:预检请求和响应字段)
- 在进行复杂请求时,会先发送一个
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(一些自定义的头部字段)
- 服务端获取到预检请求时,会进行响应报文格式如下:
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)如果请求不满足响应头的条件,则触发 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 也是一种方式。