Mike

3 July, 2025

Vite 與 Nuxt 環境變數的安全遷移指南!

問題背景

過去在做專案的遷移的時候,許多開發者在使用 Nuxt3 時,會延續 Nuxt2 或其他框架的習慣,使用 vite.define 來處理環境變數。這種方式超級不安全的,近期就發現公司其他的專案環境變數的 token 居然就外洩出去了,然後追查中發現居然就是 vite.define 所造成的問題,所以這次我特別寫了這篇文章來存檔,除了提醒自己以後要特別注意外,也讓其他還沒有遇到問題的朋友可以特別注意。

因為主要這件事情網路上去搜尋很多資料都還是教大家使用 vite.define 來搭配 process.env ,但這會造成你在 build 的時候把你的一些隱秘的 token 什麼的打包到 dist 或是 .output 資料夾中,這真的太危險了!

// ❌ 不安全的做法
// nuxt.config.js

export default defineNuxtConfig({
  vite: {
    define: {
      'process.env':process.env
    },
  }
});
// 在 components/ 或 page/ 中使用
const payToken = process.env.PAY_TOKEN; // 🚨 超危險!
const slackToken = process.env.SLACK_TOKEN;  // 🚨 超危險!

為什麼需要遷移?

  1. 安全性問題:所有環境變數都暴露到 Client 端
  2. 違反官方文件建議的方式:不符合 Nuxt3 官方建議的使用環境變數的方法
  3. 維護成本變高:混合了 Public 和 Private 配置

安全風險分析

🚨 vite.define 的嚴重安全風險

1. 敏感資料完全暴露

// Build 後的 JavaScript 中會直接出現:
const payToken = "app_clpTkttGNzsJYcqwc1232aBFxS4"; // 🚨 直接暴露!
const slackToken = "xoxb-1234567890123-abc2ff13ddefgh"; // 🚨 極度危險!
 

2. 攻擊者可以做什麼?

  • 竊取 API 金鑰:直接從瀏覽器開發者工具提取
  • 未授權 API 調用:使用你的金鑰發送請求,產生費用
  • 資料庫外洩:如果資料庫密碼暴露
  • 服務濫用:使用你的第三方服務配額
  • 內部資訊洩露:Slack token 可以讀取內部對話

3. 真實案例風險

  • 支付 API 金鑰外洩:攻擊者可以冒充你的應用發起支付
  • Slack Bot Token 外洩:竊取公司內部敏感對話
  • 第三方簡訊廠商被盜用:被盜用簡訊廠商服務,被打了2000萬封簡訊


🔍 如何檢查你的應用是否有風險

  1. 打開瀏覽器開發者工具 (F12)!
  2. 搜尋你的敏感資料 (例如 "app_clpTkttGNzsJYcqwc1232aBFxS4") 等 token 字眼!
  3. 或是 IDE 直接搜尋 build 後的資料夾 ( dist 或是 .output )

解決方案對比

✅ Nuxt3 runtimeConfig 的優勢

特點 vite.define Nuxt3 runtimeConfig
安全性 ❌ 全部暴露 ✅ 自動區分公開/私密
效能 ❌ Bundle 膨脹 ✅ 支援 Tree Shaking
動態替換 ❌ Build 時固定 ✅ Runtime 替換
TypeScript ❌ 類型不安全 ✅ 自動類型推斷
SSR / CSR ❌ 設定上混亂 ✅ 完美支援

Nuxt3 runtimeConfig 的自動安全機制

使用官方文件的推薦的方式 runtimeConfig 可以避免這類的問題,以及 .env 的環境變數加上 NUXT_PUBLIC_NUXT_ 前綴字 Nuxt 就可以自動帶入你的 .env 的環境變數。

環境變數命名規則

// nuxt.config.js
export default defineNuxtConfig({
  runtimeConfig: {
    // Private 私密(只在 server-side)
    payToken: '',     // NUXT_PAY_TOKEN
    slackToken: '',  // NUXT_SLACK_TOKEN
    
    public: {
      // Public 公開(client + server)
      apiUrl: '',       // NUXT_PUBLIC_API_URL
      gaId: '',         // NUXT_PUBLIC_GA_ID
    }
  }
});
// .env
NUXT_PUBLIC_API_URL=https://staging.api.com/
NUXT_PUBLIC_GA_ID=G-3BMYF8K234

NUXT_PAY_TOKEN=app_clpTkttGNzsJYcqwc1232aBFxS4
NUXT_SLACK_TOKEN=xoxb-1234567890123-abc2ff13ddefgh

如何在 Client 端,也就是像 components/ 或 page/ 中使用

// ❌ 傳統舊專案的方法
<script setup>
     const apiUrl = process.env.API_URL;
</script>


// ✅ Nuxt官方所提供的方法
<script setup>
     const config = useRuntimeConfig();
     const apiUrl = config.public.apiUrl;

     // 只在 Client 端可用
     if (import.meta.client) {
        console.log('Public config:', config.public);
     }
</script>

如何在 Server 端中使用

// server/api/payment.js

export default defineEventHandler(async (event) => {
  const config = useRuntimeConfig(event);
  
  // ✅ 可以存取所有的環境變數
  const payToken= config.tapPayAppKey;  //  Private 
  const apiUrl = config.public.apiUrl;   //  Public
  
  // 處理後續邏輯...
});

process.server 或 process.client 遷移

// ❌ 傳統專案的舊方法
if (process.server) {
  console.log('Server');
}
if (process.client) {
  console.log('Client');
}

// ✅ Vite 以及 Nuxt 的新方法
if (import.meta.server) {
  console.log('Server');
}
if (import.meta.client) {
  console.log('Client');
}

使用 process.env 實際遇到的情境案例

API 檔案使用環境變數重構

關於 API 的管理跟架構可以參考我之前寫的這篇文章,使用Axios你的API都怎麼管理?

❌ 舊的專案 API 使用方式

// app/api/apiUtils/apiDomain.js
export const apiDomain = () => {
  return process.env.API_URL;
};

// app/api/apiAuth.js
import axios from 'axios';
import { apiDomain } from '@/api/apiUtils/apiDomain.js';

const authRequest = axios.create({
   baseURL: process.env.API_URL,
});

接下來你改成 runtimeConfig 就會出現 ❌模組化 API 檔案的 composable 的錯誤問題

// app/api/apiUtils/apiDomain.js
export const apiDomain = () => {
  const config = useRuntimeConfig(); // ❌ 錯誤:在模組載入時執行
  return config.public.apiUrl;
};

// app/api/apiAuth.js
import { apiDomain } from '@/api/apiUtils/apiDomain.js';
const authRequest = axios.create({
  baseURL: apiDomain(), // ❌ 立即執行,沒有 Nuxt context
});

✅ 正確的處理方式
解決方案:Plugin + Composable

// plugins/api-create.js

import axios from 'axios';

export default defineNuxtPlugin(() => {
  const config = useRuntimeConfig();

  const request = axios.create({
    baseURL: config.public.apiUrl,
  });

  return {
    provide: { request },
  };
});

建立一個 Composable

// 建立 composables/useApiCreate.js
export const useApiCreate = () => {
  const { $request } = useNuxtApp();
  return $request;
};
然後就可以從 useApiCreate 中取得 api 的 request 方法,包含你的環境變數
// app/api/apiAuth.js

import { useApiCreate } from '@/composables/useApiCreate.js';

/*
 *  取得用戶的 info 資料 API
 */
export const apiGetUserInf0Auth = data => {
  const request = useApiCreate();
  return request.get('/api/user/info');
};

常見問題

Q1: Client 端讀取不到 Private 配置的環境變數怎麼辦?
A: 這是正確的安全使用方式!Private 配置只能在 server-side 使用:

// ✅ 正確:在 server API 中使用
// server/api/auth.js
export default defineEventHandler(async (event) => {
      const config = useRuntimeConfig(event);
      const payToken = config.payToken; // 可以存取
});

// ❌ 錯誤:在 Client 端嘗試存取
// pages/index.vue
<script setup>
      const config = useRuntimeConfig();
      console.log(config.payToken);     // undefined (這是安全機制)
</script>


Q2: TypeScript 支援如何配置?

A: Nuxt3 會自動生成類型,但也可以手動定義:

// types/runtime-config.d.ts

declare module 'nuxt/schema' {
  interface RuntimeConfig {
    payToken: string;
    slackToken: string;
  }
  
  interface PublicRuntimeConfig {
    apiUrl: string;
    gaId: string;
  }
}
export {}

最佳實踐建議

1. 安全性最佳實踐

  • 永遠不要在 runtimeConfig.public 中放置敏感資料
  • 定期檢查 Client 端 bundle 是否包含敏感資訊
  • 使用後端代理模式處理敏感 API 呼叫
  • 為不同環境使用不同的金鑰

2. 效能最佳實踐

  • 使用 import.meta.server/client 而不是 process.server/client
  • 對於簡單請求盡量使用 $fetch 而不是 axios

3. 開發體驗最佳實踐

  • 建立統一的 API client 管理
  • 使用 TypeScript 確保類型安全
  • 建立清楚的環境變數文件

總結

vite.define 遷移到 Nuxt3 runtimeConfig 不僅是一個技術升級,更是一個重要的安全性提升。這個遷移可以:

  • 🛡️ 防止敏感資料洩露:自動區分公開和私密配置
  • 🚀 提升應用效能:享受 Tree Shaking 和其他優化
  • 📱 改善開發體驗:TypeScript 支援和更好的 IDE 整合
  • 🔄 支援動態配置:Runtime 環境變數替換
  • ✅ 符合最佳實踐:遵循 Nuxt3 官方建議

雖然遷移需要一些工作,但考慮到安全性和長期維護性,這是一個必要且值得的投資

記住:安全性不是可選的,而是必需的。趕快開始你的環境變數遷移計畫,保護你的敏感資料!