前端关于网络安全看似高深莫测,其实来来回回就那么点东西,我总结一下就是 3 + 1 = 4,3个用字母描述的【分别是 XSS、CSRF、CORS】 + 一个中间人攻击。当然 CORS 同源策略是为了防止攻击的安全策略,其他的都是网络攻击。除了这 4 个前端相关的面试题,其他的都是一些不常用的小喽啰。
我将会在我的《面试题一网打尽》专栏中先逐一详细介绍,然后再来一篇文章总结,预计一共5篇文章,欢迎大家关注~
本篇文章是前端网络安全相关的第三篇文章,内容就是 CORS 同源策略。
本篇文章的基础是需要一个服务端的项目,可以跟着我的这篇文章搭建自己的服务端项目。或者直接克隆我的仓库代码在这个提交上拉一个新分支,本篇文章所有的代码都是在这个提交基础上进行的。
在本篇文章之前,我已经写了 xss 攻击和 csrf 攻击的文章,所以在你拉取我的 git 最新代码的时候,已经有很多更新的提交了。不过,无论是从上面我说的那个提交拉取新分支,还是拉取最新的代码都可以,我的仓库的所有的合并都是相互独立的。
不论你先看 XSS 教程还是先看 CSRF 教程,还是这篇关于跨域的文章都可以。
两个网站协议名、域名、端口号有一个不同就是非同源,就是跨域。跨域问题就是浏览器的同源策略造成的。
同源是指协议名、域名、端口号 必须完全一致!
http 默认端口号是80,https 默认端口号是443
一般来说,同源策略是指对 javascript 脚本的限制,
简单请求不会发生跨域 cors 预检请求,预检请求 Preflight Request 是用于验证是否允许非简单请求的一种 OPTIONS 请求。预检请求指示为了减少跨域请求的复杂性和延迟,不是说简单请求就一定不会报跨域错误。而是非简单请求跨域的概率大一些,所以要预检。预检请求是 CORS 机制的一部分,用于确保跨域请求的安全性,预检失败,不会发送实际的跨域请求。
(1)head、get、post是这三种方法之一【注意,我们常见的post请求不会发送预检请求】或者
(2)没有自定义 http 请求头,除了下面的字段
话虽然这样说,但是实际的简单请求头还有很多字段,比如 orign 、host等【如下图也是一个简单请求的头部】这些字段是浏览器自动设置的。下面的 cache-control 也是浏览器自动加的。
(3)content-type 仅限于 application/x-www-form-urlencoded、multipart/form-data、text/plain
注意这三个条件是或的关系,如果 get 请求加上了自定义的请求头,那么就不是简单请求了。或者,简单请求的 content-type 设置了其他值也就不是简单请求了。简单请求的跨域请求不会发送预检请求。
mdn 官网都有说呢
多说无益,直接写代码,你会更好理解。我们看看什么样的情况会有预检请求。
(1)新建 cors/index.html
Document
cors
(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)提交代码
我们要创造一个跨域的请求,但是我们只有一个服务,其实也很简单,那就是使用 localhost 去访问 ip 地址,自然就跨域了。
我们使用 fetch 只写一个简单的 get 请求,,会发现没有请求列表中没有多余的请求
我们给请求加上自定义的请求头,就会多出一个预检请求
同理,对于head、post 请求如果我们不加自定义响应头,也不会有预检请求
请求头设置了除了 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
头部指示了响应中实体的类型,帮助客户端正确解析响应的内容
put 或者 delete等 请求,无论有没有自定义的响应头,无论有没有设置 content-type 都会发送预检请求。
再次强调一下,简单请求还是非简单请求,我们研究的前提都是对于跨域请求的,对于非跨域的请求,是没有预检这一说的。
现在我们解决这个跨域的错误,对于跨域问题前端的同学其实不用做什么操作的,主要还是服务端的配置。
pnpm i cors
我们使用的 express 实现跨域,可以直接安装 cors 这个包
就这么简单,就不会有跨域问题了。
我们使用 cors 这个 npm 包就不用手动配置 http 的响应头了。
这里面有一个知识点,就是 http 请求响应的状态码是204
204 No Content
对于该请求没有的内容可发送,但头部字段可能有用。用户代理可能会用此时请求头部信息来更新原来资源的头部缓存字段。
如果我们不使用 cors 这个包,我们就需要自定义响应头,关于跨域的响应头主要有三个,分别是:
这里面有个关于 express 的知识点,就是 express 自定义跨域响应头的时候要使用中间件【app.use】,如果你直接在请求中设置是不会生效的,因为跨域请求有一个预检请求。
在Web开发中,"携带凭证"(Credentials)是指允许在跨域请求中发送和接收带有身份验证信息的请求。身份验证信息通常包括使用Cookie、HTTP认证或客户端证书等方式进行的用户认证。
我们常说的携带凭证其实就是携带 cookie
我们在前端需要修改 index.html
关于 fetch 的使用,可以看官网
只设置前端是不够的,会报错,服务端需要配置另外一个响应头。
这样就可以完美的携带cookie了
JSONP 就是 JSON with padding,是一种跨域通信技术,为了解决脚本跨域的问题
利用 script 标签的跨域特性,script 标签是可以跨域的,在页面中插入一个指向跨域资源的 script 标签,以回调函数的形式返回数据。服务器返回的数据被包装在这个回调函数中,使得跨域请求的数据能够被当前页面取到。
重点如下
注意到没有,这块和我们之前写的 XSS 攻击的原理类似,具体可以看下这篇文章,里面详细讲解了各种类型的 XSS 攻击。
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);
Document
jsonp
npm run dev jsonp
jsonp 发起跨域请求的方式已经很少用了,已经被 CORS 所取代了。
具体使用方法,可以参考这篇文章
这里面有个概念,正向代理 vs 反向代理
一句话总结,对客户端的代理是正向代理,对服务端的代理是反向代理,【客户是正面的
~~】
正向代理和反向代理的区别-CSDN博客
我们接下来用 expess 来实现以下反向代理,对服务端的代理。
应用场景:假设后端给了一个服务地址https://a.com,但是后端这个服务没有设置允许跨域,你要自己调试的时候,就可以自己实现一个本地的、非跨域的服务,然后代理后端的地址。你本地的前端访问你本地的服务http://localhost:3000,你本地的服务再把请求转发给后端的地址。
这里面有个知识点,服务端之间进行请求是不存在跨域的,跨域只针对前端和服务端,因为跨域是浏览器的同源策略,需要有浏览器参与,才有跨域问题。
pnpm i http-proxy-middleware
在根目录下新建 proxy 文件夹,并新建 proxy/index.html 文件、proxy/index.js
假设,本地服务 localhost:3000, 本来需要访问 localhost:3001,但是跨域,因为 localhost:3001的服务没有设置允许跨域;所以可以 localhost:3000 访问 localhost:3000的非跨域服务;同时 localhost:3000 服务在服务器端对 3001 端口进行反向代理。
Document
proxy
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)
npm run dev proxy
关于跨域的问题已经总结完成,本篇文章详细介绍了如何使用并配置CORS、JSONP 的实现、Express 进行反向代理。
我的仓库地址如下,欢迎查看
yangjihong2113/learn-express
内容较多,难免疏漏,如有问题,欢迎指正。
这是一系列的文章,续更新中,欢迎关注。