@skyroc/utils
Crypto
基于 AES 的对称加密类,用于本地存储敏感数据的加密保护
概述
import { Crypto } from '@skyroc/utils';Crypto<T> 是一个泛型加密类,基于 crypto-js 的 AES 实现。它将任意可 JSON 序列化的对象加密为字符串,并支持还原回原始类型——常用于在 localStorage 或 sessionStorage 中存储 token、用户信息等敏感数据时的加密层。
快速上手
import { Crypto } from '@skyroc/utils';
// 定义要加密的数据类型
type TokenPayload = {
accessToken: string;
refreshToken: string;
expiresAt: number;
};
// 实例化,传入密钥
const crypto = new Crypto<TokenPayload>('your-secret-key');
// 加密
const cipher = crypto.encrypt({
accessToken: 'eyJhbGci...',
refreshToken: 'dGhpcyBp...',
expiresAt: Date.now() + 3600_000,
});
// 'U2FsdGVkX1+...'(密文字符串,可安全存储)
// 解密
const payload = crypto.decrypt(cipher);
// { accessToken: 'eyJhbGci...', refreshToken: 'dGhpcyBp...', expiresAt: 1745... }
// 或 null(解密失败时)工作原理
加密流程:
T(对象)→ JSON.stringify → 明文字符串 → AES.encrypt(key) → 密文字符串
解密流程:
密文字符串 → AES.decrypt(key) → 明文字符串 → JSON.parse → T(对象)
解密过程发生任何错误 → 捕获异常 → 返回 nullAES 采用 crypto-js 默认模式(CBC + PKCS7 填充),密钥长度取决于传入字符串的哈希派生结果(crypto-js 内部使用 MD5 派生密钥)。
API
new Crypto<T>(secret)
const crypto = new Crypto<MyDataType>('your-secret-key');| 参数 | 类型 | 说明 |
|---|---|---|
secret | string | 加密密钥,建议使用足够复杂的字符串,不要硬编码在代码中 |
secret 作为公开属性挂在实例上(crypto.secret),如需更换密钥,直接赋值即可:
crypto.secret = 'new-secret-key';encrypt(data) → string
将对象加密为密文字符串。内部步骤:
JSON.stringify(data)序列化CryptoJS.AES.encrypt(json, secret)加密.toString()转为 Base64 格式密文
const cipher = crypto.encrypt({ userId: 'alice', role: 'admin' });
console.log(cipher); // 'U2FsdGVkX1+R5...'(每次加密结果不同,含随机盐值)注意:AES-CBC 模式每次加密会生成随机 IV,所以同一明文每次加密结果不同,这是正常且安全的行为。
decrypt(encrypted) → T | null
将密文字符串解密还原为原始对象。内部步骤:
CryptoJS.AES.decrypt(encrypted, secret)解密toString(CryptoJS.enc.Utf8)转为 UTF-8 字符串JSON.parse(...)还原为T
解密失败的情况(均返回 null,不抛出异常):
- 密文被篡改或损坏
- 密钥不匹配
- 原始数据不是合法 JSON(理论上不会发生,仅防御边界情况)
const data = crypto.decrypt(cipher);
if (data === null) {
// 解密失败:token 可能已失效或被篡改,重新登录
redirectToLogin();
}与 createStorage 配合使用
最常见的用法是在 createStorage 读写时加一层加密,防止用户直接从 DevTools 读取明文:
import { Crypto, createStorage } from '@skyroc/utils';
type AppStorage = {
tokenCipher: string; // 存储密文
};
const storage = createStorage<AppStorage>('local', 'app__');
const crypto = new Crypto<{ token: string; role: string }>('APP_SECRET_2024');
// 写入时加密
function saveToken(token: string, role: string) {
const cipher = crypto.encrypt({ token, role });
storage.set('tokenCipher', cipher);
}
// 读取时解密
function getToken() {
const cipher = storage.get('tokenCipher');
if (!cipher) return null;
return crypto.decrypt(cipher); // { token, role } | null
}安全注意事项
密钥管理:
// ❌ 避免硬编码在源码中
const crypto = new Crypto('my-secret-key');
// ✅ 从环境变量读取
const crypto = new Crypto(import.meta.env.VITE_CRYPTO_SECRET);适用范围:
Crypto 适合本地存储数据的混淆加密,不适合替代 HTTPS 传输层加密:
| 场景 | 适用? | 说明 |
|---|---|---|
| localStorage 加密存储 | ✅ | 防止普通用户直接读取 |
| Cookie 加密存储 | ✅ | 同上 |
| 客户端到服务端传输 | ❌ | 应使用 HTTPS,不应在前端加密 |
| 替代 JWT 验签 | ❌ | AES 是对称加密,无法实现签名验证 |
关于"安全强度":
crypto-js 的 AES 加密在密钥保密的前提下是安全的,但前端 JS 代码可被用户审查,密钥本质上只能做到"混淆"而非"绝对保密"。对于真正需要保密的数据,应由服务端持有密钥并负责加解密。
处理解密失败
建议统一封装解密访问,避免 null 检查遗漏:
class TokenStorage {
private crypto = new Crypto<TokenPayload>(import.meta.env.VITE_CRYPTO_SECRET);
private storage = createStorage<{ token: string }>('local', 'app__');
save(payload: TokenPayload) {
this.storage.set('token', this.crypto.encrypt(payload));
}
load(): TokenPayload | null {
const cipher = this.storage.get('token');
if (!cipher) return null;
return this.crypto.decrypt(cipher);
}
clear() {
this.storage.remove('token');
}
}
export const tokenStorage = new TokenStorage();