文章目录
- 参考
- 描述
- 同源策略
- 同源
- 同源策略
- 示例
- CSRF 攻击
- 解决跨域问题
- CORS
- CORS 响应头部
- Access-Control-Allow-Origin
- 简单请求
- 预检请求
- 预检请求包含的两次请求
- 解决
- CORS 中间件
- 使用 CORS 中间件处理跨域请求
- JSONP
- 通过原生 JS 向服务器端发起 JSONP 请求
- 通过 jQuery 向客户端发起 JSONP 请求
参考
项目 | 描述 |
---|---|
W3school | AJAX |
MDN | 同源策略 |
MDN | CORS |
搜索引擎 | Bing |
哔哩哔哩 | 黑马程序员 |
C 语言中文网 | JSONP 是什么 |
描述
项目 | 描述 |
---|---|
NodeJS | v18.13.0 |
nodemon | 2.0.20 |
cors | 2.8.5 |
Edge | 109.0.1518.61 (正式版本) (64 位) |
jQuery | 3.6.3 |
npm | 8.19.3 |
同源策略
同源
如果两个 URL 的协议、主机地址及端口号均相同,则这两个 URL 是同源的。
对比:
与 URL http://store.company.com/dir/page.html 的源进行对比为例:
项目 | 结果 | 原因 |
---|---|---|
http://store.company.com/dir2/other.html | 同源 | 两个 URL 的协议、主机地址及端口号均相同,仅存在路径上的不同。 |
http://store.company.com/dir/inner/another.html | 同源 | 仅路径存在不同。 |
https://store.company.com/secure.html | 非同源 | 两者使用的协议并不相同。 |
http://store.company.com:81/dir/etc.html | 非同源 | 两者使用的端口不同,http 默认使用 80 端口。 |
http://news.company.com/dir/other.html | 非同源 | 两者使用的主机地址不同。 |
IE:
IE 浏览器在对两个 URL 进行比较时,仅会检查两者的主机地址与使用的协议是否相同,如果这些内容相同,则两者同源。
同源策略
同源策略(Same Origin Policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说 Web 是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。
同源策略是浏览器的行为,拦截的是服务器端的响应,即请求发送了,服务器对此进行响应,但是无法被浏览器接收并呈现给用户。
上述内容整理自 百度百科
注:
同源策略的相关功能由浏览器来实现,这说明我们可以通过 BurpSuite 、Postman 等工具来绕过同源策略。
示例
我为你准备了一个示例,希望能加深你对同源策略的理解。
服务器端代码:
const express = require('express');
const app = express();
// 监听本机 9090 端口
app.listen(9090);
app.get('/', (req, res) => {
console.log('【已接收到客户端发出的请求】');
// 向客户端发送响应信息
res.send(req.query);
})
客户端代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>客户端</title>
</head>
<body>
<!-- 设置按钮用于发送 AJAX 请求 -->
<button id="btn">GET</button>
<!-- 导入 jQuery -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
<script>
// 为按钮绑定点击事件,在点击按钮后将向服务器发送 GET 请求
$('#btn').on('click', function() {
$.ajax({
type: 'GET',
url: 'http://127.0.0.1:9090',
// 定义需要发送的数据
data: {user: 'RedHeart', age: 18, gender: 'male'},
// 在接收到服务器端的响应信息后在浏览器控制台中打印响应信息
success: (res) => {
console.log(res);
}
})
})
</script>
</body>
</html>
执行效果:
在点击客户端页面的按钮向服务器端发送 AJAX 请求(GET 请求)后,我们将在浏览器控制台中观察到如下信息:
这表明我们的请求已经成功发送到服务器端且服务器端对此进行了响应,但由于同源策略,我们并不能观察到响应内容。
分析:
在使用客户端向服务器端发送 GET 请求时,客户端页面的 URL 为
file:///C:/Users/36683/TwoMoons/WWW/MoonLight/src/index.html
可以看到该 URL 在协议、主机地址以及端口号方面均不与服务器端 URL (http://127.0.0.1:9090)相同。
修改:
你可以使用如下服务器端代码以告知客户端浏览器服务器可以接收跨域请求。
const express = require('express');
const app = express();
// 监听本机 9090 端口
app.listen(9090);
// 设置路由以处理客户端的 GET 请求
app.get('/', (req, res) => {
// 设置服务器端的响应头
res.setHeader(' Access-Control-Allow-Origin', '*');
// 对客户端的请求进行相应
res.send(req.query);
})
CSRF 攻击
同源策略的一个主要功能就是有效阻止可能发生的 CSRF(Cross Site Request Forgery,跨站伪造请求) 。
假设有 A、B 两个网站,其中 B 网站是一家对你较为重要的网站。如果你在登录 B 网站后,前往 A 网站。A 网站可以构造一个 AJAX 请求向 B 网站发送请求,由于此时浏览器已经保存有你的 Cookie 信息(A 网站可以用 Cookie 登录 B 网站),所以该请求可以伪造成你的身份对 B 网站进行 A 网站指定的操作(在 B 网站没有做其他防护的前提下)。
而由于同源策略的存在,A 网站并不能接收到 B 网站服务器端的响应信息。从而达到了保护用户的目的(完全防御 CSRF 攻击还需要进行其他操作)。
解决跨域问题
在某些情况下,同源策略会对我们的 Web 应用起到限制作用。比如:
你搭建一个提供接口的网站,但由于同源策略的原因,其他网站无法请求并使用这个接口。
解决跨域问题有两种方式,即 CORS(Cross Origin Resource Sharing,跨域资源共享) 及 JSONP(JSON with Padding, 携带资源的 JSON)。
- CORS
目前的主流方案,推荐使用。 - JSONP
仅支持 GET 请求方式。
CORS
CORS (Cross-Origin Resource Sharing,跨域资源共享)由一系列 HTTP 响应头组成,这些 HTTP 响应头决定了浏览器是否阻止前端的 JavaScript 代码跨域获取资源。
浏览器的同源安全策略默认会阻止网页“跨域”获取资源。但如果服务器配置了 CORS 相关的 HTTP 响应头,就可以解除浏览器端的跨域访问限制。
注:
- 解决跨域问题仅需在服务器端进行配置,客户端中无需进行任何操作。
- CORS 存在兼容性问题,仅能够在支持 XMLHttpRequest Level2 的浏览器中使用。
3. CORS 的相关响应头部的前缀均为 Acess-Control-Allow 。
CORS 响应头部
Access-Control-Allow-Origin
该响应头用于服务器告知客户端浏览器允许该外域 URL 访问本服务器中的资源。
你可以通过如下代码设置响应头以使外域 URL https://www.baidu.com 访问本机服务器资源。
res.setHeader(' Access-Control-Allow-Origin', 'https://www.baidu.com');
注:
你可以使用 * 来指代所有的 URL。例如,我们可以使用如下代码设置响应头以告知浏览器允许任何 URL 访问本服务器中的资源。
res.setHeader(' Access-Control-Allow-Origin', '*');
简单请求
若客户端请求满足如下条件,则可视该次请求为简单请求。
-
使用如下请求方式之一向服务器端发起请求
- GET
- POST
- HEAD
-
使用如下请求头中的部分或全部向服务器端发起请求
- Accept
- Accept-Language
- Content-Language
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
- Content-Type (值仅限于 text/plain、multipart/form-data、application/x-www-form-urlencoded 三者之一)
注:
如果你的请求方式不满足简单请求的要求,在默认情况下,该次请求将失败。
举个栗子:
客户端代码:
在这次请求过程中,我们将使用不在简单请求中的 DELETE 请求方式向服务器端发送请求。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>客户端</title>
</head>
<body>
<!-- 设置按钮用于发送 AJAX 请求 -->
<button id="btn">DELETE</button>
<!-- 导入 jQuery -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
<script>
// 为按钮绑定点击事件,在点击按钮后将向服务器发送 DELETE 请求
$('#btn').on('click', function() {
$.ajax({
type: 'DELETE',
url: 'http://127.0.0.1:9090',
// 在接收到服务器端的响应信息后在浏览器控制台中打印响应信息
success: (res) => {
console.log(res);
}
})
})
</script>
</body>
</html>
服务器端代码:
const express = require('express');
const app = express();
// 监听本机 9090 端口
app.listen(9090);
// 设置路由处理客户端发送的 DELETE 请求
app.delete('/', (req, res) => {
// 设置 CORS 响应头
res.setHeader('Access-Control-Allow-Origin', '*');
// 对客户端进行响应
res.send('Hello World');
})
执行结果:
分析:
虽然,我们已经将 Access-Control-Allow-Origin 响应头设置为 * ,但客户端仍然无法正常接收服务端发出的响应。这是由于我们使用了不在简单请求范围内的请求方式 DELTET 。
预检请求
若客户端使用不在简单请求范围内的请求头或请求方式,那么该请求属于预检请求。
我们可以通过设置响应头 Access-Control-Allow-Headers 来允许客户端使用简单请求中不包括的请求头;通过使用 Access-Control-Allow-Methods 来允许客户端请求简单请求中不包括的请求方式。
Access-Control-Allow-Headers 与 Access-Control-Allow-Origin 均可以通过通配符 * 来允许客户端使用任何简单请求不允许的请求方式或响应头。
现在让我们修改服务器端的代码以使客户端可以接收到发出的 DELETE 请求的服务器端响应。
服务器端代码:
const express = require('express');
const app = express();
// 监听本机 9090 端口
app.listen(9090);
// 设置路由以处理客户端的 DELETE 请求
app.delete('/', (req, res) => {
// 允许向服务器发送跨送请求
res.setHeader('Access-Control-Allow-Origin', '*');
// 允许向服务器发送 DELETE 请求
res.setHeader('Access-Control-Allow-Methods', 'DELETE');
// 对客户端的请求进行响应
res.send('Hello World');
})
执行结果:
预检请求包含的两次请求
在前面的示例中,我们明明已经设置了相关的响应头,可浏览器的控制台的错误输出信息却告知我们没有设置 Access-Control-Allow-Origin 响应头来允许客户端进行跨域请求。
我们是否可以猜测服务器端的 DELETE 路由并没有执行,使用如下服务器端代码来对此进行验证。
验证:
const express = require('express');
const app = express();
// 监听本机 9090 端口
app.listen(9090);
// 设置路由以处理客户端的 DELETE 请求
app.delete('/', (req, res) => {
// 在接收到 DELETE 请求后向服务器终端打印信息
console.log('【已接收到客户端发出的 DELETE 请求】');
// 允许向服务器发送跨送请求
res.setHeader('Access-Control-Allow-Origin', '*');
// 允许向服务器发送 DELETE 请求
res.setHeader('Access-Control-Allow-Methods', 'DELETE');
// 对客户端的请求进行响应
res.send('Hello World');
})
执行结果:
在服务器端执行该段代码后,在客户端向服务器端发出 DELETE 请求后,服务器终端中并没有输出信息 【已接收到客户端发出的 DELETE 请求】 ,这表明我们的猜想是正确的。
原因:
出现这种现象的原因是因为预检请求共包含两次请求,一次是 options 请求,即预检请求,该请求由浏览器自动发送;一次是正式请求。
预检请求将在正式请求发起前发送,预检请求将向服务器询问服务器端是否允许某某类型的预检请求(由正式请求使用的请求方式及请求头决定),如果服务器端允许这类预检请求,客户端将发送正式请求。否则客户端将不会正常发送正式请求。
我们可以通过浏览器的开发者工具中的 网络 分栏来对此进行验证:
在这里你将能够观察到当前网页向服务器端收发数据包的情况。在进入该界面后,通过前述客户端程序向服务器端发送 DELETE 请求。你将得到与以下内容类似的内容。
可以看到,在末尾多出了两条记录:
最后一条记录即为预检请求。点击该条记录可以发现该请求属于 OPTIONS 请求。
解决
由于预检请求将先于我们需要发送的 DELETE 请求,所以服务器端的 DELETE 路由并没有执行修改响应头的相关代码(路由没有与请求成功匹配)。这也就导致客户端浏览器认为服务器端不允许该类型的预检请求,最终导致正式请求没有成功发送。
全局中间件:
要解决该问题,我们可以设置一个专门用于修改响应头部的中间件函数,并将该中间件函数注册为全局中间件。
const express = require('express');
const app = express();
// 监听本机 9090 端口
app.listen(9090);
// 设置一个专门用于修改响应头部的中间件函数并
// 将该函数注册为全局中间件。
// 全局中间件将对所有类型的客户端请求进行预处理,
// 并在处理完成后将该请求交给相关的路由。
app.use((req, res, next) => {
// 允许向服务器发送跨送请求
res.setHeader('Access-Control-Allow-Origin', '*');
// 允许向服务器发送 DELETE 请求
res.setHeader('Access-Control-Allow-Methods', 'DELETE');
next();
})
// 设置路由以处理客户端的 DELETE 请求
app.delete('/', (req, res) => {
// 对客户端的请求进行响应
res.send('Hello World');
})
在将服务器端代码修改为上述内容并执行后,向服务器端发起 DELETE 请求,你将在客户端浏览器控制台中观察到如下内容:
OPTIONS 路由:
const express = require('express');
const app = express();
// 监听本机 9090 端口
app.listen(9090);
// 设置路由以处理客户端的 DELETE 请求
app.delete('/', (req, res) => {
// 允许向服务器发送跨送请求
res.setHeader('Access-Control-Allow-Origin', '*');
// 允许向服务器发送 DELETE 请求
res.setHeader('Access-Control-Allow-Methods', 'DELETE');
// 对客户端的请求进行响应
res.send('Hello World');
})
// 设置路由以处理客户端的 OPTIONS 请求
app.options('/', (req, res, next) => {
// 允许向服务器发送跨送请求
res.setHeader('Access-Control-Allow-Origin', '*');
// 允许向服务器发送 DELETE 请求
res.setHeader('Access-Control-Allow-Methods', 'DELETE');
next();
})
注:
请注意为处理预检请求的路由及正式请求的路由设置 CORS 相关的请求头,否则你将在客户端浏览器控制台中观察到如下错误信息。
CORS 中间件
NodeJS 的相关包管理器提供了实现 CORS 的第三方中间件,如果你使用的是 npm(NodeJS Package Manager) 包管理器,那么你可以通过在终端中输入如下命令来对 cors 中间件进行下载安装。
npm install cors
使用 CORS 中间件处理跨域请求
客户端
我们仍将使用发起 DELETE 请求的客户端:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>客户端</title>
</head>
<body>
<!-- 设置按钮用于发送 AJAX 请求 -->
<button id="btn">DELETE</button>
<!-- 导入 jQuery -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
<script>
// 为按钮绑定点击事件,在点击按钮后将向服务器发送 DELETE 请求
$('#btn').on('click', function() {
$.ajax({
type: 'DELETE',
url: 'http://127.0.0.1:9090',
// 在接收到服务器端的响应信息后在浏览器控制台中打印响应信息
success: (res) => {
console.log(res);
}
})
})
</script>
</body>
</html>
服务器端
const express = require('express');
// 导入 CORS 中间件
const cors = require('cors');
const app = express();
// 监听本机 9090 端口
app.listen(9090);
// 将 CORS 中间件注册为全局中间件
app.use(cors());
// 设置路由以处理客户端的 DELETE 请求
app.delete('/', (req, res) => {
// 对客户端的请求进行响应
res.send('Hello World');
})
执行结果:
JSONP
JSONP 全称 JSON with Padding ,译为 携带数据的 JSON ,它是 JSON 的一种使用模式。通过 JSONP 可以绕过浏览器的同源策略,进行跨域请求。
在进行 Ajax 请求时,由于同源策略的影响,不能进行跨域请求,而 script 标签的 src 属性却可以加载跨域的 JavaScript 脚本,JSONP 就是利用这一特性实现的。与普通的 Ajax 请求不同,在使用 JSONP 进行跨域请求时,服务器不再返回 JSON 格式的数据,而是返回一段调用某个函数的 JavaScript 代码。script 标签接收到响应数据后,会将其立即执行。
JSONP 的优点是兼容性好,在一些老旧的浏览器种也可以运行,但它的缺点也非常明显,那就是只能进行 GET 请求。
通过原生 JS 向服务器端发起 JSONP 请求
客户端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>客户端</title>
</head>
<body>
<!-- 定义一个函数用于接收服务器端传输的数据 -->
<script>
function func(data){
console.log(data);
}
</script>
<!-- 通过 script 标签的 src 属性以 JSONP 的请求方式向服务器端发起请求 -->
<script src="http://127.0.0.1:9090/?callback=func"></script>
</body>
</html>
客户端
const express = require('express');
const app = express();
// 监听本机 9090 端口
app.listen(9090);
// 设置路由以对 GET 请求进行处理
app.get('/', (req, res) => {
// 获取查询字符串中 callback 参数对应的参数值
const callback = req.query.callback;
// 定义需要发送至客户端的数据
const raw = {name: 'RedHeart', age: 18};
// 对字符串进行拼接并将需要发送的数据转化为 JSON 格式的字符串
const data = `${callback}(${JSON.stringify(raw)})`;
// 对客户端进行响应
res.send(data);
})
执行结果:
注:
- 服务器端将对客户端的 JSONP 请求响应一个函数,客户端接收到该函数后将立即执行该函数。我们可以通过预先设置同名函数来接收服务器端发送的数据。
- 位于服务器端的 `${callback}(${JSON.stringify(raw)})` 这段代码两端的符号为 反引号 而不是 单引号,请注意。
通过 jQuery 向客户端发起 JSONP 请求
该方式所用到的服务器端代码与上一个示例相同,因此这里不再重复给出。
客户端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>客户端</title>
</head>
<body>
<button id="btn">JSONP</button>
<!-- 通过 CDN 导入 jQuery -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
<script>
$('#btn').on('click', () => {
$.ajax({
method: 'GET',
dataType: 'JSONP',
url: 'http://127.0.0.1:9090',
success: (res) => {
console.log(res);
}
})
})
</script>
</body>
</html>
执行结果
在执行上述代码后,点击页面中的按钮后,你将在浏览器控制台中观察到如下数据。
注:
虽然在上述代码中我们使用了 $.ajax() 函数,但这并不表示 JSONP 请求使用了 AJAX 技术。事实上,JSONP 请求并没有利用 AJAX 技术,仅仅只是采用了 GET 类型的请求方式。