前言
前端关于网络安全看似高深莫测,其实来来回回就那么点东西,我总结一下就是 3 + 1 = 4,3个用字母描述的【分别是 XSS、CSRF、CORS】 + 一个中间人攻击。当然 CORS 同源策略是为了防止攻击的安全策略,其他的都是网络攻击。除了这 4 个前端相关的面试题,其他的都是一些不常用的小喽啰。
我将会在我的《面试题一网打尽》专栏中先逐一详细介绍,然后再来一篇文章总结,预计一共5篇文章,欢迎大家关注~
本篇文章是前端网络安全相关的第三篇文章,内容就是 CORS 同源策略。
一、准备工作
1.1 拉取仓库
本篇文章的基础是需要一个服务端的项目,可以跟着我的这篇文章搭建自己的服务端项目。或者直接克隆我的仓库代码在这个提交上拉一个新分支,本篇文章所有的代码都是在这个提交基础上进行的。
在本篇文章之前,我已经写了 xss 攻击和 csrf 攻击的文章,所以在你拉取我的 git 最新代码的时候,已经有很多更新的提交了。不过,无论是从上面我说的那个提交拉取新分支,还是拉取最新的代码都可以,我的仓库的所有的合并都是相互独立的。
不论你先看 XSS 教程还是先看 CSRF 教程,还是这篇关于跨域的文章都可以。
1.2 新增 CORS 文件夹
二、同源策略
两个网站协议名、域名、端口号有一个不同就是非同源,就是跨域。跨域问题就是浏览器的同源策略造成的。
同源是指协议名、域名、端口号 必须完全一致!
http 默认端口号是80,https 默认端口号是443
2.1 同源策略的限制
一般来说,同源策略是指对 javascript 脚本的限制,
- js 脚本不能跨域访问 cookie、localstorage、indexDB
- js 脚本不能跨域操作 dom
- 不能跨域发送 ajax 请求
三、CORS 解决跨域问题
3.1 简单请求
简单请求不会发生跨域 cors 预检请求,预检请求 Preflight Request 是用于验证是否允许非简单请求的一种 OPTIONS 请求。预检请求指示为了减少跨域请求的复杂性和延迟,不是说简单请求就一定不会报跨域错误。而是非简单请求跨域的概率大一些,所以要预检。预检请求是 CORS 机制的一部分,用于确保跨域请求的安全性,预检失败,不会发送实际的跨域请求。
3.1.1 简单请求的条件
(1)head、get、post是这三种方法之一【注意,我们常见的post请求不会发送预检请求】或者
(2)没有自定义 http 请求头,除了下面的字段
- Accept
- Accept-Language
- Content-Language
- Content-Type【只允许3个类型】
- Range
话虽然这样说,但是实际的简单请求头还有很多字段,比如 orign 、host等【如下图也是一个简单请求的头部】这些字段是浏览器自动设置的。下面的 cache-control 也是浏览器自动加的。
(3)content-type 仅限于 application/x-www-form-urlencoded、multipart/form-data、text/plain
注意这三个条件是或的关系,如果 get 请求加上了自定义的请求头,那么就不是简单请求了。或者,简单请求的 content-type 设置了其他值也就不是简单请求了。简单请求的跨域请求不会发送预检请求。
多说无益,直接写代码,你会更好理解。我们看看什么样的情况会有预检请求。
3.1.2 搭建 cors 服务
(1)新建 cors/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
cors
</body>
</html>
(2)新建 cors/index.js
const express = require('express');
const path = require('path');
const bodyParser = require('body-parser')
const app = express();
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.get('/', function (req, res) {
res.sendFile(path.join(__dirname, '/index.html'));
});
app.listen(3000);
(3)运行 npm run dev cors
(4)提交代码
3.1.3 设置 get 非简单请求
我们要创造一个跨域的请求,但是我们只有一个服务,其实也很简单,那就是使用 localhost 去访问 ip 地址,自然就跨域了。
(1)get 简单请求
我们使用 fetch 只写一个简单的 get 请求,,会发现没有请求列表中没有多余的请求
(2)get 自定义请求头
我们给请求加上自定义的请求头,就会多出一个预检请求
同理,对于head、post 请求如果我们不加自定义响应头,也不会有预检请求
(3)get 设置 content-type
请求头设置了除了 application/x-www-form-urlencoded、multipart/form-data、text/plain 这三个值之外,都会发送预检请求,可以自己测试一下。
content-type 有很多取值,可以自己看一下官方文档。
我们需要记住几个比较常见的
application/json
: 用于指示请求或响应中的实体是 JSON 格式的数据。
application/xml
: 用于指示请求或响应中的实体是 XML 格式的数据。
text/html
: 用于指示请求或响应中的实体是 HTML 格式的文本。
text/plain
: 用于指示请求或响应中的实体是纯文本,没有特定的格式。【简单请求】
multipart/form-data
: 用于指示请求中包含多个部分,通常用于文件上传。【简单请求】
application/x-www-form-urlencoded
: 用于指示请求中的数据是 URL 编码的表单数据,通常用于普通表单提交。【简单请求】
application/octet-stream
: 用于指示请求或响应中的实体是二进制流,可以是任意类型的数据。
image/jpeg
,image/png
,image/gif
, 等: 用于指示请求或响应中的实体是图片文件,具体的媒体类型根据具体的图片格式而定。这些只是一些常见的
Content-Type
取值,实际上还有许多其他可能的值,取决于你要传输的数据类型。当你发送 HTTP 请求时,通过设置适当的Content-Type
,可以确保服务器能够正确地解析请求体中的数据。同样,在处理 HTTP 响应时,Content-Type
头部指示了响应中实体的类型,帮助客户端正确解析响应的内容
3.1.4 put、delete 非简单请求
put 或者 delete等 请求,无论有没有自定义的响应头,无论有没有设置 content-type 都会发送预检请求。
再次强调一下,简单请求还是非简单请求,我们研究的前提都是对于跨域请求的,对于非跨域的请求,是没有预检这一说的。
3.2 CORS 跨域资源共享
现在我们解决这个跨域的错误,对于跨域问题前端的同学其实不用做什么操作的,主要还是服务端的配置。
3.2.1 安装 cors
pnpm i cors
我们使用的 express 实现跨域,可以直接安装 cors 这个包
3.2.2 修改 index.js
就这么简单,就不会有跨域问题了。
我们使用 cors 这个 npm 包就不用手动配置 http 的响应头了。
这里面有一个知识点,就是 http 请求响应的状态码是204
对于该请求没有的内容可发送,但头部字段可能有用。用户代理可能会用此时请求头部信息来更新原来资源的头部缓存字段。
3.2.3 提交代码
3.3 自定义响应头
如果我们不使用 cors 这个包,我们就需要自定义响应头,关于跨域的响应头主要有三个,分别是:
- Access-Control-Allow-Origin 允许的 origin
- Access-Control-Allow-Methods 允许的方法
- Access-Control-Allow-Headers 允许的请求头
3.3.1 设置跨域
这里面有个关于 express 的知识点,就是 express 自定义跨域响应头的时候要使用中间件【app.use】,如果你直接在请求中设置是不会生效的,因为跨域请求有一个预检请求。
3.3.2 提交代码
3.4 携带凭证
3.4.1 前端携带 cookie
在Web开发中,"携带凭证"(Credentials)是指允许在跨域请求中发送和接收带有身份验证信息的请求。身份验证信息通常包括使用Cookie、HTTP认证或客户端证书等方式进行的用户认证。
我们常说的携带凭证其实就是携带 cookie
我们在前端需要修改 index.html
关于 fetch 的使用,可以看官网
只设置前端是不够的,会报错,服务端需要配置另外一个响应头。
3.4.2 服务端允许携带 cookie
这样就可以完美的携带cookie了
3.4.3 提交代码
四、JSONP 实现跨域
JSONP 就是 JSON with padding,是一种跨域通信技术,为了解决脚本跨域的问题
4.1 JSONP原理
利用 script 标签的跨域特性,script 标签是可以跨域的,在页面中插入一个指向跨域资源的 script 标签,以回调函数的形式返回数据。服务器返回的数据被包装在这个回调函数中,使得跨域请求的数据能够被当前页面取到。
重点如下
- 利用 script 标签
- 在 script 标签 的 src 中加上 callback 参数函数
- 前端定义 callback 函数,并把他的名称拼接在 script 标签的 src 上
- 服务器获取 src 上的参数函数,并调用这个函数,给函数传递参数,服务端返回就会执行这个函数,注意到了没有?这个理论逻辑和反射型 xss 攻击是一样的,服务端返回 js 代码,浏览器会自动执行。
- 后端调用完参数,前端就自动执行了定义的函数
- jsonp 只支持方法
- 可能会受到 xss 攻击,所以已经被 cors 所取代
注意到没有,这块和我们之前写的 XSS 攻击的原理类似,具体可以看下这篇文章,里面详细讲解了各种类型的 XSS 攻击。
4.2 代码实现
4.2.1 搭建jsonp 服务器
4.2.2 修改 index.js
const express = require('express');
const path = require('path')
const app = express();
app.get('/', function (req, res) {
res.sendFile(path.join(__dirname, '/index.html'));
});
app.get('/data', function (req, res) {
const { query } = req
// 获取参数上的回调
const callback = query.callback
// 服务端返回执行回调函数,并传递参数 { name: 'test' }
res.send(`${callback}({ name: 'test' })`)
});
app.listen(3000);
4.2.3 修改 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
jsonp
<script>
function handleData(data) {
console.log('jsonp 返回的值', data);
// 跨域请求,服务端,没有配置可以跨域
fetch('http://10.10.25.120:3000/data')
}
</script>
<!-- 这个脚本地址要设置成跨域的-->
<script src="http://10.10.25.120:3000/data?callback=handleData" ></script>
</body>
</html>
4.2.4 运行
npm run dev jsonp
jsonp 发起跨域请求的方式已经很少用了,已经被 CORS 所取代了。
4.2.5 提交代码
五、其他的跨域方式
5.1 postMessage
- 页面和其打开的新窗口传递数据
- 多窗口之间消息传递
- 页面与嵌套的 iframe 消息传递
- iframe.contentWindow.postMessage + window.addEventListener
具体使用方法,可以参考这篇文章
5.2 服务器代理 / websocket
- 可以自己定义个 express 服务器进行代理,使用 node + express + http-proxy-middleware 等插件
- nginx 做反向代理【反向代理是服务端的代理隐藏真实服务器,正式代理是客户端的代理隐藏客户端】
- vue 框架 node + vue + webpack + webpack-dev-server在 config 文件中配置 devServers,原理是利用 http-proxy-middleware + express 这个http代理中间件
- vite 框架 config 文件中也可以配置 server proxy
- websocket 没有浏览器跨域的限制,因为它基于 tcp 协议,没错websocket 可以实现跨域
- EventSource 基于http,所以会跨域,需要服务端设置请求头。
这里面有个概念,正向代理 vs 反向代理
一句话总结,对客户端的代理是正向代理,对服务端的代理是反向代理,【客户是正面的
~~】
5.3 express 实现反向代理
我们接下来用 expess 来实现以下反向代理,对服务端的代理。
应用场景:假设后端给了一个服务地址https://a.com,但是后端这个服务没有设置允许跨域,你要自己调试的时候,就可以自己实现一个本地的、非跨域的服务,然后代理后端的地址。你本地的前端访问你本地的服务http://localhost:3000,你本地的服务再把请求转发给后端的地址。
这里面有个知识点,服务端之间进行请求是不存在跨域的,跨域只针对前端和服务端,因为跨域是浏览器的同源策略,需要有浏览器参与,才有跨域问题。
5.3.1 安装 http-proxy-middleware
pnpm i http-proxy-middleware
5.3.2 搭建 proxy 服务
在根目录下新建 proxy 文件夹,并新建 proxy/index.html 文件、proxy/index.js
假设,本地服务 localhost:3000, 本来需要访问 localhost:3001,但是跨域,因为 localhost:3001的服务没有设置允许跨域;所以可以 localhost:3000 访问 localhost:3000的非跨域服务;同时 localhost:3000 服务在服务器端对 3001 端口进行反向代理。
(1)index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
proxy
<script>
// 页面地址 localhost:3000
// 本来需要请求 localhost:3001,但是跨域,
// 所以请求 localhost:3000/api/info 进行代理
function init () {
// 跨域
fetch('http://localhost:3001/api/info').then((res) => {
console.log('请求成功', res)
}).catch(() => {
console.log('请求失败')
})
// 代理
fetch('/api/info').then((res) => {
console.log('请求成功', res)
})
}
init()
</script>
</body>
</html>
(2)index.js
const express = require('express');
const path = require('path')
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
app.get('/', function (req, res) {
res.sendFile(path.join(__dirname, '/index.html'));
});
// 把针对 /api 的请求,转发给 3001 端口的服务为
app.use('/api', createProxyMiddleware({
target: 'http://localhost:3001',
timeout: 3000,
changeOrigin: true,
}))
app.listen(3000);
// 第二个服务 3001 端口,未配置允许跨域请求
const app1 = express();
app1.get('/api/info', function(req, res){
res.send('proxy ok')
})
app1.listen(3001)
(3)运行结果
npm run dev proxy
总结
关于跨域的问题已经总结完成,本篇文章详细介绍了如何使用并配置CORS、JSONP 的实现、Express 进行反向代理。
我的仓库地址如下,欢迎查看
内容较多,难免疏漏,如有问题,欢迎指正。
这是一系列的文章,续更新中,欢迎关注。