---
# System prepended metadata

title: 前端加密 (Nuxt3)
tags: [已重新整理]

---

# Nuxt3 前端加密

如何在 Nuxt3 中實作 RSA 加密功能,保護敏感資料傳輸安全。

---

## 常見加密方法

### 1. MD5 加密
- **特性**: 單向雜湊函數,無法解密
- **用途**: 密碼雜湊、資料完整性驗證
- **安全性**: ⚠️ 不建議用於密碼儲存 (易被暴力破解)

### 2. Base64 編碼
- **特性**: 編碼而非加密,可輕易解碼
- **用途**: 資料傳輸、二進制資料轉文字
- **安全性**: ❌ 無安全性,僅用於編碼

### 3. RSA 加密 (推薦)
- **特性**: 非對稱加密,使用公鑰/私鑰對
- **用途**: 敏感資料加密傳輸
- **安全性**: ✅ 高安全性

---

## RSA 加密原理

### 單向加密
```
前端: 公鑰加密 → 傳送到後端 → 後端: 私鑰解密
```
- 公鑰可以公開
- 私鑰必須保密
- 只能用私鑰解密

### 雙向加密 (更安全)
```
需要 2 組密鑰對

前端:
- 第一組公鑰加密 (發送資料)
- 第二組私鑰解密 (接收資料)

後端:
- 第二組公鑰加密 (發送資料)
- 第一組私鑰解密 (接收資料)
```

**優勢**: 即使前端私鑰洩漏,攻擊者也無法解密後端傳來的資料

---

## 實作方式比較

| 方式 | 優點 | 缺點 | 建議使用場景 |
|------|------|------|-------------|
| **CDN** | 不需安裝套件,減少打包大小 | 依賴外部 CDN,可能有載入延遲 | 專案打包時遇到問題 |
| **NPM** | 本地引入,不依賴外部資源 | 需要額外配置 transpile | 正式專案,穩定性要求高 |

---

## 方法一: 使用 CDN (簡單快速)

### 檔案結構

```
project/
├── composables/
│   └── useJsencrypt.js       # 加密解密邏輯
├── pages/
│   └── encryption.vue         # 示範頁面
├── .env                       # 環境變數 (公私鑰)
└── nuxt.config.ts             # Nuxt 配置
```

---

### 1. 安裝與配置

**nuxt.config.ts**

```typescript
export default defineNuxtConfig({
  app: {
    head: {
      script: [
        {
          src: 'https://cdnjs.cloudflare.com/ajax/libs/jsencrypt/3.3.2/jsencrypt.min.js',
          defer: true
        }
      ]
    }
  },
  
  runtimeConfig: {
    public: {
      // 公鑰 (用於加密)
      jsencryptPublicKey: process.env.NUXT_PUBLIC_JSENCRYPT_PUBLIC_KEY || '',
      // 私鑰 (用於解密)
      jsencryptPrivateKey: process.env.NUXT_PUBLIC_JSENCRYPT_PRIVATE_KEY || ''
    }
  }
})
```

---

### 2. 環境變數設定

**.env**

```bash
# 公鑰 (Public Key) - 用於加密
NUXT_PUBLIC_JSENCRYPT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCKXtONNNt3fYEFhpA7231EN4Ww
UNdXWBigUYCqoxCyfP/zfH+8GhnUFknmiiXTkQqGhIVXdan+cNk5Gq/ji7IAYWH
BquHUgf5N7N2Hi7fYH7z+tAiRPMaIQxsH1Bo6zHW4+a+yB4pioFiKmdsKdzaOZi
BhcWZeO+uAeLJQgfpRpwIDAQAB
-----END PUBLIC KEY-----"

# 私鑰 (Private Key) - 用於解密
NUXT_PUBLIC_JSENCRYPT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQCKXtONNNt3fYEFhpA7231EN4WwUNdXWBigUYCqoxCyfP/zfH+8
GhnUFknmiiXTkQqGhIVXdan+cNk5Gq/ji7IAYWHBquHUgf5N7N2Hi7fYH7z+tAiR
PMaIQxsH1Bo6zHW4+a+yB4pioFiKmdsKdzaOZiBhcWZeO+uAeLJQgfpRpwIDAQAB
AoGAIVgioMeZD5115w/7WAFXmYXLuKZyjkDThma9m+E519lZkKJy4bBkgwBwBJdL
8ETmhW4P9/iJ45/sKN+ufSYf2a6qygrjjk27087ViEL9TWDtReN1i1izvPuT12x1
sR50YE0Zokl+ZOSHl3lOZuAWgmBgYnoj2XfYlDXf63tPVGECQQDKZP8s/JYbSrii
DZJypEZ7L6lrNZsOaz2ljWOUqfwXc1ucazOAGV24xUpQsZJj1oNRRWIoKRp8Xdly
abvyXZy3AkEArwTJypcT0EXyttxbNe/SDnSuYJznyGmPpcIcx0H9P/eSr8KEvD3f
CjlFXG/2TxoKzsjksZNxG6PTj71huT3ikQJAQmTtXNmjeGKDvQ2MvDTtth2Fi1jz
e8BsYbHHOA6nVx4NsHtuUph/qUx3O491AXFudKu5LomFWcUDv0e2UySocwJAOClL
L41HOGAZwH/5bNdmCml6w1nWLsNg6wnc/ju4rlwdX/UFbvpTpg42qbDr0vpCWZSx
fiIX+4yYiNl2kGDBYQJAYr+d8az1RxinmFEzMuPvkfcXrUyC/3Hn2/eJZQAgBjYe
uBXDhMlQUfCEfyQ/tLfDtSOsCwTjn0C/JnQsLxjtZg==
-----END RSA PRIVATE KEY-----"
```

**⚠️ 重要提醒:**
- 請替換為你自己產生的密鑰對
- 私鑰絕對不要提交到版本控制系統
- 將 `.env` 加入 `.gitignore`

---

### 3. Composable 實作

**composables/useJsencrypt.js**

```javascript
export const useJsencrypt = () => {
  // 取得配置
  const config = useRuntimeConfig()

  /**
   * 加密資料
   * @param {string} data - 要加密的資料
   * @returns {string|false} 加密後的字串,失敗返回 false
   */
  const encryptData = (data) => {
    if (!process.client) return null
    if (!data) return null

    try {
      // 創建 JSEncrypt 實例
      const encrypt = new JSEncrypt()

      // 設置公鑰
      const publicKey = config.public.jsencryptPublicKey
      if (!publicKey) {
        console.error('未設定公鑰')
        return null
      }

      encrypt.setPublicKey(publicKey)

      // 使用公鑰加密
      const encryptedData = encrypt.encrypt(data)

      if (!encryptedData) {
        console.error('加密失敗')
        return null
      }

      return encryptedData
    } catch (error) {
      console.error('加密過程發生錯誤:', error)
      return null
    }
  }

  /**
   * 解密資料
   * @param {string} data - 要解密的資料
   * @returns {string|false} 解密後的字串,失敗返回 false
   */
  const decryptData = (data) => {
    if (!process.client) return null
    if (!data) return null

    try {
      // 創建 JSEncrypt 實例
      const decrypt = new JSEncrypt()

      // 設置私鑰
      const privateKey = config.public.jsencryptPrivateKey
      if (!privateKey) {
        console.error('未設定私鑰')
        return null
      }

      decrypt.setPrivateKey(privateKey)

      // 使用私鑰解密
      const decryptedData = decrypt.decrypt(data)

      if (!decryptedData) {
        console.error('解密失敗')
        return null
      }

      return decryptedData
    } catch (error) {
      console.error('解密過程發生錯誤:', error)
      return null
    }
  }

  return {
    encryptData,
    decryptData
  }
}
```

---

### 4. 頁面使用範例

**pages/encryption.vue**

```vue
<template>
  <div class="encryption-demo">
    <h2>RSA 加密示範</h2>

    <!-- 顯示區域 -->
    <div class="result-box">
      <h3>{{ isEncrypted ? '加密結果' : '原始內容' }}</h3>
      <div class="result-content">
        {{ displayText }}
      </div>
    </div>

    <!-- 控制按鈕 -->
    <div class="button-group">
      <button 
        @click="handleEncrypt" 
        :disabled="isEncrypted"
        class="btn btn-primary"
      >
        🔒 加密
      </button>
      
      <button 
        @click="handleDecrypt" 
        :disabled="!isEncrypted"
        class="btn btn-secondary"
      >
        🔓 解密
      </button>
      
      <button 
        @click="handleReset"
        class="btn btn-outline"
      >
        🔄 重置
      </button>
    </div>

    <!-- 狀態提示 -->
    <div v-if="statusMessage" class="status-message" :class="statusType">
      {{ statusMessage }}
    </div>
  </div>
</template>

<script setup>
// 引入加密功能
const { encryptData, decryptData } = useJsencrypt()

// 原始資料
const originalText = ref('This is a secret message! 這是一段機密訊息!')

// 顯示的文字
const displayText = ref(originalText.value)

// 是否已加密
const isEncrypted = ref(false)

// 狀態訊息
const statusMessage = ref('')
const statusType = ref('') // 'success' | 'error'

/**
 * 顯示狀態訊息
 */
const showStatus = (message, type = 'success') => {
  statusMessage.value = message
  statusType.value = type
  
  // 3 秒後清除訊息
  setTimeout(() => {
    statusMessage.value = ''
    statusType.value = ''
  }, 3000)
}

/**
 * 加密處理
 */
const handleEncrypt = () => {
  const encrypted = encryptData(originalText.value)
  
  if (encrypted) {
    displayText.value = encrypted
    isEncrypted.value = true
    showStatus('✅ 加密成功', 'success')
  } else {
    showStatus('❌ 加密失敗,請檢查公鑰設定', 'error')
  }
}

/**
 * 解密處理
 */
const handleDecrypt = () => {
  const decrypted = decryptData(displayText.value)
  
  if (decrypted) {
    displayText.value = decrypted
    isEncrypted.value = false
    showStatus('✅ 解密成功', 'success')
  } else {
    showStatus('❌ 解密失敗,請檢查私鑰設定', 'error')
  }
}

/**
 * 重置
 */
const handleReset = () => {
  displayText.value = originalText.value
  isEncrypted.value = false
  showStatus('🔄 已重置', 'success')
}
</script>

<style scoped>
.encryption-demo {
  max-width: 800px;
  margin: 2rem auto;
  padding: 2rem;
}

h2 {
  text-align: center;
  margin-bottom: 2rem;
  color: #2c3e50;
}

.result-box {
  background: #f8f9fa;
  border: 2px solid #dee2e6;
  border-radius: 8px;
  padding: 1.5rem;
  margin-bottom: 2rem;
}

.result-box h3 {
  margin-top: 0;
  margin-bottom: 1rem;
  color: #495057;
  font-size: 1rem;
}

.result-content {
  min-height: 150px;
  padding: 1rem;
  background: white;
  border-radius: 4px;
  word-break: break-all;
  font-family: 'Courier New', monospace;
  font-size: 0.9rem;
  line-height: 1.6;
  color: #212529;
}

.button-group {
  display: flex;
  gap: 1rem;
  justify-content: center;
  margin-bottom: 1rem;
}

.btn {
  padding: 0.75rem 1.5rem;
  border: none;
  border-radius: 6px;
  font-size: 1rem;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.3s ease;
}

.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.btn-primary {
  background: #007bff;
  color: white;
}

.btn-primary:hover:not(:disabled) {
  background: #0056b3;
  transform: translateY(-2px);
  box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
}

.btn-secondary {
  background: #6c757d;
  color: white;
}

.btn-secondary:hover:not(:disabled) {
  background: #545b62;
  transform: translateY(-2px);
  box-shadow: 0 4px 8px rgba(108, 117, 125, 0.3);
}

.btn-outline {
  background: white;
  color: #6c757d;
  border: 2px solid #6c757d;
}

.btn-outline:hover {
  background: #6c757d;
  color: white;
  transform: translateY(-2px);
}

.status-message {
  text-align: center;
  padding: 1rem;
  border-radius: 6px;
  font-weight: 500;
}

.status-message.success {
  background: #d4edda;
  color: #155724;
  border: 1px solid #c3e6cb;
}

.status-message.error {
  background: #f8d7da;
  color: #721c24;
  border: 1px solid #f5c6cb;
}
</style>
```

---

## 方法二: 使用 NPM 安裝 (推薦正式專案)

### 1. 安裝套件

```bash
npm install jsencrypt
```

---

### 2. 檔案結構

```
project/
├── plugins/
│   └── jsencrypt.client.js    # Plugin 封裝
├── pages/
│   └── encryption.vue          # 示範頁面
├── .env                        # 環境變數
└── nuxt.config.ts              # Nuxt 配置
```

---

### 3. 配置 Nuxt

**nuxt.config.ts**

```typescript
export default defineNuxtConfig({
  build: {
    // 重要: 需要 transpile jsencrypt
    transpile: ['jsencrypt']
  },
  
  runtimeConfig: {
    public: {
      jsencryptPublicKey: process.env.NUXT_PUBLIC_JSENCRYPT_PUBLIC_KEY || '',
      jsencryptPrivateKey: process.env.NUXT_PUBLIC_JSENCRYPT_PRIVATE_KEY || ''
    }
  }
})
```

---

### 4. Plugin 封裝

**plugins/jsencrypt.client.js**

```javascript
import JSEncrypt from 'jsencrypt'

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

  /**
   * 加密資料
   */
  const encryptData = (data) => {
    if (!data) return null

    try {
      const encrypt = new JSEncrypt()
      const publicKey = config.public.jsencryptPublicKey
      
      if (!publicKey) {
        console.error('未設定公鑰')
        return null
      }

      encrypt.setPublicKey(publicKey)
      const encryptedData = encrypt.encrypt(data)

      if (!encryptedData) {
        console.error('加密失敗')
        return null
      }

      return encryptedData
    } catch (error) {
      console.error('加密錯誤:', error)
      return null
    }
  }

  /**
   * 解密資料
   */
  const decryptData = (data) => {
    if (!data) return null

    try {
      const decrypt = new JSEncrypt()
      const privateKey = config.public.jsencryptPrivateKey
      
      if (!privateKey) {
        console.error('未設定私鑰')
        return null
      }

      decrypt.setPrivateKey(privateKey)
      const decryptedData = decrypt.decrypt(data)

      if (!decryptedData) {
        console.error('解密失敗')
        return null
      }

      return decryptedData
    } catch (error) {
      console.error('解密錯誤:', error)
      return null
    }
  }

  // 提供給全域使用
  return {
    provide: {
      encryptData,
      decryptData
    }
  }
})
```

---

### 5. 頁面使用

**pages/encryption.vue**

```vue
<script setup>
// 從 plugin 取得加密方法
const { $encryptData, $decryptData } = useNuxtApp()

const originalText = ref('This is a secret message!')
const displayText = ref(originalText.value)
const isEncrypted = ref(false)

const handleEncrypt = () => {
  const encrypted = $encryptData(originalText.value)
  if (encrypted) {
    displayText.value = encrypted
    isEncrypted.value = true
  }
}

const handleDecrypt = () => {
  const decrypted = $decryptData(displayText.value)
  if (decrypted) {
    displayText.value = decrypted
    isEncrypted.value = false
  }
}
</script>

<template>
  <!-- 與方法一相同的 template -->
</template>
```

---

## 產生 RSA 密鑰對

### 線上工具 (快速)

- [travistidwell.com/jsencrypt/demo](http://travistidwell.com/jsencrypt/demo/)
- [cryptotools.net/rsagen](https://cryptotools.net/rsagen)

### 使用 OpenSSL (推薦)

```bash
# 1. 產生私鑰
openssl genrsa -out private_key.pem 1024

# 2. 從私鑰產生公鑰
openssl rsa -in private_key.pem -pubout -out public_key.pem

# 3. 查看私鑰內容
cat private_key.pem

# 4. 查看公鑰內容
cat public_key.pem
```

**安全建議:**
- 生產環境使用 2048 位元以上: `openssl genrsa -out private_key.pem 2048`
- 私鑰務必妥善保管,不要提交到版本控制
- 定期更換密鑰對

---

## 實際應用場景

### 1. 登入密碼加密

```vue
<script setup>
const { encryptData } = useJsencrypt()

const login = async (username, password) => {
  // 加密密碼
  const encryptedPassword = encryptData(password)
  
  // 發送到後端
  await $fetch('/api/login', {
    method: 'POST',
    body: {
      username,
      password: encryptedPassword
    }
  })
}
</script>
```

---

### 2. 敏感資料傳輸

```vue
<script setup>
const { encryptData } = useJsencrypt()

const submitCreditCard = async (cardNumber) => {
  // 加密信用卡號
  const encryptedCard = encryptData(cardNumber)
  
  await $fetch('/api/payment', {
    method: 'POST',
    body: {
      card: encryptedCard
    }
  })
}
</script>
```

---

### 3. 雙向加密通訊

```javascript
// composables/useSecureApi.js
export const useSecureApi = () => {
  const { encryptData, decryptData } = useJsencrypt()

  const securePost = async (url, data) => {
    // 加密請求資料
    const encryptedData = encryptData(JSON.stringify(data))
    
    // 發送請求
    const response = await $fetch(url, {
      method: 'POST',
      body: { data: encryptedData }
    })
    
    // 解密回應資料
    const decryptedResponse = decryptData(response.data)
    return JSON.parse(decryptedResponse)
  }

  return { securePost }
}
```

---

## 常見問題

### Q: Build 時出現 `Cannot find module 'jsencrypt'` 錯誤?

A: 需要在 `nuxt.config.ts` 中設定 transpile:

```typescript
export default defineNuxtConfig({
  build: {
    transpile: ['jsencrypt']
  }
})
```

---

### Q: 為什麼私鑰會暴露在前端?

A: 
- **單向加密**: 前端只需要公鑰加密,私鑰在後端解密
- **雙向加密**: 前端確實需要私鑰,但這是用來解密後端傳來的資料,無法解密前端發出的資料 (使用不同密鑰對)

**正確做法:**
```
前端發送: 後端公鑰加密 → 後端私鑰解密
後端發送: 前端公鑰加密 → 前端私鑰解密
```

---

### Q: RSA 加密的資料長度有限制嗎?

A: 有的!RSA 只能加密較短的資料。

**限制:**
- 1024 位元密鑰: 最多 117 bytes
- 2048 位元密鑰: 最多 245 bytes

**解決方案:**
1. 分段加密長資料
2. 使用 RSA 加密對稱密鑰 (如 AES),再用對稱密鑰加密資料 (混合加密)

---

### Q: CDN 載入失敗怎麼辦?

A: 可以下載 jsencrypt.min.js 放在 `public/js/` 目錄:

```typescript
// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      script: [
        { src: '/js/jsencrypt.min.js', defer: true }
      ]
    }
  }
})
```

---

### Q: 如何驗證加密解密是否正常?

A: 可以在瀏覽器 Console 測試:

```javascript
// 1. 加密
const encrypted = $encryptData('test message')
console.log('加密結果:', encrypted)

// 2. 解密
const decrypted = $decryptData(encrypted)
console.log('解密結果:', decrypted) // 應該是 'test message'
```

---

## 安全建議

### ✅ 應該做的

1. **使用 HTTPS**: RSA 加密只是多一層保護,仍需 HTTPS
2. **密鑰管理**: 私鑰不要硬編碼,使用環境變數
3. **定期更換**: 定期更換密鑰對
4. **長度足夠**: 生產環境使用 2048 位元以上
5. **錯誤處理**: 加密失敗時要有適當處理

### ❌ 不應該做的

1. **不要單獨使用**: 不能取代 HTTPS
2. **不要儲存私鑰**: Git 中不要提交私鑰
3. **不要過度依賴**: 前端加密容易被繞過,後端仍需驗證
4. **不要加密大檔案**: RSA 不適合大量資料加密
5. **不要重複使用**: 不同專案使用不同密鑰對

---

## 效能優化

### 1. 延遲載入

```vue
<script setup>
const { encryptData, decryptData } = useJsencrypt()
const isLoaded = ref(false)

onMounted(() => {
  // 確認 JSEncrypt 已載入
  if (process.client && window.JSEncrypt) {
    isLoaded.value = true
  }
})
</script>
```

---

### 2. 快取加密實例

```javascript
// composables/useJsencrypt.js
let encryptInstance = null
let decryptInstance = null

export const useJsencrypt = () => {
  const config = useRuntimeConfig()

  const getEncryptInstance = () => {
    if (!encryptInstance) {
      encryptInstance = new JSEncrypt()
      encryptInstance.setPublicKey(config.public.jsencryptPublicKey)
    }
    return encryptInstance
  }

  const getDecryptInstance = () => {
    if (!decryptInstance) {
      decryptInstance = new JSEncrypt()
      decryptInstance.setPrivateKey(config.public.jsencryptPrivateKey)
    }
    return decryptInstance
  }

  // ...
}
```

---

## 參考資源

- [JSEncrypt 官方文檔](https://travistidwell.com/jsencrypt/)
- [RSA 加密原理](https://zh.wikipedia.org/wiki/RSA%E5%8A%A0%E5%AF%86%E6%BC%94%E7%AE%97%E6%B3%95)
- [OpenSSL 工具](https://www.openssl.org/)
- [MDN - Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API)

<!-- ---

## 進階主題

### 使用 Web Crypto API (原生)

現代瀏覽器支援原生加密 API,無需額外套件:

```javascript
// 產生密鑰對
const generateKeyPair = async () => {
  return await window.crypto.subtle.generateKey(
    {
      name: 'RSA-OAEP',
      modulusLength: 2048,
      publicExponent: new Uint8Array([1, 0, 1]),
      hash: 'SHA-256'
    },
    true,
    ['encrypt', 'decrypt']
  )
}

// 加密
const encrypt = async (publicKey, data) => {
  const encoded = new TextEncoder().encode(data)
  return await window.crypto.subtle.encrypt(
    { name: 'RSA-OAEP' },
    publicKey,
    encoded
  )
}
```

**優勢:** 原生支援,效能更好,無需載入額外套件  
**劣勢:** API 較複雜,瀏覽器相容性需注意 -->