当网页从一个源(https://baidu.com)请求另一个源(如 https://taobao/api)的资源时,就发生了跨域。由于安全原因(防止恶意网站通过脚本访问用户在其他网站上的数据),浏览器对跨域请求设置了限制,这样的机制称为同源策略。
浏览器特有的现象
应该说,跨域不是浏览器特有的现象,跨域几乎在所有客户端都存在,但是同源策略是浏览器特有的。当你在浏览器中执行 AJAX 请求,浏览器首先会检查此请求的源是否合法(即与当前页面的源是否相同)。如果请求是跨域的,浏览器会立即阻止该请求发送(后端会连请求都收不到)。
但是微信小程序,桌面应用和APP等客户端应用是不会受到同源策略的直接限制的,因为他们运行的客户端环境并不是浏览器,但是他们需要遵守的是所处的运行环境的其他规范。比如微信小程序上线后能请求的API必须用的是https协议,而且能请求哪些域名下的API必须在后台自己的小程序管理下面进行登记 (没记错的话,上一次开发小程序至今已有一坤年) ,开发环境下倒是无所谓。
假设我在浏览器端和在微信小程序端同时发送一个请求给后端,那后端收到的请求头分别长什么样子?以下只对比一些关键字段:
1️⃣ 浏览器
- User-Agent:用来标识当前浏览器类型和版本,例如:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36
- Referer:重头戏,指示请求来源的 URL,在JavaScript里面对这个字段能进行的修改极端有限,甚至可以说没有,Referer 的值是由浏览器自动设置的。
- Accept:表示浏览器可以处理的内容类型
- Cookie:如果有的话
2️⃣ 微信小程序
- User-Agent:微信小程序的请求头中的 User-Agent 会有不同的格式,通常包含 “MicroMessenger” 字样,例如:Mozilla/5.0 (Linux; Android 10; SM-G9500 Build/QP1A.190711.020; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/12.0 WeChat/8.0.0.1 MiniProgramPlatform/1.0.0.0
- Referer:微信小程序可以设置特定的 Referer,但也不是在开发过程中能够通过JavaScript来控制的,在使用
wx.request
的时候Referer由小程序框架自动填充,通常会被设置为小程序的合法请求域名,即你在小程序后台配置的接口域名 - Accept:通常为*/*
- Cookie:如果有的话
处理跨域问题(浏览器端)
CORS
首先得知道,CORS并没有直接作用于客户端,CORS配置是服务端的响应头配置,比较关键的字段是以下两个:
const responseHeaders = {
"Access-Control-Allow-Origin": "*" // 允许跨域请求的域名
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS" // 允许跨域请求的方法
}
它规定了哪些方法,来自哪些域名的请求可以成功调用服务端的接口,这保护的是服务端的安全性。当浏览器发送跨域请求时,服务器通过 CORS 响应头(如 Access-Control-Allow-Origin)来告诉浏览器是否允许该请求,具体来说,服务器在返回的 HTTP 响应中添加 CORS 相关的头部信息,允许特定的源进行访问。
到目前为止,CORS的作用还是停留在对服务器的保护上,但是却抛出了一个问题💡:如果我的请求被浏览器的同源策略所拦截,我压根发不出去,更不可能知道响应头的Access-Control-Allow-Origin
到底是什么,那我的AJAX岂不一辈子都发不出去?
所以OPTIONS,预检请求登场。OPTIONS 请求是一种 HTTP 请求方法,用于查询服务器支持的 HTTP 方法及其他选项,而不是直接请求特定资源。根据 CORS 规范,预检请求通常在以下情况下发送:
1️⃣ 请求方法为 PUT、DELETE、PATCH、OPTIONS,或其他非 GET 和 POST 的方法
2️⃣ 如果 Content-Type 头为除 application/x-www-form-urlencoded、multipart/form-data 或 text/plain 以外的任何值(例如 application/json)
3️⃣ 当请求中包含自定义头部(例如 X-Requested-With)
如果OPTIONS询问目标服务器后发现自己的接下来要发送的请求能够符合服务器那边的CORS配置,那同源策略就会放行,AJAX就能发到后端。
顺便提一嘴,开发微信小程序有时候也得后端配合设置CORS,说不定你的referer一开始没在人家的白名单里呢。
JSONP
JSONP算一种投机取巧的方法,在如果服务器是团队内部的,那还是用CORS。 JSON是不受同源策略限制的,但是这种方法只能支持GET请求而且相对不安全。 至今不知道不安全在哪,被攻击到的我觉得是服务端而不是客户端。
给一个demo,后端用了Egg:
// router.js
module.exports = (app) => {
const { router, controller } = app;
// JSONP 数据接口
router.get('/api/data', controller.home.getData);
};
// home.js
const Controller = require('egg').Controller;
class HomeController extends Controller {
async getData() {
const { ctx } = this;
// 获取回调函数名(来自query传参数)
const callback = ctx.query.callback;
// 要返回的数据
const data = { message: 'Hello from Egg.js!' };
// 检查是否提供了callback参数
if (callback) {
ctx.body = `${callback}(${JSON.stringify(data)})`; // 返回 JSONP 响应
} else {
ctx.body = { error: 'No callback provided' }; // 返回错误
}
}
}
module.exports = HomeController;
前端:
<script>
function handleResponse(data) {
// 处理返回的 JSONP 数据
console.log(data);
document.getElementById("result").innerText = data.message;
}
function fetchData() {
const script = document.createElement("script");
script.src = "http://localhost:7001/api/data?callback=handleResponse"; // 注意 CORS 端口应与后端一致
document.body.appendChild(script); // 动态插入 script 标签进行 JSONP 请求
}
</script>
但是这种script标签的用法和开发中用的比较多的用script来下载其他库不是一回事。
使用 script 标签发起 JSONP 请求的目的是从不同源(跨域)获取数据。服务端会以一个可调用的 JavaScript 函数的形式返回数据,以便前端能够在加载该脚本后执行回调函数来处理数据。
而用它来加载库是为了引入第三方 JavaScript 代码,以便在你的网页中使用这些库提供的功能。加载后的库通常会直接在全局范围内注册变量。
Proxy
在同源服务器上设置代理,通过中间的服务器转发请求,浏览器请求的是同源服务器,从而绕过同源策略。比较常用的是Nginx的反向代理功能,开发中Webpack和Vite都提供了开启内置的反向代理服务器的功能。
既然要浏览器请求的是同源服务器,那axios里面的url就不能是真正的API URL,而应该是前端应用的生产环境URL,然后在Nginx内做了一次转发,把请求转发到后端服务器去。这种代理和典型的反向代理。
提一嘴正向代理和方向代理。
反向代理是指客户端通过代理服务器发起请求。1️⃣ 客户端只把请求发到代理服务器 2️⃣ 代理服务器接收请求,并根据负载均衡策略或配置将请求转发给一个或多个后端服务器 3️⃣ 后端服务器处理请求后,将响应返回给反向代理服务器 4️⃣ 反向代理服务器再将响应转发回客户端。
如果说反向代理是作用于服务端,那正向代理就是作用于客户端,通常是为了匿名、绕过限制、内容过滤等,比如翻墙用的就是正向代理。了解实在有限,以后再研究研究
iframe标签
可以用iframe实现跨域请求,但是我没用过,甚至连这个标签我都用的很少。
先创建一个主页面,其中嵌入了另一个跨域的页面:
<script>
function sendMessage() {
const iframe = document.getElementById('myIframe');
// 发送消息给 iframe
iframe.contentWindow.postMessage('Hello from the main page!', '*');
}
window.addEventListener('message', (event) => {
// 监听来自 iframe 的消息
console.log('Received from iframe:', event.data);
document.getElementById('result').innerText = event.data;
});
</script>
<body>
<h1>主页面</h1>
<button onclick="sendMessage()">发送消息到iframe</button>
<div id="result"></div>
<iframe id="myIframe" src="https://example.com" width="600" height="400"></iframe>
</body>
再创建一个跨域页面iframe.html,必须支持接收消息和发送响应:
<script>
window.addEventListener('message', (event) => {
// 监听主页面发送的消息
console.log('Received from main page:', event.data);
const result = fetch(..........)
// 发送响应回主页面
event.source.postMessage('Hello from the iframe!', result);
});
</script>
运行方法:
- 将主页面的 index.html 文件放在一个能够访问的 Web 服务器上。
- 将跨域页面的 iframe.html 放在另一个可以访问的地址上(务必符合跨源域)。
- 在主页面上,通过点击按钮“发送消息到 iframe”,会发送一个消息给嵌入的 iframe。
- iframe 页面接收到消息后,会回复一个消息,主页面也会接收到这个回复。