# 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 較複雜,瀏覽器相容性需注意 -->