第4章:小程序本地存储安全

本地存储,说白了就是小程序在用户手机里存数据的地方。很多开发者觉得「存个token而已,能有什么问题?」——嗯,我以前也这么想,直到帮一个电商小程序做安全审计,发现用户的收货地址、手机号全明文躺在Storage里。那感觉,就像把家门钥匙挂在门外。

4.1 Storage加密:别让数据裸奔

小程序的 wx.setStorageSyncwx.getStorageSync 用起来确实方便,但默认是不加密的。我个人的习惯是:所有敏感数据,进Storage之前先过一道加密

核心原则:客户端存储 ≠ 安全存储。加密是底线,不是可选项。

常用的加密方案有两种:

  • 对称加密(AES):速度快,适合大量数据。密钥可以动态生成,但别写死在代码里。
  • 非对称加密(RSA):安全性更高,但性能开销大。适合加密小体积的敏感信息,比如支付密钥。

我在项目中遇到过一个问题:用AES加密后,存进去的数据长度变长了,导致某些低端手机读写变慢。后来我改用 crypto-jsenc.Base64 编码,配合压缩,才把性能拉回来。

// 推荐:AES-256-CBC 加密存储
const CryptoJS = require('crypto-js');

function encryptData(data, key) {
  const iv = CryptoJS.lib.WordArray.random(16);
  const encrypted = CryptoJS.AES.encrypt(data, key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
  });
  // 将 IV 和密文一起存储
  return iv.toString() + ':' + encrypted.toString();
}

function decryptData(ciphertext, key) {
  const parts = ciphertext.split(':');
  const iv = CryptoJS.enc.Hex.parse(parts[0]);
  const encrypted = parts[1];
  const decrypted = CryptoJS.AES.decrypt(encrypted, key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
  });
  return decrypted.toString(CryptoJS.enc.Utf8);
}

小技巧:密钥不要硬编码。我习惯用「设备指纹 + 用户ID + 服务端下发的盐值」组合生成动态密钥。这样即使一台设备被攻破,其他设备的数据也是安全的。

4.2 敏感数据保护:哪些该存,哪些不该存

你想想看,一个电商小程序,用户的信用卡号、支付密码、身份证照片——这些数据真的需要存在本地吗?

我见过最离谱的案例:某社交小程序把用户的聊天记录全文存在Storage里,结果被恶意插件直接读取。所以,敏感数据保护的第一步,是「能不存就不存」

数据类型 建议存储位置 说明
登录Token Storage(加密) 设置过期时间,定期刷新
用户手机号 服务端 仅展示时从接口获取,用完即弃
支付密码 绝不存储 每次支付由用户输入,或使用生物识别
用户偏好设置 Storage(明文可) 不涉及隐私,加密反而增加开销

说白了,本地存储只适合放「丢了也不心疼」的数据。真正敏感的信息,应该走服务端接口,用HTTPS传输,用完就清掉。

4.3 Keychain/Keystore 使用:iOS和Android的保险箱

小程序本身不直接提供Keychain或Keystore的API,但我们可以通过「插件」或「原生能力」间接使用。嗯,这里要注意:不要自己造轮子,直接用平台提供的能力最靠谱。

我个人的做法是:

  • iOS Keychain:通过小程序插件调用原生模块,把Token存进Keychain。Keychain的数据在设备锁定时会自动加密,即使手机丢了,别人也读不到。
  • Android Keystore:利用Android的硬件安全模块,生成非对称密钥对。私钥永远不出Keystore,只导出公钥用于加密。

避坑指南:我曾经在Android低版本(API 18以下)上踩过坑——Keystore不支持硬件加密,回退到软件加密后性能暴跌。后来我加了版本判断,低版本直接走AES加密,不依赖Keystore。

// 伪代码:通过插件调用原生Keychain
// iOS 端
- (void)saveToKeychain:(NSString *)data forKey:(NSString *)key {
    // 使用 Security.framework 的 SecItemAdd
    NSDictionary *query = @{
        (id)kSecClass: (id)kSecClassGenericPassword,
        (id)kSecAttrAccount: key,
        (id)kSecValueData: [data dataUsingEncoding:NSUTF8StringEncoding],
        (id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly
    };
    SecItemAdd((CFDictionaryRef)query, NULL);
}

// Android 端
public void saveToKeystore(String alias, String data) {
    KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
    keyStore.load(null);
    // 生成密钥对
    KeyPairGenerator generator = KeyPairGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore");
    generator.initialize(new KeyGenParameterSpec.Builder(alias,
        KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
        .build());
    // 加密数据...
}

4.4 数据清除策略:该放手时就放手

很多开发者只关心「怎么存」,不关心「怎么删」。但数据清除,恰恰是安全防护的最后一道防线。

我遇到过的情况:用户在小程序里退出了登录,但Storage里的数据还留着。下一个用户登录后,竟然能看到上一个用户的浏览记录。这要是电商平台,购物车里的商品信息就全暴露了。

所以,我建议制定以下清除策略:

  • 退出登录时:清除所有用户相关的Storage数据,包括Token、缓存、偏好设置。
  • 切换账号时:先清除旧数据,再加载新数据。顺序不能反。
  • 应用卸载时:小程序本身会自动清除Storage,但如果你用了插件存数据到Keychain,记得在插件里实现「卸载时清除」的回调。
  • 定期清理:对于临时缓存(比如图片、接口响应),设置过期时间。我习惯用 wx.getStorageInfoSync 检查总大小,超过阈值就清理最旧的数据。
// 退出登录时的清除逻辑
function logout() {
  // 1. 清除Storage
  wx.clearStorageSync();
  
  // 2. 清除Keychain/Keystore中的敏感数据
  // 调用插件方法
  wx.nativePlugin.clearKeychainData();
  
  // 3. 清除内存中的敏感变量
  userToken = null;
  userInfo = null;
  
  // 4. 跳转到登录页
  wx.reLaunch({ url: '/pages/login/login' });
}

额外提醒:清除数据时,记得先「置空」再「删除」。有些系统在删除后,数据块还在闪存里,直到被覆盖才会真正消失。置空操作可以加速这个覆盖过程。

好了,关于小程序本地存储安全,核心就这四点:加密、筛选、用原生保险箱、及时清除。你想想看,只要把这四步做到位,本地存储这块基本就稳了。下一章我们会聊网络传输安全,到时候再细说HTTPS和证书校验的那些坑。