为什么 Vite 的请求有时候是相对路径,有时候是 /@fs/ + 绝对路径?

简介: 为什么 Vite 的请求有时候是相对路径,有时候是 /@fs/ + 绝对路径?

Vite 的请求路径种类


  • 相对路径,相对于根目录的路径。如:http://localhost/src/main.ts
  • /@fs/ 开头 + 绝对路径,例如:http://localhost/@fs/app/vite/packages/vite/dist/client/env.mjs

其中 /app/vite/packages/vite/dist/client/env.mjs 为绝对路径,可以直接访问文件。

这两种不同路径种类的使用场景,其实很简单,就是看要访问的文件,是否在项目根目录中?

如果文件在 Vite root 根目录中,则直接使用相对路径

但如果在 Vite root 根目录外,相对路径就需要使用 ../ 这种,这种形式不能马上看出文件的位置,因此直接使用绝对路径更好,但是需要跟相对路径做区分,因此用 /@fs/ 开头 + 绝对路径的方式

这里一个两种请求种类都有的项目,在线运行地址

1686401436181.png

该项目设置了 root 为 /root 文件夹,因此 public 文件夹就在 root 外了,因此访问 /public/vite.svg 就会用 /@fs/ + 绝对路径的方式访问了。

1686401426962.png

在开发 monorepo 项目的时候,经过就会遇到模块是在 Vite root 目录外的。


源码解析


Vite 在转换一个文件时,会将它的 import 的模块的路径标准化,例如:

我们访问 http://localhost/src/main.ts 时,Vite 会转换 main.ts 的代码,转换前和转换后的结果如下:


// 转换前的源代码
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')
// 转换后的代码
import { createApp } from "/node_modules/.vite/deps/vue.js?v=3386baa1";
import "/src/style.css";
import App from "/src/App.vue";
createApp(App).mount("#app");
可以看到 import 的模块路径被改变了,路径被标准化为基于根目录的相对路径(如果在 Vite 根目录外,则用 /@fs/)

我们再来看看路径标准化的相关源码(有节选):


// 标准化 url,例如: ./App.vue -> /src/App.vue
const normalizeUrl = async (
    url: string,
    pos: number,
    forceSkipImportAnalysis: boolean = false,
): Promise<[string, string]> => {
    // 解析 url,resolved.id 就是当前文件的绝对路径
    const resolved = await this.resolve(url, importerFile)
    // 通过绝对路径判断
    // 如果路径在 Vite 根目录内,就用相对路径
    if (resolved.id.startsWith(root + '/')) {
        // 去掉 root 根目录的前缀,就是相对路径了
        url = resolved.id.slice(root.length)
    } else if (
        // 如果文件存在
        fs.existsSync(cleanUrl(resolved.id))
    ) {
        // 在绝对路径前,拼接 /@fs/
        url = path.posix.join('/@fs/', resolved.id)
    } else {
        // 文件不存在,这可能是一个 Vite 的虚拟模块
        // 例如:plugin-vue:export-helper,不是真实存在的模块,但在 Vue 插件中会被转换成代码
        // 这个可以不管,跟本文无关
        url = resolved.id
    }
    return [url, resolved.id]
}
从这里可以看出,相对路径和绝对路径的使用场景,就是根据文件是否在 root 目录中来决定的

到这里,其实已经解决了我们的问题了,但我们可以想得更深:

既然可以绝对路径访问文件,那输入另一个的路径,是不是就能访问到别的文件了? 这样有安全问题了啊


安全问题


支持绝对路径访问文件是有风险的,坏人可以通过输入其他路径,获取到整个机器的所有文件了(只要能知道路径),可能那些文件里面就有敏感信息,因此非常危险。

为了避免产生安全问题,Vite 限制了 Dev Server 的文件访问范围,让其只能访问到部分项目用到的文件,这就是 Vite 的文件安全访问策略。

如果访问了允许范围外的文件,Vite 就会返回以下错误页面。

1686401388348.png

我们通过 localhost 访问的,别人用 localhost + 绝对路径也是访问它自己的机器,这应该没什么安全问题?

如果是本地开发,使用 localhost 访问,那的确没有什么安全问题。Vite 的 server.host 默认值是 localhost,因此 Dev Server 也只会绑定到 localhost,别人是没办法访问的。

但其实还有另一种开发模式 —— 远程开发。代码是写在服务器上的,然后 Vite 也是跑在服务器上的,然后通过网络去访问页面。这种情况下,就要远程访问 Dev Server,就会有安全问题,要防止别人通过绝对路径,访问到服务器上的其他数据了。

有关远程开发细节,可以查看我的文章《JetBrains 远程开发的使用和心得》


Vite 文件安全访问策略


我们直接从源码看看,Vite 是如何判断是否有允许访问的:


// 函数返回 true 就是允许访问
function ensureServingAccess(
  url: string,
  server: ViteDevServer,
  res: ServerResponse,
  next: Connect.NextFunction,
): boolean {
  // 判断是否允许访问
  if (isFileServingAllowed(url, server)) {
    return true
  }
  // 如果不允许访问,但文件又是存在的,就会返回 403 的页面
  if (isFileReadable(cleanUrl(url))) {
    const urlMessage = `The request url "${url}" is outside of Vite serving allow list.`
    // 当前允许访问的路径
    const hintMessage = `
${server.config.server.fs.allow.map((i) => `- ${i}`).join('\n')}
Refer to docs https://vitejs.dev/config/server-options.html#server-fs-allow for configurations and more details.`
    server.config.logger.error(urlMessage)
    server.config.logger.warnOnce(hintMessage + '\n')
    res.statusCode = 403
    // 响应请求,响应的是 403 页面。
    res.write(renderRestrictedErrorHTML(urlMessage + '\n' + hintMessage))
    res.end()
  } else {
    // 如果文件不存在,那就不管了,别的 server 中间件会返回 404 HTTP 状态码
    next()
  }
  return false
}

从上述代码中可以知道,我们上一小节看到的 Vite 403 错误页面,就是这里返回的

是否允许访问的核心判断逻辑在 isFileServingAllowed


export function isFileServingAllowed(
  url: string,
  server: ViteDevServer,
): boolean {
  // 如果不执行不严格的 fs 策略,就允许访问。
  if (!server.config.server.fs.strict) return true
  //  标准化为绝对路径
  const file = fsPathFromUrl(url)
  if (server._fsDenyGlob(file)) return false
  if (server.moduleGraph.safeModulesPath.has(file)) return true
  if (server.config.server.fs.allow.some((dir) => isParentDirectory(dir, file)))
    return true
  return false
}

主要有几个判断:

  1. 是否执行了严格的 fs 策略,对应的 Vite 配置是 server.fs.strict,默认是 true
  2. 是否命中 deny 拒绝名单,对应的配置是 server.fs.deny,默认为 ['.env', '.env.*', '*.{pem,crt}']
  3. 是否为项目中使用到的文件server.moduleGraph.safeModulesPath 是一个 Set<string>,它记录了所有项目中被 import 的文件的绝对路径。因此,如果项目中使用到了在 root 根目录外的文件,也是能被正常访问到的。但没有使用的文件就不行了。
  4. 是否命中 allow 名单。对应的配置是 server.fs.allow,如果不配置,Vite 将当前目录加入到 allow,如果是 monorepo 项目,还会将 workspaces 的目录加入到 allow

如果不被允许,Vite 就会返回 403 页面,从而保证了安全性

为什么不直接用 url 判断,而是要先将 url 标准化为绝对路径再判断?

因为需要确保安全性。假如通过 url 是否是 root 开头,来判断是否允许访问,是有问题的。

假如 Vite 的 root 为 /root,那坏人可以 /@fs/root/../other/password.txt,去绕过这个策略,这就会出现安全漏洞了。


总结


本文以一个开发中的一个小问题作为开头,提出疑问:为什么 Vite 的请求有时候是相对路径,有时候是 /@fs/ 开头 + 绝对路径?

然后逐步进行解答,最终得出结论:在 root 外的会用 /@fs/ 进行访问

问题虽然很简单,但还可以再一步深入,提出了潜在安全问题,并探索 Vite 是如何解决的,最终还从源码中了解到了 Vite 文件安全访问策略

如果这篇文章对您有所帮助,可以点赞加收藏👍,您的鼓励是我创作路上的最大的动力。也可以关注我的公众号订阅后续的文章:Candy 的修仙秘籍(点击可跳转)


关联阅读


目录
相关文章
|
9月前
|
JSON 自然语言处理 运维
不只是告警:用阿里云可观测 MCP 实现 AK 高效安全审计
本文介绍了运维工程师小王如何通过阿里云操作审计日志与MCP结合,快速排查一次AK异常访问事件。借助自然语言查询技术,小王实现了对敏感操作、高风险行为及Root账号使用的实时追踪与分析,提升了安全响应效率与系统可控性。
449 32
|
前端开发 API UED
深入理解微前端架构:构建灵活、高效的前端应用
【10月更文挑战第23天】微前端架构是一种将前端应用分解为多个小型、独立、可复用的服务的方法。每个服务独立开发和部署,但共同提供一致的用户体验。本文探讨了微前端架构的核心概念、优势及实施方法,包括定义服务边界、建立通信机制、共享UI组件库和版本控制等。通过实际案例和职业心得,帮助读者更好地理解和应用微前端架构。
|
JavaScript 前端开发 数据可视化
用Vue搭建一个大屏数据可视化页面实战一(Vue实战系列)
用Vue搭建一个大屏数据可视化页面实战一(Vue实战系列)
2421 3
|
存储 API Android开发
微信图片分享支持url,缩略图支持url
在集成微信分享的过程中,如果`缩略图`是url形式,或者`大图分享`的图片是个url,就需要我们先把图片下载下来,然后依据微信的要求对图片做一些压缩操作,最后将图片的数据设置给要分享的对象即可。
|
存储 SQL 缓存
一看就会的Next.js App Router版 -- Routing(上)(一)
一看就会的Next.js App Router版 -- Routing
1635 1
|
JavaScript
Echarts渲染不报错但是没有内容
Echarts渲染不报错但是没有内容
885 0
Echarts渲染不报错但是没有内容
|
小程序 iOS开发
uniapp中IOS端小程序底部黑线适配的方法(整理)
uniapp中IOS端小程序底部黑线适配的方法(整理)
|
存储 自然语言处理 安全
搭建自己的私有云盘工具总结
用网盘工具搭建自己的私有云 优点:自己控制数据、不限速(但取决于服你的务器)、功能多、无广告 缺点:稳定性不如大公司、成本高、有一定技术门槛 请在下面选一个自己需要的即可,对应官网有详细的安装说明
7090 0
|
数据采集 移动开发 算法
论设备指纹的唯一性:始于硬件ID,终于云端交互
互联网时代,用户拉新几乎是所有公司必须面对的话题,从投入运营的初期阶段到快速成长期,再到稳定的成熟阶段,拉新贯穿了产品的整个生命周期,毕竟有了新用户才能创造出价值。
1653 0
论设备指纹的唯一性:始于硬件ID,终于云端交互
|
存储 Oracle 关系型数据库
探索MySQL:历史、版本与安装
本文深入探讨了MySQL数据库的历史、版本特性以及安装与配置过程。MySQL作为一款备受欢迎的开源关系型数据库管理系统,在其多年的发展中取得了显著成就。通过介绍MySQL的发展历程,读者可以了解到其从创始人之手到今天广受欢迎的开源数据库的演变过程。此外,本文还突出了MySQL的关键版本,如MySQL 5.7和MySQL 8.0,以及它们引入的重要特性,这些特性不仅丰富了MySQL的功能,也为开发者提供了更多创新的可能性。最后,本文详细介绍了MySQL的安装和配置步骤,为读者提供了安装MySQL并使其成功运行的实用指南。
891 0