解决方案"/>
跨域及其解决方案
目录
- 同源策略
- 同源和非同源
- 跨域出现的场景
- 跨域实际解析
- 标签跨域问题
- AJAX 跨域
- 跨域解决方案
- CORS
- 简单请求
- 预检请求(preflight)
- 案例
- 对简单请求的验证
- 对预检请求的验证
- node 中实现 CORS
- cookie
- JSONP
- 代理
- 选择
同源策略
同源策略
是浏览器
的一套安全机制
,也可以理解为规则,当一个源的文档或脚本,与另一个源的资源进行通信时,同源策略就会对这个通信做出不同程度的限制
- 即对
同源资源
放行对非同源资源
限制,因为这个限制而造成的问题,就被我们称之为跨域问题
- Tips:注意是 浏览器 中的同源策略,也就意味着在浏览器中才会出现这个所谓的跨域
同源和非同源
-
什么是
源
源 = 协议 + 域名 + 端口
-
例如:
-
的源为
- http://127.0.0.1:5500/index.html
的源为
http://127.0.0.1:5500
-
-
因此两个 URL 地址的
源相同的时候就为同源
,否则为非同源
-
我们可以来看一下如下的对比:
URL 地址1 URL 地址2 是否同源 :8000/api/user :8000/api/books yes
: 8000
/api/user: 8888
/api/booksno
http:
//aaa:8000/api/userhttps:
//aaa:8000/api/booksno
http:// aaa
:8000/api/userhttp:// bbb
:8000/api/userno
-
经过上面的对比,我们可以得出,是否同源不需要看
端口后面的路径
,只需要看协议 + 域名 + 端口
是否一样,不一样则为非同源
跨域出现的场景
- 跨域一般会出现在如下三种场景中:
- 网络通信:
a元素的跳转
、加载css、js等文件
、AJAX
等 - JS API:
window.open打开新的页面
等 - 存储:
localStorage
、sessionStorage
等
- 网络通信:
- 下面我们可与具体说一下上面的场景,主要讲解网络通信中的
AJAX
问题,了解这个之后,对于其他问题产生的跨域也会理解
跨域实际解析
标签跨域问题
-
假设我们现在在浏览器打开的页面的URL地址为:http://127.0.0.1:5500/index.html,那么这个地址的源为
http://127.0.0.1:5500
,那么我们现在使用img
标签中请求发出了一个图片的请求,这个图片的地址为http://localhost:8001/upload/images/1677722567194.jpg,当然这个图片地址是我本地开的服务器提供的,大家自行测试时候这个地址是无法生效的<img src="http://localhost:8001/upload/images/1677722567194.jpg" />
-
那么请问上述的请求中是否发生了跨域了,发生了,我们先开看一下效果:
-
可以看到图片是成功请求出来了,并且没有出现跨域的报错,这是为什么呢?还记得我们开始说的
浏览器对于这个通信会做出不同程度的限制
吗,现在就体现出来了 -
浏览器对于标签的限制是微乎其微的,几乎是没有的,所以我们使用 script、link、img 等标签的时候,都可以访问非同源的资源,比如我们最常见的 CDN 引入一些库,img 标签经常访问其他服务器的图片
AJAX 跨域
-
浏览器对 AJAX 的跨域请求限制是
非常严格
-
什么是 AJAX,
AJAX是指在 web 程序中异步向服务器发送请求
,它并不是什么XMLHttpRequest(xhr)
、fetch
,xhr、fetch只是他的实现方式
,准确点说,AJAX 就是一个概念 -
我们可以同时用 img 标签和 fetch 来发送同一张图片请求,看一下对比效果:
-
相信通过这个对比大家就可以很清晰的感觉到这个限制的差别了
-
在这里和大家说一点,我们这个请求发出去了,服务器其实是接收到了的,只是返回给浏览器的时候,
浏览器校验没有通过
,还是那个规则,跨域只存在浏览器中,服务器中并没有跨域这一说
跨域解决方案
CORS
- 跨源资源共享 (CORS)(Cross-Origin Resource Sharing)是一种基于 HTTP 头的机制,通过对接口的响应头进行设置,来配合客户端发送的各种请求。是正统的跨域解决方案,同时也是浏览器
推荐的解决方案
- CORS 是一套规则,可以帮助浏览器判断校验是否通过
- 还记得上面例子中请求发出去了,只是返回的时候校验没有通过,这个校验的规则就是
CORS
- CORS 实现原理:
- 只要服务器表示
允许
,则校验通过 - 如果服务器拒绝或者没有表示,则校验失败
因此使用 CORS 方案,必须通过服务器
- 只要服务器表示
- CORS 将请求分为
简单请求
和预检请求
,对于不同的请求他的规则也有一些区别
简单请求
文档地址:
- 我们简单说一下什么样的请求可以划分为简单请求:
- 请求方法必须是
GET
、POST
、HEAD
三者之一 - 头部字段满足 CORS 安全规范,详情可以从
w3c
官网了解一下,当然,浏览器自带的默认头部字段都是符合安全规范的,只要开发者不改动和新增,都不会违反
- 如果头部携带了
Content-type
字段,必须是下面三种值之一text/plain
:一般表示文本multipart/form-data
:一般表示文件application/x-www-form-urlencoded
:窗体数据被编码为名称/值对,这是标准的编码格式,不过现在很少使用了
- 请求方法必须是
- 通常情况下,只要遵守这三条,都是简单请求,下面我们看一下预检请求
预检请求(preflight)
- 只要不是简单请求,都是预检请求
案例
-
案例1:只是单纯的请求,GET 请求方法,没有改动请求头部
fetch('') // 简单请求
-
案例2:新增请求头部字段
fetch('', { headers: { ext: .png }}) // 预检请求
对简单请求的验证
- 对于简单请求的验证,就比较简单了,在发送请求的时候携带上源就好了,当然,浏览器会自动给我们携带上,假设我们的源是
- 请求发送到服务器后,服务器通常做出响应处理,告诉浏览器 这个源是允许的就OK了
- 当然可以简单讲一下服务器怎么配置,配置一个字段:Access-Control-Allow-Origin: ,这样浏览器校验的时候就可以通过了
- 当然可以配置多个,使用 * 表示允许所有源访问,不过一般不会使用 *,当然可以动态配置,我们后续在讲解
对预检请求的验证
-
在浏览中,如果是预检请求,与服务器通信之前,浏览器会先发送
options
请求进行预校验
,来判断服务器是否允许该请求访问 -
那这个 options 是什么呢,它会携带一些信息,比如请求源是什么、请求方式是什么、本次请求修改了那些头部字段等等,
-
只有这一次预检请求通过之后,才会真正的校验通过
-
如何区分预检和简单请求,其实在浏览器的网络调试面板可以很清楚的看到中可以很清楚的看到,如下:
-
可以看到第一个请求时预检请求,随后才是真正的请求,携带了数据,状态码为200,当然这个演示的话需要自己写好服务器来接收文件上传,不懂文件上传接口怎么编写的,可以翻看我这篇文档:
-
我们也可以点开看一下,服务器允许的请求方法、字段有哪些,当然信息很多,大家可以自行多了解一下
node 中实现 CORS
-
可能大家看了这么多,对这个服务器怎么处理这些就有一些疑惑,不过本文重点不是将服务器,所以给大家简单介绍一下服务器如何实现 CORS
-
设置允许的域名
,设置属性Access-Control-Allow-Origin
res.setHeader(Access-Control-Allow-Origin, '*' ) // 允许所有网站 res.setHeader(Access-Control-Allow-Origin, 'aaa' ) // 仅允许此域名访问
-
设置允许的请求头字段
,设置属性Access-Control-Allow-Headers
,默认情况下,CORS 仅支持向客户端发送服务器发送 9 个请求头:Accept
、Accept-Language
、Content-Language
、DPR
、Downlink
、Save-Data
、Viewport-Width
、Width
、Content-Type
(值仅限于 text/plain、multipart/form-data、application/x-www-form-urlencoded 三者之一,如果发送了额外的请求头,就需要在服务器进行配置res.setHeader(Access-Control-Allow-Headers, 'Content-Type, X-Custom-Header' ) // 允许额外的请求头响应
-
设置允许的请求方法
,设置属性Access- Cotrol-Allow-Meyhods
,默认情况下,CORS 仅支持客户端发起 GET、POST、HEAD 请求。如果客户端希望通过 PUT、DELETE 等请求方法,则需要通过 Access-Control-Alow-Methods 来指明允许使用的 HTTP 方法res.setHeader(Access-Control-Allow-Methods, 'POST, GET, HEAD, DELETE' ) // 允许这四个方法 res.setHeader(Access-Control-Allow-Methods, '*' ) // 允许所有方法
-
那我们来讲一下如果不设置 * 如何实现多域名白名单处理,是用逗号分隔吗,我也很想可以有这种方式,但是很遗憾在 node 中并不支持,我们以 koa 为例,如果想动态配置域名白名单,我们可以使用路由
router.all
方法,代码如下:router.all('*', async (ctx, next) => {if(ctx.request.header.origin !== 'aaa') returnctx.set('Access-Control-Allow-Origin', ctx.request.header.origin);await next(); });
-
上面代码中,可以通过判断,来达到实现多域名配置的效果,即
动态匹配
,当然我在距离时只给了一个条件,你可以改成数组,判断是否允许,这些基础相信大家都知道 -
我这里还可以提供另外一个方案,安装一个 cors 的库,使用也很简单,如下:
// 1.引入cors包 const cors = require('cors')// 2.注册全局cors中间件,注意是全局,且因为执行顺序大家主要放在首位 app.use(cors())
cookie
- 上面提供的方案将域名设置为 * 号时其实是不能对携带 cookie 生效的,如果需要我们还需要添加上
ctx.set('Access-Control-Allow-Credentials', 'true')
- 同时前端如果是 xhr 创建的请求,需要设置
withCredentials 为 true
- 如果 fetch 创建的请求,需要设置
credentials 为 include
JSONP
-
在很早之前,并没有出现 CORS 这种解决跨域的方案,所以出现了 JSONP 这种曲线救国的方法,不过它的缺点也很明显
- 仅能使用GET请求
- 容易产生安全隐患:恶意攻击者可能利用
callback=恶意函数
的方式实现XSS
攻击
-
因此,这种方式几乎都见不到了,只有在一些古老的项目中可能才会看见,也
不推荐使用
,但是我们这里还是对他做一个讲解,大家作为一个了解即可 -
具体的实现其实就利用
script 标签发送请求 + callback
的方式,实现 JSONP 需要前后端配合,我们案例约定为这个携带过去的query
参数中值为cb
的就是回调函数名称,当然有可能同时我们还需要传递一下其他的参数,我们这里传递一下 name 和 age,我们发送请求后,需要后端对这个用户的 age + 2,在返回给我结果,前端代码如下<script>// 创建一回调函数 foofunction foo(result) {console.log('jsonp请求回来的数据', result.age)}// 创建一个 script 标签const script = document.createElement('script')// 并将 script 的标签的 src 属性设置为请求路径script.src = 'http://localhost:8001/jsonp?foo=cb&name="zhangsn"&age=18'// 将标签加入 html 中document.body.appendChild(script) </script>
-
我们来看一下 node 中的后端代码,当然,我只展示接口处理部分,如下:
// jsonp 的接口 jsonpRouter.get('/', (ctx, next) => {const query = ctx.request.query// 提取回调函数名称for (const key in query) {// 返回给客户端的函数名称应该和上传的一样if (query[key] === 'cb') {ctx.body = `${key}({ name: ${query.name}, age: ${query.age * 1 + 2} })`}} })
-
我们来看一下响应结果
-
可以看到实现了请求响应,那么如果真的需要使用 JSONP 方式实现,所以我们可以对这个方法进行一些封装,我们修改前端的代码即可,如下:
<script>// 封装 jsonp 发送函数function jsonp(url, fn) {const script = document.createElement('script')script.src = urldocument.body.appendChild(script)// 通过将 fn 这个回调函数挂载在window上实现调用window[fn] = function (result) {console.log('jsonp', result)// 使用完成后移除这个属性,避免污染全局delete window[fn]}}// 传入一个地址与函数名称即可jsonp('http://localhost:8001/jsonp?foo=cb&name="zhangsn"&age=18', 'foo') </script>
-
结果我就不在演示了,毕竟已经成为一个淘汰的方案了,作为了解即可,当然,如果你可以将它使用 promise 的方式进行包裹,甚至可以使用随机字符串决定回调函数的名称,避免重复,随机字符串我就不在演示了,比较简单,演示一下 promise 的封装吧,如下:
<script>function jsonp(url, fn) {// 其实也很简单,包裹成一个 promise 对象即可return new Promise((reslove, reject) => {const script = document.createElement('script')script.src = urldocument.body.appendChild(script)// 通过将 fn 这个回调函数挂载在window上实现调用window[fn] = function (result) {reslove(result)delete window[fn]}// 同时元素使用完成之后就可以将它移除script.onload = () => {script.remove()}})}// 传入一个地址与函数名称即可jsonp('http://localhost:8001/jsonp?foo=cb&name="zhangsn"&age=18', 'foo').then(res => {console.log('promise封装的jsonp', res)}) </script>
代理
- 代理是怎么解决的跨域的呢?再次重申,跨域只存在于浏览器中,而
服务器与服务器之间是不存在跨域的
,而代理就是找一个中间服务器
进行代理,用一个例子来说明就是,古代中大臣觐见皇帝,直接闯进去就是当场格杀,那么他是不是需要先通过一个人的传禀,皇帝才能接收他的请求,并召见,处理大臣的请求 - 明白了原理之后,我们来说一下实现思路,首先我们前端是准备好的,其次我们有一台
服务器(服务器1号)
,但是我们要获取的数据是其他服务器(服务器2号)
的,但是这个服务器2号
又不是我们可以控制的,那么我就就需要通过服务器1号
来帮我们实现代理,我们请求发给 1,1再去请求 2,然后 1 获得结果之后返回给我客户端即可 - 这个实现很简单,无非就是在
服务器1号
中使用 axios 请求一下服务器2号
的数据,所以不在进行演示
选择
- 在了解三种解决跨域的方式之后,我们应该如何选择呢?
- 首先一定要考虑
生产环境
,也就是上线后的环境,我使用哪种方式上线后是不会造成影响的 - 排除淘汰的 JSONP,首选肯定是 CROS,但是当需要获取的数据都是无法控制的服务器时,我们就会选择 代理
- 当然在 vue 中,他给我们提供了一个代理服务,使用起来很方便,但是原理是一样的
更多推荐
跨域及其解决方案
发布评论