在开发项目时,我发现有时候请求资源的路径是相对路径,有时候是 /@fs/
开头的绝对路径,这是为什么呢?
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/
开头 + 绝对路径的方式
这里一个两种请求种类都有的项目,在线运行地址
该项目设置了 root 为 /root
文件夹,因此 public
文件夹就在 root 外了,因此访问 /public/vite.svg
就会用 /@fs/
+ 绝对路径的方式访问了。
在开发 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 就会返回以下错误页面。
我们通过 localhost 访问的,别人用 localhost + 绝对路径也是访问它自己的机器,这应该没什么安全问题?
如果是本地开发,使用 localhost 访问,那的确没有什么安全问题。Vite 的 server.host
默认值是 localhost
,因此 Dev Server 也只会绑定到 localhost,别人是没办法访问的。
但其实还有另一种开发模式 —— 远程开发。代码是写在服务器上的,然后 Vite 也是跑在服务器上的,然后通过网络去访问页面。这种情况下,就要远程访问 Dev Server,就会有安全问题,要防止别人通过绝对路径,访问到服务器上的其他数据了。
有关远程开发细节,可以查看我的文章《JetBrains 远程开发的使用和心得》
我们直接从源码看看,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
}
主要有几个判断:
true
['.env', '.env.*', '*.{pem,crt}']
server.moduleGraph.safeModulesPath
是一个 Set<string>
,它记录了所有项目中被 import 的文件的绝对路径。因此,如果项目中使用到了在 root 根目录外的文件,也是能被正常访问到的。但没有使用的文件就不行了。如果不被允许,Vite 就会返回 403 页面,从而保证了安全性
为什么不直接用 url 判断,而是要先将 url 标准化为绝对路径再判断?
因为需要确保安全性。假如通过 url 是否是 root 开头,来判断是否允许访问,是有问题的。
假如 Vite 的 root 为 /root
,那坏人可以 /@fs/root/../other/password.txt
,去绕过这个策略,这就会出现安全漏洞了。
本文以一个开发中的一个小问题作为开头,提出疑问:为什么 Vite 的请求有时候是相对路径,有时候是 /@fs/ 开头 + 绝对路径?
然后逐步进行解答,最终得出结论:在 root 外的会用 /@fs/
进行访问。
问题虽然很简单,但还可以再一步深入,提出了潜在安全问题,并探索 Vite 是如何解决的,最终还从源码中了解到了 Vite 文件安全访问策略。