Nuxt3 项目部署 Nginx 防盗链后特定 JS 文件 403 问题修复方案

Nuxt3 项目部署 Nginx 防盗链后特定 JS 文件 403 问题修复方案

问题描述

Nuxt3 前端项目打包部署到配置了防盗链规则的 Nginx 服务器后,浏览器访问页面时部分 JavaScript 文件返回 403 Forbidden

受影响的文件

以下 JS 文件在浏览器中均返回 403:

bash 复制代码
/_nuxt/DApBM9Zf.js    → pages/index/[lang]/productcenter.vue
/_nuxt/ByoahgwI.js    → pages/index/[lang]/productcenter/sishan.vue
/_nuxt/teKmI3we.js    → sishan 页面的 CSS chunk
/_nuxt/GsJ1Y5_8.js    → pages/index/[lang]/foodcenter.vue
/_nuxt/BB-wqL4x.js    → pages/index/[lang]/aboutus.vue
/_nuxt/C1eDSpvq.js    → pages/index/[lang]/aboutus/company.vue
/_nuxt/BEHuFA0U.js    → pages/index/[lang]/contactus.vue
/_nuxt/BhVIYuKy.js    → pages/index/[lang]/productcenter/sishanfeature.vue
/_nuxt/C9OvFcjC.js    → pages/index/[lang]/productcenter/sishanmax.vue

环境信息

项目 说明
框架 Nuxt 3.13.0
构建工具 Vite (Rollup)
部署架构 CDN(含WAF) → Nginx → Nuxt Node.js SSR
Nginx 防盗链配置 valid_referers *.域名.com 域名.com localhost;

根因分析

1. Nginx 防盗链规则缺少 none

项目 Nginx 配置中的防盗链规则:

nginx 复制代码
location ~* .*\.(gif|jpg|ico|png|css|js|swf|flv|mp4|webp)$ {
    valid_referers *.域名.com 域名.com localhost;
    #                    ↑ 缺少 none
    if ($invalid_referer) {
        return 403;
    }
    proxy_pass http://localhost:3000;
}

valid_referers 中没有 none 关键字,意味着没有 Referer 头的请求会被直接拦截返回 403

2. Nuxt3 构建产物中 JS 文件有两种加载方式

通过分析构建产物和 Nginx 访问日志,发现 JS 文件有两种加载方式,其 Referer 行为截然不同:

Nuxt3 对当前路由直接依赖的模块,在 SSR 渲染的 HTML 中生成 <link> 标签:

html 复制代码
<link rel="modulepreload" as="script" crossorigin href="/_nuxt/DYp9iSB5.js">
<link rel="modulepreload" as="script" crossorigin href="/_nuxt/CXwAfdS5.js">

浏览器通过 <link> 标签加载资源时始终携带 Referer 头(值为当前页面 URL),因此不会被防盗链拦截。

方式二:import() 动态加载(返回 403)

Nuxt3 对非当前路由的页面组件使用 Vite 的代码分割功能,生成独立的动态 chunk。当用户通过 NuxtLink 导航到新路由时,才通过 import() 动态加载:

javascript 复制代码
// DYp9iSB5.js 中的路由定义
component: () => Cn(
  () => import("./DApBM9Zf.js"),   // productcenter 页面
  __vite__mapDeps([45, 11, 8, 1, 2, 3, 13, 46]),
  import.meta.url
)

浏览器对 import() 动态加载的请求不发送 Referer 头,触发防盗链规则返回 403。

3. Nginx Access Log 证据

请求文件 Referer 状态码 加载方式
/_nuxt/DYp9iSB5.js http://localhost/zh 200 <link rel="modulepreload">
/_nuxt/CXwAfdS5.js http://localhost/zh 200 <link rel="modulepreload">
/_nuxt/DApBM9Zf.js -(空) 403 import() 动态加载
/_nuxt/ByoahgwI.js -(空) 403 import() 动态加载
/_nuxt/BEHuFA0U.js -(空) 403 import() 动态加载

问题链路总结

scss 复制代码
Nuxt3 构建过程
  → 页面路由组件被 Vite 代码分割为独立 chunk (isDynamicEntry)
  → 非当前路由的页面组件不在 HTML 的 <link rel="modulepreload/prefetch"> 列表中
  → 用户通过 NuxtLink 导航时,Nuxt3 用 import() 动态加载这些 chunk
  → 浏览器对 import() 请求不发送 Referer 头
  → Nginx 防盗链规则 valid_referers 缺少 none,拦截无 Referer 请求
  → 返回 403

修复方案

步骤一:Nitro Plugin 注入 Modulepreload

在 HTML 渲染时,将所有页面路由组件以 <link rel="modulepreload"> 注入到 <head> 中。浏览器对 <link> 标签加载的资源始终携带 Referer,从而绕过防盗链限制。后续 import() 时浏览器直接从缓存读取,不再发起新请求。

实现文件

server/plugins/preload-all-routes.js

javascript 复制代码
/**
 * Nitro Plugin: 预加载所有页面路由组件
 *
 * 问题根因:
 *   Nuxt3 对非当前路由的页面组件使用 import() 动态加载,
 *   浏览器对 import() 请求不发送 Referer 头,
 *   nginx 防盗链规则 (valid_referers 缺少 none) 会拦截无 Referer 的请求返回 403。
 *
 * 解决方案:
 *   在 HTML 渲染时,将所有页面路由组件以 <link rel="modulepreload"> 注入到 <head> 中,
 *   浏览器对 <link> 标签加载的资源始终携带 Referer,从而绕过防盗链限制。
 */

export default defineNitroPlugin((nitroApp) => {
  let cachedManifest = null

  async function getManifest() {
    if (cachedManifest) return cachedManifest

    try {
      // 通过 Nitro 虚拟模块导入 manifest,开发和生产环境均可正确解析
      const mod = await import('#build/dist/server/client.manifest.mjs')
      cachedManifest = mod.default || mod
      if (typeof cachedManifest === 'function') cachedManifest = await cachedManifest()
      return cachedManifest
    } catch (e) {
      console.warn('[preload-all-routes] Failed to import client manifest:', e.message)
      return null
    }
  }

  nitroApp.hooks.hook('render:html', async (html, { event }) => {
    const manifest = await getManifest()
    if (!manifest || !html.head) return

    const runtimeConfig = useRuntimeConfig(event)
    const buildAssetsDir = runtimeConfig.app?.buildAssetsDir
      ? `/${runtimeConfig.app.buildAssetsDir}`.replace(/\/+/g, '/').replace(/\/$/, '') + '/'
      : '/_nuxt/'
    const preloadLinks = []

    // 收集已有的 preload 文件名,避免重复注入
    const existingFiles = new Set()
    for (const h of html.head) {
      const match = h.match(/href="[^"]*\/([^/"]+\.js)"/)
      if (match) existingFiles.add(match[1])
    }

    // 遍历 manifest,找到所有页面路由动态 chunk
    for (const [key, entry] of Object.entries(manifest)) {
      if (
        entry.isDynamicEntry &&
        entry.resourceType === 'script' &&
        !existingFiles.has(entry.file)
      ) {
        const href = buildAssetsDir + entry.file
        preloadLinks.push(
          `<link rel="modulepreload" as="script" crossorigin href="${href}">`
        )
        existingFiles.add(entry.file)

        // 同时注入该 chunk 的依赖(imports)
        if (entry.imports && Array.isArray(entry.imports)) {
          for (const importKey of entry.imports) {
            const importEntry = manifest[importKey] || manifest[`_${importKey}`]
            if (importEntry && importEntry.file && !existingFiles.has(importEntry.file)) {
              const importHref = buildAssetsDir + importEntry.file
              preloadLinks.push(
                `<link rel="modulepreload" as="script" crossorigin href="${importHref}">`
              )
              existingFiles.add(importEntry.file)
            }
          }
        }
      }
    }

    if (preloadLinks.length > 0) {
      html.head.push(...preloadLinks)
    }
  })
})
工作原理
  1. 通过 Nitro 虚拟模块 #build/dist/server/client.manifest.mjs 导入 Vite 构建生成的 manifest,获取所有 chunk 的映射关系(开发和生产环境均可正确解析)
  2. render:html 异步钩子中,遍历 manifest 找到所有 isDynamicEntry 的 JS chunk(即页面路由组件)
  3. 通过 useRuntimeConfig(event) 获取 buildAssetsDir 配置,确保资源路径正确
  4. 检查 HTML <head> 中是否已存在该文件的 preload 声明,避免重复注入
  5. 将缺失的动态 chunk 以 <link rel="modulepreload"> 注入,同时注入其依赖链
  6. 浏览器通过 <link> 标签预加载这些资源(携带 Referer),后续 import() 直接命中缓存

步骤二:启用 Prerender 爬取链接

在步骤一的基础上,启用 crawlLinks 选项,使 Nuxt 在构建时自动发现并预渲染可爬取的页面为静态 HTML,进一步减少运行时 SSR 的压力。

配置修改

nuxt.config.js

javascript 复制代码
nitro: {
  prerender: {
    crawlLinks: true,   // 自动遍历链接,发现可预渲染的页面
    failOnError: false,
  },
  server: {
    host: '0.0.0.0',
  },
}

说明crawlLinks: true 让 Nuxt 在预渲染时自动爬取页面中的链接,无需手动枚举所有路由。jobdetail/[id]newsdetail/[id] 等动态路由因参数依赖 API 数据,不会被自动爬取,仍由运行时 SSR 处理。


验证结果

修复前

通过 Nginx 访问,所有页面路由组件返回 403:

perl 复制代码
DApBM9Zf.js (no referer) => Forbidden
ByoahgwI.js (no referer) => Forbidden
BEHuFA0U.js (no referer) => Forbidden

修复后

HTML 中已注入所有动态 chunk 的 modulepreload 声明:

ini 复制代码
DApBM9Zf modulepreload => True
ByoahgwI modulepreload => True
BEHuFA0U modulepreload => True
GsJ1Y5_8 modulepreload => True
BB-wqL4x modulepreload => True
C1eDSpvq modulepreload => True
BhVIYuKy modulepreload => True
C9OvFcjC modulepreload => True

浏览器通过 <link> 标签加载时携带 Referer,Nginx 防盗链放行,返回 200。


补充说明:其他可选方案(未采用)

方案 A:修改 Nginx 防盗链规则

valid_referers 中添加 none,允许无 Referer 的请求通过:

nginx 复制代码
valid_referers none *.your-domain.com your-domain.com localhost;
#              ^^^^

或仅对媒体资源启用防盗链,JS/CSS 不做限制:

nginx 复制代码
location ~* .*\.(gif|jpg|ico|png|svg|swf|flv|mp4|webp)$ {
    valid_referers none *.your-domain.com your-domain.com localhost;
    if ($invalid_referer) { return 403; }
}

未采用原因:用户明确要求从构建层面解决问题,不修改 Nginx 配置。且生产环境的 CDN/WAF 配置可能不受前端团队控制。

方案 B:修改 Vite 构建配置修改文件路径

buildAssetsDir/_nuxt/ 改为 /assets/,并在 Nginx 中为 /assets/ 配置独立的 location 规则跳过防盗链。

未采用原因 :本质上仍是依赖 Nginx 配置变更,且未解决 import() 不携带 Referer 的根本问题。


涉及文件清单

文件 操作 说明
server/plugins/preload-all-routes.js 新增 Nitro 插件,通过 Nitro 虚拟模块导入 manifest,注入所有动态 chunk 的 modulepreload
nuxt.config.jsnitro.prerender 修改 启用 crawlLinks 自动爬取预渲染路由,配置 server.host0.0.0.0
相关推荐
kyriewen2 小时前
别再每次都 Google 了:我整理了前端日常最常踩的 10 个 Git 坑,附速查表
前端·javascript·git
一颗奇趣蛋2 小时前
Web 视频开发完全指南:从入门到精通
前端
非洲农业不发达2 小时前
windows终端体验大升级,让你拥有macos级别的美化
前端·后端
妙码生花2 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(十七):登录接口完善,登录页接口整合,解决跨域
前端·后端·ai编程
唐诗2 小时前
改 3 行配置,我的 Tauri dev 冷启动从 100 秒干到 4 秒
前端·客户端
SmartBoyW3 小时前
深入ECMAScript规范:彻底搞懂JS隐式类型转换与底层ToPrimitive机制
前端·javascript
牧艺3 小时前
Cursor Rules / Skills 分层设计:让 Agent 像「团队新同事」
前端·人工智能·cursor
光影少年3 小时前
react navite 跨端核心原理
前端·react native·react.js
monologues3 小时前
Vue 3 渲染器的核心秘密:从 VNode 创建到快速 Diff 算法
前端