Core Docs
@skyroc/utils

Crypto

基于 AES 的对称加密类,用于本地存储敏感数据的加密保护

概述

import { Crypto } from '@skyroc/utils';

Crypto<T> 是一个泛型加密类,基于 crypto-js 的 AES 实现。它将任意可 JSON 序列化的对象加密为字符串,并支持还原回原始类型——常用于在 localStoragesessionStorage 中存储 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(对象)
  解密过程发生任何错误 → 捕获异常 → 返回 null

AES 采用 crypto-js 默认模式(CBC + PKCS7 填充),密钥长度取决于传入字符串的哈希派生结果(crypto-js 内部使用 MD5 派生密钥)。

API

new Crypto<T>(secret)

const crypto = new Crypto<MyDataType>('your-secret-key');
参数类型说明
secretstring加密密钥,建议使用足够复杂的字符串,不要硬编码在代码中

secret 作为公开属性挂在实例上(crypto.secret),如需更换密钥,直接赋值即可:

crypto.secret = 'new-secret-key';

encrypt(data)string

将对象加密为密文字符串。内部步骤:

  1. JSON.stringify(data) 序列化
  2. CryptoJS.AES.encrypt(json, secret) 加密
  3. .toString() 转为 Base64 格式密文
const cipher = crypto.encrypt({ userId: 'alice', role: 'admin' });
console.log(cipher); // 'U2FsdGVkX1+R5...'(每次加密结果不同,含随机盐值)

注意:AES-CBC 模式每次加密会生成随机 IV,所以同一明文每次加密结果不同,这是正常且安全的行为。


decrypt(encrypted)T | null

将密文字符串解密还原为原始对象。内部步骤:

  1. CryptoJS.AES.decrypt(encrypted, secret) 解密
  2. toString(CryptoJS.enc.Utf8) 转为 UTF-8 字符串
  3. 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();

On this page