cookie & sessionStorage加密 (crypto-js)
Cookie / SessionStorage 加密(crypto-js)
TL;DR前端用 crypto-js AES 加密 user/token 後存進 cookie / sessionStorage 是「障眼法」,不是真安全。真正的 secret 永遠不該存在前端。要做 auth → 用 httpOnly Secure cookie + server session,讓 JS 完全讀不到 token。
這個 pattern 的根本問題加密金鑰也在前端 → 駭客 F12 看 source code 就能找到 → 加密只是 obfuscation,不是 security。真正的安全層必須在 server side。
還是有合理用途
雖然 不能拿來存 secret,但加密後存仍有 非安全性 的好處:
| 用途 | 是否合理 |
|---|---|
| 存 非敏感的使用者偏好(避免明文被肉眼看到) | ✅ 合理 |
| 存 Auth token / Session token | ❌ 根本不該這樣 |
| 存 密碼 | ❌ 永遠不要 |
| 存 信用卡資訊 | ❌ 永遠不要 |
| 存 使用者 PII(個資) | ❌ 違反 GDPR/個資法 |
| 存 AI prompt 設定 / 暫存草稿 | ✅ 合理 |
crypto-js AES 基本用法
npm install crypto-js
npm install -D @types/crypto-js
import CryptoJS from 'crypto-js';
// 加密
const plaintext = 'Hello, world!';
const passphrase = 'mySecretPassphrase';
const ciphertext = CryptoJS.AES.encrypt(plaintext, passphrase).toString();
// 解密
const bytes = CryptoJS.AES.decrypt(ciphertext, passphrase);
const decrypted = bytes.toString(CryptoJS.enc.Utf8);
passphrase 來源程式碼裡 絕對不要寫死 passphrase。從 process.env.NEXT_PUBLIC_PASSPHRASE 讀? 那也只是延遲被找到的時間,bundle 裡仍然有。如果加密金鑰必須能被前端讀到,那這個加密就不是真的安全。接受這個事實後,只把它當「不要明文存」的層級即可。
包成 wrapper(實際使用)
// utils/secure-storage.ts
import CryptoJS from 'crypto-js';
const PASSPHRASE = process.env.NEXT_PUBLIC_STORAGE_KEY ?? 'fallback-key';
export function encryptValue<T>(value: T): string {
const json = JSON.stringify(value);
return CryptoJS.AES.encrypt(json, PASSPHRASE).toString();
}
export function decryptValue<T>(ciphertext: string): T | null {
try {
const bytes = CryptoJS.AES.decrypt(ciphertext, PASSPHRASE);
const json = bytes.toString(CryptoJS.enc.Utf8);
return JSON.parse(json) as T;
} catch {
return null;
}
}
// SessionStorage wrapper
export const SecureSessionStorage = {
set<T>(key: string, value: T) {
sessionStorage.setItem(key, encryptValue(value));
},
get<T>(key: string): T | null {
const item = sessionStorage.getItem(key);
return item ? decryptValue<T>(item) : null;
},
remove(key: string) {
sessionStorage.removeItem(key);
},
};
Cookie vs SessionStorage 對比
| 特性 | Cookie | SessionStorage |
|---|---|---|
| 容量 | 4KB | ~5-10MB |
| 跨分頁 | ✅ 共享 | ❌ 每個分頁獨立 |
| 過期 | 可設 expires / Max-Age | 關閉分頁清除 |
| 每次 request 自動帶 | ✅ 是 | ❌ 否 |
| httpOnly 保護 | ✅ 有(JS 讀不到) | ❌ 無 |
| CSRF 風險 | ⚠️ 有(自動帶) | ❌ 沒有(JS 主動讀) |
| 適合 | server-side auth | 短期 client-only state |
Auth 安全鐵則Auth token 一定要走 httpOnly cookie。
- JS 完全讀不到,XSS 偷不走
- 加上
Secureflag,只走 HTTPS- 加上
SameSite=Strict或Lax,擋 CSRF- 設合理
Max-Age,過期自動失效
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict; Max-Age=86400; Path=/
那原本程式碼裡的做法呢?
// ❌ 這段 code 看起來有加密,但仍不安全
const onLogin = async (e) => {
e.preventDefault();
const userCredential = await signInWithEmailAndPassword(auth, email, password);
const user = userCredential.user;
const ciphertext = CryptoJS.AES.encrypt(JSON.stringify(user), process.env.passphrase).toString();
document.cookie = `user=${ciphertext}`; // ⚠️ 不是 httpOnly!
router.push('/');
};
這寫法有 3 個問題:
document.cookieset 出來不是 httpOnly,JS 讀得到 → XSS 危險- 沒設 Secure / SameSite
process.env.passphrase沒加NEXT_PUBLIC_前綴,在 client 是 undefined → 加密失敗
正確做法:讓 server 設 cookie:
// server-side: app/api/login/route.ts(Next.js App Router)
import { cookies } from 'next/headers';
export async function POST(req: Request) {
const { email, password } = await req.json();
// 在 server 驗證(不是 client)
const sessionToken = await createServerSession(email, password);
cookies().set('session', sessionToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 24,
path: '/',
});
return Response.json({ ok: true });
}
Client 只發 POST 請求,token 由 server 處理,JS 完全摸不到。這才是真正的 auth 安全做法。
Cookie 讀取(若真的要用)
如果只是做「使用者偏好」這類 非敏感 的東西,可以用前端讀寫:
import CryptoJS from 'crypto-js';
function getCookie(name: string): string | null {
const match = document.cookie
.split('; ')
.find((row) => row.startsWith(`${name}=`));
return match ? decodeURIComponent(match.split('=')[1]) : null;
}
const cipher = getCookie('user-prefs');
if (cipher) {
try {
const bytes = CryptoJS.AES.decrypt(cipher, PASSPHRASE);
const prefs = JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
setUserPrefs(prefs);
} catch {
setUserPrefs(null);
}
}
但說真的,使用者偏好沒人會想看,就直接明文 JSON 存 LocalStorage 就好,加密反而讓除錯困難。
SessionStorage 版本
const onLogin = async (e: React.FormEvent) => {
e.preventDefault();
const userCredential = await signInWithEmailAndPassword(auth, email, password);
const user = userCredential.user;
// ⚠️ 仍不是真安全(同上),但比明文好一點
const ciphertext = CryptoJS.AES.encrypt(
JSON.stringify(user),
process.env.NEXT_PUBLIC_PASSPHRASE!,
).toString();
sessionStorage.setItem('user', ciphertext);
router.push('/');
};
SessionStorage 比 cookie 略安全(沒 CSRF 自動帶)但仍不抗 XSS。
何時可以用 crypto-js
| 情境 | 適不適合 |
|---|---|
| 分享連結 內含加密參數(可被拆但設計上 OK) | ✅ 合理 |
| 前端表單草稿(避免重要 PII 明文 localStorage) | ✅ 遮蓋肉眼,不抗駭 |
| 離線 PWA 暫存敏感資料 | ⚠️ 需配合裝置加密 |
| Auth / 支付 / 個資 | ❌ 絕對不行 |
替代:WebCrypto API(原生,更現代)
2026 年首選:瀏覽器原生 WebCrypto,不用 import 套件:
async function encryptText(plaintext: string, password: string): Promise<string> {
const enc = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw', enc.encode(password), { name: 'PBKDF2' }, false, ['deriveKey']
);
const salt = crypto.getRandomValues(new Uint8Array(16));
const key = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: 100_000, hash: 'SHA-256' },
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false, ['encrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv }, key, enc.encode(plaintext)
);
// 把 salt + iv + ciphertext 拼成可儲存格式
return btoa(String.fromCharCode(...salt, ...iv, ...new Uint8Array(ciphertext)));
}
優:零依賴 + AES-GCM 比 crypto-js AES-CBC 安全(GCM 有完整性驗證) 劣:寫起來囉嗦,但可包成 utility
總結:資安鐵則
資安原則
- 前端任何 secret 都不是真 secret(JS 都看得到)
- Auth / Session / 真敏感資料 → 必須 server-side(httpOnly cookie)
- ==前端加密 = obfuscation,不是 encryption==
- 不要把使用者個資存 client side
- 加密金鑰 永遠在 server