指纹哈希与存储:SHA256、Base64、LocalStorage 与 IndexedDB
指纹采集完了,原始数据可不能直接扔到浏览器里存着。为什么?你想想看,一个完整的设备指纹字符串,少说也有几百个字符,里面包含了浏览器版本、显卡型号、字体列表……这些信息太敏感了。直接存,等于把用户的家底全亮给攻击者看。
所以,我们得做两件事:哈希和编码。哈希是为了防篡改,编码是为了方便传输和存储。今天我们就来聊聊这背后的技术选型,以及我在实际项目中踩过的坑。
为什么选 SHA256?
哈希算法很多,MD5、SHA1、SHA256、SHA512……我个人习惯,指纹场景首选 SHA256。原因有三:
- 安全性够用:SHA256 至今没有实际可行的碰撞攻击。MD5 和 SHA1 已经被证明可以构造碰撞,用在防篡改上不靠谱。
- 性能均衡:相比 SHA512,SHA256 的计算量更小。在移动端低端设备上,这个差异很明显。我曾经在几百台老旧 Android 设备上测试过,SHA256 的耗时大约是 SHA512 的 60%。
- 输出长度适中:256 位,转成十六进制就是 64 个字符。不长不短,存起来刚刚好。
fingerprint_raw + "FP_SALT_2024" 再算 SHA256。这样即使攻击者拿到了你的哈希算法,没有 salt 也无法伪造出相同的哈希值。
Base64 编码:为什么不是十六进制?
哈希算出来是一串二进制字节。我们得把它转成可读的字符串。最常见的两种方式是十六进制(Hex)和 Base64。
十六进制每个字节转成 2 个字符,Base64 每 3 个字节转成 4 个字符。算下来,SHA256 的 32 字节输出:
- Hex:64 个字符
- Base64:约 44 个字符(末尾可能有填充 =)
说白了,Base64 比 Hex 节省了约 30% 的存储空间。别小看这 20 个字符,当你的指纹数据需要频繁写入 IndexedDB 或者通过 URL 参数传递时,积少成多。
代码示例:SHA256 + Base64
浏览器原生支持 SubtleCrypto API,不需要引入任何第三方库。下面是我常用的工具函数:
async function hashFingerprint(raw) {
// 1. 加盐
const salted = raw + "FP_SALT_2024";
// 2. 转成 Uint8Array
const encoder = new TextEncoder();
const data = encoder.encode(salted);
// 3. 计算 SHA256
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
// 4. 转成 Base64
const hashArray = Array.from(new Uint8Array(hashBuffer));
const binaryString = hashArray.map(b => String.fromCharCode(b)).join('');
const base64 = btoa(binaryString);
return base64;
}
// 使用
const fpHash = await hashFingerprint(deviceFingerprint);
console.log(fpHash); // 输出类似 "aB3dEfGhIjKlMnOpQrStUvWxYz1234=="
避坑指南:btoa 和 atob 只支持 ASCII 字符。如果你的指纹数据包含中文(比如用户代理里有中文字符),一定要先用 TextEncoder 转成 UTF-8 字节,再算哈希。我曾经因为没做这一步,导致同一个浏览器在不同页面算出的哈希值不一样——排查了半天才发现是编码问题。
存储选型:LocalStorage vs IndexedDB
哈希算好了,存哪里?浏览器端主要有两个选择:LocalStorage 和 IndexedDB。很多人觉得 LocalStorage 简单够用,但实际项目中,我建议你根据场景来选。
| 特性 | LocalStorage | IndexedDB |
|---|---|---|
| 容量上限 | 约 5MB(各浏览器略有差异) | 通常无上限(取决于磁盘空间) |
| 数据类型 | 仅字符串 | 结构化数据、Blob、ArrayBuffer |
| 读写速度 | 同步,快 | 异步,稍慢 |
| 事务支持 | 无 | 支持读写事务 |
| 查询能力 | 只能按 key 查 | 支持索引、范围查询 |
我的建议很简单:
- 只存一个指纹哈希:用 LocalStorage。key-value 结构,一行代码搞定。
- 需要存多条指纹记录或关联数据:用 IndexedDB。比如你要存指纹哈希、生成时间、设备型号、用户 ID,还要支持按时间排序——IndexedDB 是唯一选择。
LocalStorage 实战
存指纹哈希,LocalStorage 是最直接的方式:
// 存储
localStorage.setItem('fp_hash', fpHash);
// 读取
const storedHash = localStorage.getItem('fp_hash');
// 删除
localStorage.removeItem('fp_hash');
requestIdleCallback 或 setTimeout 里执行存储,避免阻塞主线程。我遇到过因为 LocalStorage 写入导致页面滚动卡顿的情况,后来改成异步写入就解决了。
IndexedDB 实战
当你的指纹系统需要存储更多信息时,IndexedDB 是更好的选择。下面是一个简单的封装:
class FingerprintStore {
constructor() {
this.dbName = 'FingerprintDB';
this.storeName = 'fingerprints';
this.version = 1;
}
async open() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
const store = db.createObjectStore(this.storeName, {
keyPath: 'id',
autoIncrement: true
});
store.createIndex('hash', 'hash', { unique: true });
store.createIndex('timestamp', 'timestamp', { unique: false });
}
};
request.onsuccess = (event) => resolve(event.target.result);
request.onerror = (event) => reject(event.target.error);
});
}
async save(hash, metadata = {}) {
const db = await this.open();
const tx = db.transaction(this.storeName, 'readwrite');
const store = tx.objectStore(this.storeName);
const record = {
hash: hash,
timestamp: Date.now(),
metadata: metadata
};
return new Promise((resolve, reject) => {
const request = store.add(record);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async findByHash(hash) {
const db = await this.open();
const tx = db.transaction(this.storeName, 'readonly');
const store = tx.objectStore(this.storeName);
const index = store.index('hash');
return new Promise((resolve, reject) => {
const request = index.get(hash);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
// 使用
const store = new FingerprintStore();
await store.save(fpHash, { userAgent: navigator.userAgent });
整体流程:一张图说清楚
下面这张图展示了从指纹采集到存储的完整链路:
防篡改:存储后的校验机制
存进去只是第一步。关键问题是:你怎么知道存进去的哈希没有被篡改过?
我常用的做法是双重校验:
- 存储时:把原始指纹的哈希和原始指纹本身分开存储。比如哈希存 LocalStorage,原始指纹存 IndexedDB。
- 读取时:重新计算原始指纹的哈希,和存储的哈希对比。如果不一致,说明数据被篡改了。
async function verifyFingerprintIntegrity() {
// 从 IndexedDB 读取原始指纹
const rawFingerprint = await store.findByHash(storedHash);
if (!rawFingerprint) return false;
// 重新计算哈希
const recomputedHash = await hashFingerprint(rawFingerprint.metadata.raw);
// 对比
return recomputedHash === storedHash;
}
核心思路:哈希是原始数据的「数字指纹」。只要原始数据变了,哈希一定变。攻击者如果只改了原始数据没改哈希,或者只改了哈希没改原始数据,校验都会失败。这就是防篡改的基本原理。
嗯,哈希和存储这部分,说白了就是给指纹数据加一把锁,再把钥匙藏好。选对算法、选对存储方式,你的指纹系统就稳了一大半。