第4章:小程序本地存储安全
本地存储,说白了就是小程序在用户手机里存数据的地方。很多开发者觉得「存个token而已,能有什么问题?」——嗯,我以前也这么想,直到帮一个电商小程序做安全审计,发现用户的收货地址、手机号全明文躺在Storage里。那感觉,就像把家门钥匙挂在门外。
4.1 Storage加密:别让数据裸奔
小程序的 wx.setStorageSync 和 wx.getStorageSync 用起来确实方便,但默认是不加密的。我个人的习惯是:所有敏感数据,进Storage之前先过一道加密。
核心原则:客户端存储 ≠ 安全存储。加密是底线,不是可选项。
常用的加密方案有两种:
- 对称加密(AES):速度快,适合大量数据。密钥可以动态生成,但别写死在代码里。
- 非对称加密(RSA):安全性更高,但性能开销大。适合加密小体积的敏感信息,比如支付密钥。
我在项目中遇到过一个问题:用AES加密后,存进去的数据长度变长了,导致某些低端手机读写变慢。后来我改用 crypto-js 的 enc.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和证书校验的那些坑。