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