为什么 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 的修仙秘籍(点击可跳转)


关联阅读


目录
相关文章
|
存储 文件存储
如何使用Nest.js 上传文件及自定义文件名保存
在 Nest.js 中进行文件上传并自定义文件名保存的过程相对简单
484 0
|
2月前
|
前端开发 搜索推荐 开发者
前端基础(三)_路径(绝对路径、相对路径)、语义化、特殊字符
本文介绍了前端开发中的路径概念(包括绝对路径和相对路径)、HTML的语义化以及特殊字符的使用。文章解释了绝对路径和相对路径的区别和应用场景,阐述了HTML语义化的意义和好处,并通过示例代码展示了如何在HTML中使用特殊字符。
66 0
若依修改-------控制若依重定向的路径,控制路径重定向的写法路径在,在permission.js文件中控制重定向
若依修改-------控制若依重定向的路径,控制路径重定向的写法路径在,在permission.js文件中控制重定向
|
4月前
|
Java 文件存储
软件开发常用之SpringBoot文件上传和下载功能(上){fileName},利用hutool提供的依赖,拿到当前目录的路径,System.getProperty从变量获取路径 ,不存在就用mkdi
软件开发常用之SpringBoot文件上传和下载功能(上){fileName},利用hutool提供的依赖,拿到当前目录的路径,System.getProperty从变量获取路径 ,不存在就用mkdi
|
6月前
|
JavaScript 前端开发
Gulp 打包压缩 js 文件到指定目录详细流程(修改文件名与后缀)
Gulp 打包压缩 js 文件到指定目录详细流程(修改文件名与后缀)
42 0
vite2 打包的时候vendor-xxx.js文件过大的解决方法
vite2是一个非常好用的工具,只是随着代码的增多,打包的时候 vendor-xxxxxx.js 文件也越来越大,这就郁闷了。
387 0
|
JavaScript
Node.js——文件模块和路径模块(读写文件,处理路径)
Node.js——文件模块和路径模块(读写文件,处理路径)
189 0
|
JavaScript 开发者
_dirname 和_filename 获取正在执行的 js 文件的路径|学习笔记
快速学习_dirname 和 _filename 获取正在执行的 js 文件的路径
_dirname 和_filename 获取正在执行的 js 文件的路径|学习笔记
jsp页面编程时的路径问题(绝对路径与相对路径)
浅谈在进行jsp页面编程时,路径问题的解决(绝对路径与相对路径)