在 Pinia 插件中确保加密密钥的安全性是保护敏感数据的核心环节。由于前端环境的特殊性(代码和资源可被用户访问),密钥的管理需要特别谨慎。以下是几种确保密钥安全性的实践方案:
1. 避免硬编码密钥
问题:直接在代码中硬编码密钥(如 const SECRET_KEY = "my-key")会导致密钥随代码一起暴露(可通过浏览器开发者工具查看)。
解决方案:
从后端动态获取密钥,且密钥具有时效性(短期有效)。
实现示例:
import CryptoJS from 'crypto-js';
// 缓存密钥(内存中,页面刷新后失效)
let SECRET_KEY = null;
// 从后端获取临时密钥
const fetchSecretKey = async () => {
try {
const response = await fetch('/api/get-encryption-key', {
method: 'GET',
credentials: 'include' // 携带登录态,确保只有授权用户能获取
});
if (!response.ok) throw new Error('Failed to fetch key');
const { key, expiresIn } = await response.json();
SECRET_KEY = key;
// 密钥过期后自动清除(例如 1 小时后失效)
setTimeout(() => {
SECRET_KEY = null;
}, expiresIn * 1000);
return key;
} catch (error) {
console.error('密钥获取失败:', error);
throw new Error('加密功能不可用');
}
};
// 加密函数(确保密钥已获取)
const encrypt = async (data) => {
if (!SECRET_KEY) await fetchSecretKey();
return CryptoJS.AES.encrypt(JSON.stringify(data), SECRET_KEY).toString();
};
// 解密函数(确保密钥已获取)
const decrypt = async (ciphertext) => {
if (!ciphertext) return null;
if (!SECRET_KEY) await fetchSecretKey();
const bytes = CryptoJS.AES.decrypt(ciphertext, SECRET_KEY);
return JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
};
// Pinia 加密插件(使用异步加解密)
export const encryptedStoragePlugin = ({ store }) => {
// 初始化时解密恢复状态
const init = async () => {
const storedState = localStorage.getItem(`encrypted_${store.$id}`);
if (storedState) {
try {
const decryptedState = await decrypt(storedState);
store.$patch(decryptedState);
} catch (error) {
console.error('状态解密失败:', error);
}
}
};
init();
// 监听状态变化并加密存储
store.$subscribe(async (mutation, state) => {
try {
const encryptedState = await encrypt(state);
localStorage.setItem(`encrypted_${store.$id}`, encryptedState);
} catch (error) {
console.error('状态加密失败:', error);
}
}, { detached: true });
};
核心思路:
- 密钥由后端生成,通过接口动态返回(需用户登录后才能访问接口)。
- 密钥在内存中临时存储,页面刷新或过期后自动失效,避免持久化到本地。
- 后端可根据用户会话(如 JWT 令牌)验证身份,确保只有授权用户能获取密钥。
2. 使用环境变量管理密钥
适用场景:如果无法通过后端动态获取密钥(如静态网站),可使用环境变量隐藏密钥,避免直接暴露在代码中。
实现步骤:
在项目根目录创建
.env文件(添加到.gitignore,避免提交到代码库):VITE_ENCRYPTION_KEY=your-secure-key-here-123(Vue 项目中需使用
VITE_前缀,才能在客户端代码中访问)在插件中读取环境变量:
import CryptoJS from 'crypto-js';
// 从环境变量获取密钥(仅适用于无法动态获取的场景)
const SECRET_KEY = import.meta.env.VITE_ENCRYPTION_KEY;
if (!SECRET_KEY) {
throw new Error('请在环境变量中配置加密密钥');
}
// 后续加解密逻辑...
注意:
- 环境变量在构建时会被注入到代码中(仍是前端可见),仅能防止密钥被提交到代码库,不能完全避免被用户获取。
- 此方案安全性低于动态获取,适合非高敏感场景。
3. 密钥拆分与混合加密
核心思路:将密钥拆分为两部分,一部分由前端固定存储(低敏感),另一部分从后端动态获取(高敏感),使用时合并为完整密钥。
import CryptoJS from 'crypto-js';
// 前端固定存储的低敏感部分(可公开)
const STATIC_KEY_PART = 'static-part-123';
// 后端动态获取的高敏感部分
let DYNAMIC_KEY_PART = null;
// 从后端获取动态密钥部分
const fetchDynamicKey = async () => {
const response = await fetch('/api/get-dynamic-key');
const { dynamicPart } = await response.json();
DYNAMIC_KEY_PART = dynamicPart;
};
// 合并密钥
const getFullKey = async () => {
if (!DYNAMIC_KEY_PART) await fetchDynamicKey();
return STATIC_KEY_PART + DYNAMIC_KEY_PART; // 合并为完整密钥
};
// 加密函数
const encrypt = async (data) => {
const key = await getFullKey();
return CryptoJS.AES.encrypt(JSON.stringify(data), key).toString();
};
优势:
即使前端固定部分被获取,攻击者仍需同时获取后端动态部分才能解密,提高破解难度。
4. 限制密钥的使用范围
- 按用户生成密钥:后端为每个用户生成独立密钥,避免一个密钥泄露影响所有用户。
- 最小权限原则:密钥仅用于加密存储,不用于其他场景(如 API 认证),降低泄露后的风险。
5. 避免敏感数据完全依赖前端加密
前端加密只能作为辅助安全措施,不能替代后端安全:
- 核心敏感数据(如用户密码)不应在前端存储,即使加密也存在风险。
- 后端需对敏感接口进行权限校验,避免攻击者绕过前端直接访问数据。
总结
最安全的方案是通过后端动态生成短期有效的密钥,并结合用户身份验证确保只有授权用户能获取密钥。同时,前端加密应作为多层安全策略的一部分,而非唯一的安全保障。根据项目的安全级别选择合适的方案,平衡安全性和开发复杂度。