在实际项目中,将加密和解密函数应用到 Pinia 状态管理中,需要结合插件机制实现自动化加密存储,并确保代码可维护、安全性可控。以下是分步骤的落地方案:
第一步:准备加密工具函数
先创建独立的加密解密工具(复用之前的实现,确保密钥安全):
// src/utils/crypto.js
import CryptoJS from 'crypto-js';
// 生产环境:从后端接口动态获取密钥(这里仅示例)
// 注意:密钥必须是16/24/32字节,IV为16字节
let SECRET_KEY = CryptoJS.enc.Utf8.parse('temp-key-16byte'); // 临时密钥
let IV = CryptoJS.enc.Utf8.parse('temp-iv-16byte__');
// 动态更新密钥(例如登录后从后端获取)
export const updateCryptoKey = (newKey, newIV) => {
SECRET_KEY = CryptoJS.enc.Utf8.parse(newKey);
IV = CryptoJS.enc.Utf8.parse(newIV);
};
// 加密
export const encrypt = (data) => {
if (!data) return '';
try {
const dataStr = typeof data === 'string' ? data : JSON.stringify(data);
return CryptoJS.AES.encrypt(dataStr, SECRET_KEY, {
iv: IV,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}).toString();
} catch (error) {
console.error('加密失败:', error);
return '';
}
};
// 解密
export const decrypt = (encryptedStr) => {
if (!encryptedStr) return null;
try {
const bytes = CryptoJS.AES.decrypt(encryptedStr, SECRET_KEY, {
iv: IV,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
const decryptedStr = bytes.toString(CryptoJS.enc.Utf8);
return decryptedStr ? JSON.parse(decryptedStr) : null;
} catch (error) {
console.error('解密失败:', error);
return null;
}
};
第二步:创建 Pinia 加密插件
通过 Pinia 插件自动监听状态变化,实现“状态修改时加密存储,初始化时解密读取”:
// src/plugins/piniaEncrypt.js
import {
encrypt, decrypt } from '@/utils/crypto';
/**
* 创建加密存储插件
* @param {Object} options - 配置项
* @param {Storage} options.storage - 存储方式(localStorage/sessionStorage)
* @param {string[]} options.include - 需要加密的store名称列表(空则全部加密)
*/
export const createEncryptPlugin = (options = {
}) => {
const {
storage = localStorage, include = [] } = options;
return (context) => {
const {
store } = context;
const storeName = store.$id;
// 判断当前store是否需要加密(include为空则全部加密)
const shouldEncrypt = include.length === 0 || include.includes(storeName);
if (!shouldEncrypt) return;
// 1. 初始化:从存储中解密数据并同步到store
const loadEncryptedData = () => {
const encryptedData = storage.getItem(`pinia_${
storeName}`);
if (encryptedData) {
const decryptedData = decrypt(encryptedData);
if (decryptedData) {
store.$patch(decryptedData); // 合并解密后的数据到状态
}
}
};
// 2. 监听状态变化:自动加密存储
const saveEncryptedData = () => {
const state = store.$state; // 获取当前状态
const encryptedData = encrypt(state);
storage.setItem(`pinia_${
storeName}`, encryptedData);
};
// 初始化加载数据
loadEncryptedData();
// 监听状态变化并保存(防抖避免频繁存储)
let debounceTimer;
store.$subscribe(() => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(saveEncryptedData, 100); // 100ms防抖
});
// 3. 给store添加手动加密存储的方法
store.$encryptSave = saveEncryptedData;
store.$clearEncryptedStorage = () => {
storage.removeItem(`pinia_${
storeName}`);
};
};
};
第三步:在项目中配置 Pinia 并应用插件
在 Pinia 初始化时注册加密插件,指定需要加密的 store:
// src/store/index.js
import {
createPinia } from 'pinia';
import {
createEncryptPlugin } from '@/plugins/piniaEncrypt';
const pinia = createPinia();
// 应用加密插件:只加密user和settings两个store
pinia.use(createEncryptPlugin({
storage: localStorage, // 可选:使用sessionStorage
include: ['user', 'settings'] // 指定需要加密的store名称
}));
export default pinia;
第四步:定义需要加密的 Store
创建普通的 Pinia Store,插件会自动对其状态进行加密处理:
// src/store/user.js
import {
defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
userInfo: {
id: '',
name: '',
phone: '' // 敏感信息,需要加密
}
}),
actions: {
login(data) {
this.token = data.token;
this.userInfo = data.userInfo;
// 状态变化后会自动加密存储,无需手动调用
},
logout() {
this.$reset();
this.$clearEncryptedStorage(); // 清除存储的加密数据
}
}
});
第五步:在组件中使用(无需关心加密细节)
组件中正常使用 Store,加密/解密逻辑由插件自动处理:
<!-- src/views/Login.vue -->
<template>
<button @click="handleLogin">登录</button>
</template>
<script setup>
import { useUserStore } from '@/store/user';
import { updateCryptoKey } from '@/utils/crypto';
const userStore = useUserStore();
const handleLogin = async () => {
// 1. 调用登录接口,获取用户数据和动态密钥
const loginRes = await api.login({ username: 'test', password: '123' });
const { token, userInfo, cryptoKey, cryptoIV } = loginRes.data;
// 2. 更新加密密钥(关键:生产环境必须动态获取密钥)
updateCryptoKey(cryptoKey, cryptoIV);
// 3. 更新状态(插件会自动加密存储)
userStore.login({ token, userInfo });
};
</script>
核心要点与安全实践
密钥动态获取:
生产环境中,SECRET_KEY
和IV
必须通过后端接口动态返回(例如用户登录后),绝对不能硬编码在前端代码中(避免被打包暴露)。按需加密:
通过插件的include
配置,只对包含敏感信息的 Store(如user
)加密,普通 Store(如theme
)无需加密,减少性能损耗。防抖动存储:
状态频繁变化时(如输入框),通过防抖(setTimeout
)避免加密存储操作过于频繁,优化性能。异常处理:
加解密函数中添加错误捕获,避免因密钥错误或数据损坏导致整个应用崩溃。数据清除:
用户登出时,不仅要重置状态,还要调用$clearEncryptedStorage
清除本地加密数据,防止信息泄露。
通过这种方式,加密解密逻辑与业务代码解耦,既保证了敏感数据的安全性,又不影响开发体验。