前端跨域的各种文章其实已经很多了,但大部分还是不太符合我胃口的介绍跨域。看来看去,如果要让自己理解印象深刻,果然还是得自己敲一敲,并总结归纳整理一篇博客出来,以此记录。
跨域的限制
跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。
跨域解决方案
除非特别说明,否则下方标记的 html 文件默认都运行在 http://127.0.0.1:5500 服务下
CORS
CORS 即是指跨域资源共享。它允许浏览器向非同源服务器,发出 Ajax 请求,从而克服了 Ajax 只能同源使用的限制。这种方式的跨域主要是在后端进行设置。
这种方式的关键是后端进行设置,即是后端开启 Access-Control-Allow-Origin 为*
或对应的 origin
就可以实现跨域。
浏览器将 CORS 请求分成两类:简单请求和非简单请求。
只要同时满足以下两大条件,就属于简单请求。
- 请求方法是以下是三种方法之一:
- HEAD
- GET
- POST
- HTTP 的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain
凡是不同时满足上面两个条件,就属于非简单请求。
简单请求
cors.html
1 | let xhr = new XMLHttpRequest() xhr.open('GET', 'http://localhost:8002/request') |
server.js
1 | const express = require('express') |
非简单请求
上面的是简单请求,如果我们用非简单请求的方式,比如请求方法是 PUT,也可以通过设置实现跨域。
非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为”预检”请求。
浏览器先询问服务器,服务器收到”预检”请求以后,检查了 Origin、Access-Control-Request-Method 和 Access-Control-Request-Headers 字段以后,确认允许跨源请求,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。
1 | let xhr = new XMLHttpRequest() |
server.js
1 | const express = require('express') |
整个过程发送了两次请求,跨域成功。
当然,还可以设置其他参数:
- Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段
- Access-Control-Allow-Credentials
表示是否允许发送 Cookie。默认情况下,Cookie 不包括在 CORS 请求之中。
- Access-Control-Expose-Headers
CORS 请求时,XMLHttpRequest 对象的 getResponseHeader()方法只能拿到 6 个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在 Access-Control-Expose-Headers 里面指定。
- Access-Control-Max-Age
用来指定本次预检请求的有效期,单位为秒。有效期是 20 天(1728000 秒),即允许缓存该条回应 1728000 秒(即 20 天),在此期间,不用发出另一条预检请求。
Node 中间件代理
实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就没有跨域一说。
代理服务器,需要做以下几个步骤:
- 接受客户端请求 。
- 将请求转发给服务器。
- 拿到服务器响应数据。
- 将响应转发给客户端。
这次我们使用 express 中间件 http-proxy-middleware 来代理跨域, 转发请求和响应
案例三个文件都在同一级目录下:
index.html
1 | let xhr = new XMLHttpRequest() |
nodeMdServer.js
1 | const express = require('express') |
nodeServer.js
1 | const express = require('express') |
运行http://localhost:8001/index.html
,跨域成功
平常 vue/react 项目配置 webpack-dev-server 的时候也是通过 Node proxy 代理的方式来解决的。
Nginx 反向代理
实现原理类似于 Node 中间件代理,需要你搭建一个中转 nginx 服务器,用于转发请求。
这种方式只需修改 Nginx 的配置即可解决跨域问题,前端除了接口换成对应形式,然后前后端不需要修改作其他修改。
实现思路:通过 nginx 配置一个代理服务器(同域不同端口)做跳板机,反向代理要跨域的域名,这样可以修改 cookie 中 domain 信息,方便当前域 cookie 写入,实现跨域登录。
nginx 目录下的 nginx.conf 修改如下:
1 | // proxy服务器 |
启动 Nginx
index.html
1 | var xhr = new XMLHttpRequest() |
server.js
1 | var http = require('http') |
jsonp
原理:利用了 script 标签可跨域的特性,在客户端定义一个回调函数(全局函数),请求服务端返回该回调函数的调用,并将服务端的数据以该回调函数参数的形式传递过来,然后该函数就被执行了。该方法需要服务端配合完成。
实现步骤:
- 声明一个全局回调函数,参数为服务端返回的 data。
- 创建一个 script 标签,拼接整个请求 api 的地址(要传入回调函数名称如 ?callback=getInfo ),赋值给 script 的 src 属性
- 服务端接受到请求后处理数据,然后将函数名和需要返回的数据拼接成字符串,拼装完成是执行函数的形式。(getInfo(‘server data’))
- 浏览器接收到服务端的返回结果,调用了声明的回调函数。
jsonp.html
1 | function getInfo(data) { |
server.js
1 | const express = require('express') |
jQuery 的 $.ajax() 方法当中集成了 JSONP 的实现,在此就不写出来了。
在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,而且这种方式用起来也麻烦,故我们自己封装一个 jsonp 函数
1 | function jsonp({ url, params, callback }) { |
服务端解构的时候可以取出参数
1 | app.get('/', (req, res) => { |
优点:兼容性好
缺点:由于 script 本身的限制,该跨域方式仅支持 get 请求,且不安全可能遭受 XSS 攻击
postMessage
postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,且是为数不多可以跨域操作的 window 属性之一,它可用于解决以下方面的问题:
- 页面和其打开的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的 iframe 消息传递
- 上面三个场景的跨域数据传递
总之,它可以允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
otherWindow.postMessage(message, targetOrigin, [transfer]);
- otherWindow:其他窗口的一个引用,比如 iframe 的 contentWindow 属性、执行 window.open 返回的窗口对象、或者是命名过或数值索引的 window.frames。
- message: 将要发送到其他 window 的数据。
- targetOrigin:通过窗口的 origin 属性来指定哪些窗口能接收到消息事件,其值可以是字符串”*“(表示无限制)或者一个 URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配 targetOrigin 提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。
- transfer(可选):是一串和 message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
这次我们把两个 html 文件挂到两个 server 下,采取 fs 读取的方式引入,运行两个 js 文件
postMessage1.html
1 | <body> |
postMsgServer1.js
1 | const express = require('express') |
postMessage2.html
1 | <body> |
postMsgServer2.js
1 | const express = require('express') |
websocket
WebSocket 是一种网络通信协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,长连接方式不受跨域影响。由于原生 WebSocket API 使用起来不太方便,我们一般都会使用第三方库如 ws。
Web 浏览器和服务器都必须实现 WebSockets 协议来建立和维护连接。由于 WebSockets 连接长期存在,与典型的 HTTP 连接不同,对服务器有重要的影响。
socket.html(http://127.0.0.1:5500/socket.html
)
1 | let socket = new WebSocket('ws://localhost:8001') |
运行nodeServer.js
1 | const express = require('express') |
document.domain + iframe
这种方式只能用于二级域名相同的情况下。
比如 a.test.com 和 b.test.com 就属于二级域名,它们都是 test.com 的子域
只需要给页面添加 document.domain =’test.com’ 表示二级域名都相同就可以实现跨域。
比如:页面 a.test.com:3000/test1.html 获取页面 b.test.com:3000/test2.html 中 a 的值
test1.html
1 | <body> |
test2.html
1 | document.domain = 'test.com' |
window.name + iframe
浏览器具有这样一个特性:同一个标签页或者同一个 iframe 框架加载过的页面共享相同的 window.name 属性值。在同个标签页里,name 值在不同的页面加载后也依旧存在,这些页面上 window.name 属性值都是相同的。利用这些特性,就可以将这个属性作为在不同页面之间传递数据的介质。
由于安全原因,浏览器始终会保持 window.name 是 string 类型。
打开http://localhost:8001/a.html
1 | <body> |
1 | <body> |
c 页面给 window.name 设置了值, 即便 c 页面销毁,但 name 值不会被销毁;a 页面依旧能够得到 window.name。
location.hash + iframe
实现原理: a.html 欲与 c.html 跨域相互通信,通过中间页 b.html 来实现。 三个页面,不同域之间利用 iframe 的 location.hash 传值,相同域之间直接 js 访问来通信。
具体实现步骤:一开始 a.html 给 c.html 传一个 hash 值,然后 c.html 收到 hash 值后,再把 hash 值传递给 b.html,最后 b.html 将结果放到 a.html 的 hash 值中。
同样的,a.html 和 b.html 是同域的,都是http://localhost:8001,也就是说 b 的 hash 值可以直接复制给 a 的 hash。c.html 为http://localhost:8002下的
a.html
1 | <body> |
b.html
1 | window.parent.parent.location.hash = location.hash |
c.html
1 | console.log(location.hash) // #jackylin |
总结
- CORS 支持所有的 HTTP 请求,是跨域最主流的方案
- 日常工作中,用得比较多的跨域方案是 CORS 和 Node 中间件及 Nginx 反向代理
- 不管是 Node 中间件代理还是 Nginx 反向代理,主要是通过同源策略对服务器不加限制。
参考
- ps: 个人技术博文 Github 仓库,觉得不错的话欢迎 star,给我一点鼓励继续写作吧~