H5 敏感数据加密存储方案
【摘要】 1. 引言在数字化时代,Web应用广泛收集和存储用户的 敏感数据(如个人身份信息、金融凭证、健康记录、登录密码等),这些数据一旦泄露,可能导致用户隐私侵犯、财产损失甚至法律责任。然而,浏览器的本地存储环境(如 LocalStorage、 SessionStorage、 IndexedDB)本质上是 明文存储——任何能够访问浏览器存储的恶意脚本(...
1. 引言
在数字化时代,Web应用广泛收集和存储用户的 敏感数据(如个人身份信息、金融凭证、健康记录、登录密码等),这些数据一旦泄露,可能导致用户隐私侵犯、财产损失甚至法律责任。然而,浏览器的本地存储环境(如 LocalStorage、 SessionStorage、 IndexedDB)本质上是 明文存储——任何能够访问浏览器存储的恶意脚本(如XSS攻击注入的代码)或物理接触设备的攻击者,均可直接读取这些敏感信息,造成严重安全风险。
为解决这一核心问题,H5敏感数据加密存储方案 应运而生。它通过结合 现代加密算法(如AES、RSA)和 安全的密钥管理机制,在数据存入浏览器存储之前对其进行 加密转换(明文→密文),并在读取时 解密还原(密文→明文),确保即使存储介质被非法访问,攻击者也无法直接获取原始敏感数据。同时,方案还需兼顾 用户体验(如加密/解密过程对用户透明)、 性能开销(如加密算法的计算效率)和 合规要求(如GDPR、CCPA等数据保护法规)。
本文将深入讲解H5敏感数据加密存储的核心技术,涵盖其应用场景、代码实现、原理解析及实践指南,并探讨其未来趋势与挑战。
2. 技术背景
2.1 为什么需要敏感数据加密存储?
- 浏览器存储的固有风险:
LocalStorage、SessionStorage和IndexedDB等浏览器存储机制是 明文存储,数据以可读格式直接保存在用户设备的本地文件系统中。一旦攻击者通过 XSS(跨站脚本攻击) 注入恶意脚本(如窃取用户信息的恶意代码),或用户设备被物理接触(如丢失的手机被他人解锁),存储的敏感数据(如用户Token、身份证号)将被直接暴露。 - 合规与法律要求:
全球数据保护法规(如欧盟的 GDPR、美国的 CCPA、中国的 个人信息保护法)明确规定,企业必须对用户的敏感个人信息(如姓名、身份证号、金融信息)进行 加密保护,否则可能面临高额罚款和法律诉讼。例如,GDPR要求对“特殊类别的个人数据”(如健康数据)进行“适当的技术和组织措施”加密存储。 - 用户信任与隐私保护:
用户对Web应用的信任建立在“数据安全”的基础上。若应用因存储不当导致用户敏感信息泄露(如密码明文存储后被撞库攻击),将直接损害用户权益和应用的品牌声誉。加密存储是提升用户信任、保护隐私的关键技术手段。
2.2 核心概念
概念 | 说明 | 类比 |
---|---|---|
敏感数据 | 指一旦泄露可能对用户隐私、财产或权益造成严重危害的数据,如身份证号、银行卡号、登录密码、健康记录、个人位置信息等。 | 类似“保险箱中的贵重物品”——需要最高级别的保护。 |
加密存储 | 通过加密算法(如AES、RSA)将敏感数据的 明文 转换为 密文 后再存储到浏览器本地(如LocalStorage),读取时再解密回明文。确保即使存储介质被非法访问,攻击者也无法直接理解数据内容。 | 类似将贵重物品放入带密码的保险箱——只有拥有正确密码(密钥)的人才能打开。 |
加密算法 | 用于将明文转换为密文的数学函数,常见的对称加密算法(如 AES)加密和解密使用同一密钥,非对称加密算法(如 RSA)使用公钥加密、私钥解密。 | 类似密码锁——对称加密是同一把钥匙开锁和解锁,非对称加密是公钥开门、私钥锁门。 |
密钥管理 | 加密算法的核心是密钥(如AES的256位密钥、RSA的私钥),密钥的安全存储和管理(如避免硬编码在代码中、定期更换)是加密方案可靠性的关键。 | 类似保险箱的密码——密码泄露则保险箱失效,因此需严格保护密钥。 |
XSS攻击防护 | 跨站脚本攻击(XSS)是窃取浏览器存储数据的常见手段(如恶意脚本读取LocalStorage)。加密存储需结合 CSP(内容安全策略)、 输入过滤 等技术,从源头降低XSS风险。 | 类似给保险箱加装防盗门——防止小偷(恶意脚本)靠近保险箱。 |
2.3 应用使用场景
场景类型 | 加密存储示例 | 技术价值 |
---|---|---|
用户身份认证 | 存储用户的登录Token(如JWT)或会话ID,通过AES加密后存入LocalStorage,防止Token被XSS攻击窃取后直接滥用。 | 保护用户登录状态,避免会话劫持。 |
个人敏感信息 | 缓存用户的身份证号、手机号、银行卡号等个人信息(如填写表单时暂存),使用RSA公钥加密后存储到IndexedDB,仅服务器持有私钥可解密。 | 符合隐私法规,防止信息泄露。 |
金融数据保护 | 存储用户的支付密码、交易记录(如银行类Web应用),通过高强度加密算法(如AES-256)加密后存入本地,确保即使设备丢失也无法直接读取。 | 保障用户资金安全,符合金融行业规范。 |
健康医疗数据 | 缓存用户的健康记录(如病历、用药历史),使用国密算法(如SM4)或AES加密后存储,满足医疗行业的严格隐私要求。 | 保护用户健康隐私,符合法律法规。 |
企业内部工具 | 员工使用的内部Web系统(如CRM、ERP)存储客户联系方式、合同附件等敏感数据,通过加密后存入浏览器本地,防止内部人员越权访问。 | 保护企业核心数据,降低内部泄露风险。 |
3. 应用使用场景
3.1 场景1:用户登录Token加密存储(防XSS窃取)
- 需求:Web应用的用户登录成功后,服务器返回一个JWT(JSON Web Token)用于后续接口鉴权。该Token需存储在客户端(如LocalStorage),但直接明文存储可能被XSS攻击窃取并滥用(如发起恶意API请求)。通过AES加密Token后存储,即使XSS攻击获取了密文,没有解密密钥也无法使用。
3.2 场景2:身份证号等个人信息缓存(合规要求)
- 需求:用户在填写表单(如注册、实名认证)时,需要暂时缓存身份证号、手机号等敏感信息(避免重复输入),但这些信息属于“特殊类别个人数据”,必须加密存储。使用RSA公钥加密后存入IndexedDB,仅服务器持有私钥可在需要时解密。
3.3 场景3:支付密码保护(金融级安全)
- 需求:金融类Web应用(如在线支付、银行服务)要求用户设置的支付密码必须本地加密存储(如记住密码功能),且加密强度需达到金融级(如AES-256)。通过硬件安全模块(HSM)或用户主密钥(Master Key)派生加密密钥,确保支付密码即使被非法访问也无法破解。
4. 不同场景下的详细代码实现
4.1 环境准备
- 开发工具:任意支持HTML5的现代浏览器(Chrome、Firefox、Edge),以及代码编辑器(如VS Code)。
- 技术栈:原生JavaScript(结合 Web Crypto API 实现加密/解密, LocalStorage 或 IndexedDB 存储密文)。
- 核心API:
- Web Crypto API:浏览器原生提供的加密算法库(如
SubtleCrypto
接口),支持AES、RSA等标准算法,无需第三方库。 - LocalStorage/IndexedDB:用于存储加密后的密文(密钥需安全管理,不可硬编码)。
- 密钥生成与管理:通过
crypto.subtle.generateKey()
生成对称密钥(AES)或非对称密钥对(RSA),并通过安全方式(如用户密码派生、服务器下发)传递密钥。
- Web Crypto API:浏览器原生提供的加密算法库(如
- 注意事项:
- 密钥安全:加密密钥(如AES密钥、RSA私钥)不可直接存储在代码中或LocalStorage中(否则攻击者可获取密钥解密数据),需通过用户输入密码派生(如PBKDF2算法)或由服务器动态下发。
- 算法选择:优先使用 AES-256(对称加密)(性能高,适合大量数据加密)或 RSA-OAEP(非对称加密)(适合密钥交换或小数据加密),避免已破解的弱算法(如DES、RC4)。
- 数据完整性:可结合 HMAC(哈希消息认证码) 验证密文是否被篡改(如存储密文时附带HMAC签名)。
4.2 场景1:用户登录Token加密存储(AES对称加密)
4.2.1 核心代码实现
加密存储Token(保存到LocalStorage)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Token加密存储示例</title>
</head>
<body>
<h1>用户登录Token加密存储</h1>
<button id="loginBtn">模拟登录并加密存储Token</button>
<button id="getTokenBtn">获取并解密Token</button>
<div id="output"></div>
<script>
// 模拟从服务器获取的登录Token(实际场景中通过API请求获取)
const mockToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywiZXhwIjoxNjkzMzY5NjAwfQ.abcdef1234567890';
// 加密函数:使用AES-GCM算法(推荐用于Web)加密明文Token
async function encryptData(plaintext, key) {
try {
// 将明文转换为ArrayBuffer
const encoder = new TextEncoder();
const data = encoder.encode(plaintext);
// 生成随机初始化向量(IV,每个加密操作唯一)
const iv = crypto.getRandomValues(new Uint8Array(12)); // AES-GCM推荐IV长度为12字节
// 使用SubtleCrypto加密数据
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv }, // 加密算法配置
key, // 对称密钥(AES)
data // 明文数据
);
// 合并IV和密文(存储时需一起保存,解密时需要IV)
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
combined.set(iv, 0);
combined.set(new Uint8Array(ciphertext), iv.length);
// 转换为Base64字符串(便于存储到LocalStorage)
return btoa(String.fromCharCode.apply(null, combined));
} catch (error) {
console.error('加密失败:', error);
throw error;
}
}
// 解密函数:从Base64密文中还原明文Token
async function decryptData(encryptedBase64, key) {
try {
// 从Base64还原Uint8Array
const combined = new Uint8Array(atob(encryptedBase64).split('').map(c => c.charCodeAt(0)));
// 提取IV(前12字节)和密文(剩余部分)
const iv = combined.slice(0, 12);
const ciphertext = combined.slice(12);
// 使用SubtleCrypto解密数据
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv },
key,
ciphertext
);
// 转换为明文字符串
return new TextDecoder().decode(decrypted);
} catch (error) {
console.error('解密失败:', error);
throw error;
}
}
// 生成AES密钥(实际场景中密钥应通过安全方式管理,如用户密码派生)
async function generateAesKey() {
return await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 }, // AES-256算法
true, // 密钥是否可导出(生产环境应为false,仅限当前会话使用)
['encrypt', 'decrypt'] // 密钥用途
);
}
// 模拟密钥管理(实际场景中密钥不可硬编码或存储在LocalStorage!)
let aesKey; // 全局保存生成的AES密钥(仅示例用,生产环境需安全存储)
// 登录按钮:模拟登录成功后加密存储Token
document.getElementById('loginBtn').addEventListener('click', async () => {
try {
// 生成AES密钥(实际场景中应通过用户密码派生或服务器下发)
aesKey = await generateAesKey();
console.log('AES密钥已生成(示例用,生产环境需安全存储)');
// 加密Token
const encryptedToken = await encryptData(mockToken, aesKey);
// 存储加密后的Token到LocalStorage(实际场景中可存储到IndexedDB更安全)
localStorage.setItem('encryptedToken', encryptedToken);
document.getElementById('output').innerHTML = '<p style="color:green">Token已加密存储到LocalStorage</p>';
} catch (error) {
document.getElementById('output').innerHTML = '<p style="color:red">加密存储失败: ' + error.message + '</p>';
}
});
// 获取Token按钮:从LocalStorage解密并显示Token
document.getElementById('getTokenBtn').addEventListener('click', async () => {
try {
const encryptedBase64 = localStorage.getItem('encryptedToken');
if (!encryptedBase64) {
document.getElementById('output').innerHTML = '<p>未找到加密的Token</p>';
return;
}
if (!aesKey) {
document.getElementById('output').innerHTML = '<p style="color:red">AES密钥未初始化(示例用,请先点击登录)</p>';
return;
}
// 解密Token
const decryptedToken = await decryptData(encryptedBase64, aesKey);
document.getElementById('output').innerHTML = `<p style="color:blue">解密后的Token: ${decryptedToken}</p>`;
} catch (error) {
document.getElementById('output').innerHTML = '<p style="color:red">解密失败: ' + error.message + '</p>';
}
});
</script>
</body>
</html>
4.2.2 代码解析
- 加密流程:
- 生成AES密钥:使用
crypto.subtle.generateKey()
生成一个256位的AES对称密钥(AES-GCM
算法,适合Web环境)。 - 加密数据:将明文Token(如JWT)转换为
ArrayBuffer
,生成随机初始化向量(IV,12字节),通过crypto.subtle.encrypt()
加密数据,合并IV和密文后转换为Base64字符串存储(便于LocalStorage保存)。
- 生成AES密钥:使用
- 解密流程:
- 提取密文:从LocalStorage获取Base64密文,还原为
Uint8Array
,分离IV(前12字节)和密文(剩余部分)。 - 解密数据:使用相同的AES密钥和IV,通过
crypto.subtle.decrypt()
解密密文,还原为明文Token。
- 提取密文:从LocalStorage获取Base64密文,还原为
- 安全注意事项:
- 密钥管理:示例中密钥是临时生成的(仅用于演示),实际场景中密钥应通过 用户密码派生(如PBKDF2) 或 服务器动态下发,且不可硬编码或存储在LocalStorage中(否则攻击者可获取密钥)。
- IV唯一性:每次加密操作必须使用唯一的IV(示例中通过
crypto.getRandomValues()
生成),避免重复IV导致加密破解。 - 算法选择:优先使用
AES-GCM
(提供加密和完整性校验),而非AES-CBC
(需额外处理完整性)。
4.3 场景2:身份证号加密存储(RSA非对称加密)
4.3.1 核心代码实现
加密存储身份证号(使用RSA公钥)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>身份证号RSA加密示例</title>
</head>
<body>
<h1>身份证号RSA加密存储</h1>
<input type="text" id="idNumber" placeholder="输入身份证号" />
<button id="encryptBtn">加密并存储到IndexedDB</button>
<button id="decryptBtn">从IndexedDB解密并显示</button>
<div id="output"></div>
<script>
// 模拟服务器下发的RSA公钥(实际场景中通过HTTPS获取)
const mockPublicKeyPem = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJXdi5+gcX
juz+vJJgLiXQ3C4d6iZzW5W9+5z6JY2e6z3Q5J5J5J5J5J5J5J5J5J5J5J5J5J5J
5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J
5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J
5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J5J极简版公钥(仅示例,实际需完整PEM格式)-----END PUBLIC KEY-----`;
// 模拟服务器持有的RSA私钥(实际场景中仅服务器持有,用于解密)
// 注意:浏览器无法直接使用PEM格式的公钥,需转换为CryptoKey对象
let rsaPublicKey; // 全局保存RSA公钥(CryptoKey对象)
// 将PEM格式的公钥转换为CryptoKey对象(用于加密)
async function importPublicKey(pem) {
try {
// 移除PEM头尾和换行符
const pemContents = pem.replace(/-----BEGIN PUBLIC KEY-----|-----END PUBLIC KEY-----|\s/g, '');
// 将Base64字符串转换为ArrayBuffer
const binaryDer = Uint8Array.from(atob(pemContents), c => c.charCodeAt(0)).buffer;
// 导入公钥(RSA-OAEP算法,适合加密)
return await crypto.subtle.importKey(
'spki', // 公钥格式(SubjectPublicKeyInfo)
binaryDer,
{ name: 'RSA-OAEP', hash: 'SHA-256' }, // 算法配置
true, // 是否可导出(示例用true,生产环境应为false)
['encrypt'] // 密钥用途(仅加密)
);
} catch (error) {
console.error('导入公钥失败:', error);
throw error;
}
}
// 加密身份证号(使用RSA公钥)
async function encryptIdNumber(idNumber, publicKey) {
try {
const encoder = new TextEncoder();
const data = encoder.encode(idNumber); // 身份证号转为ArrayBuffer
// 使用RSA-OAEP加密
const ciphertext = await crypto.subtle.encrypt(
{ name: 'RSA-OAEP' },
publicKey,
data
);
// 转换为Base64存储
return btoa(String.fromCharCode.apply(null, new Uint8Array(ciphertext)));
} catch (error) {
console.error('加密身份证号失败:', error);
throw error;
}
}
// 解密身份证号(需RSA私钥,此处仅示例加密流程)
// 实际场景中私钥由服务器持有,客户端加密后存储,服务器解密
async function decryptIdNumber(encryptedBase64, privateKey) {
try {
const ciphertext = Uint8Array.from(atob(encryptedBase64).split('').map(c => c.charCodeAt(0)));
// 使用RSA-OAEP解密(需私钥)
const decrypted = await crypto.subtle.decrypt(
{ name: 'RSA-OAEP' },
privateKey,
ciphertext
);
return new TextDecoder().decode(decrypted);
} catch (error) {
console.error('解密身份证号失败:', error);
throw error;
}
}
// 存储加密后的身份证号到IndexedDB
async function saveToIndexedDB(encryptedData) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('SecureDataDB', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('secureData')) {
db.createObjectStore('secureData', { keyPath: 'id', autoIncrement: true });
}
};
request.onsuccess = (event) => {
const db = event.target.result;
const transaction = db.transaction('secureData', 'readwrite');
const store = transaction.objectStore('secureData');
store.put({ data: encryptedData, type: 'idNumber' });
transaction.oncomplete = () => resolve();
transaction.onerror = (e) => reject(e.target.error);
};
request.onerror = (event) => reject(event.target.error);
});
}
// 从IndexedDB获取加密的身份证号
async function getFromIndexedDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('SecureDataDB', 1);
request.onsuccess = (event) => {
const db = event.target.result;
const transaction = db.transaction('secureData', 'readonly');
const store = transaction.objectStore('secureData');
const getRequest = store.getAll();
getRequest.onsuccess = () => {
const records = getRequest.result;
const idNumberRecord = records.find(r => r.type === 'idNumber');
resolve(idNumberRecord ? idNumberRecord.data : null);
};
getRequest.onerror = (e) => reject(e.target.error);
};
request.onerror = (event) => reject(event.target.error);
});
}
// 模拟导入公钥(实际场景中通过HTTPS从服务器获取)
async function initPublicKey() {
rsaPublicKey = await importPublicKey(mockPublicKeyPem);
console.log('RSA公钥已导入(示例用)');
}
// 页面加载时初始化公钥
initPublicKey().catch(console.error);
// 加密并存储身份证号
document.getElementById('encryptBtn').addEventListener('click', async () => {
const idNumber = document.getElementById('idNumber').value;
if (!idNumber) {
document.getElementById('output').innerHTML = '<p style="color:red">请输入身份证号</p>';
return;
}
try {
const encryptedData = await encryptIdNumber(idNumber, rsaPublicKey);
await saveToIndexedDB(encryptedData);
document.getElementById('output').innerHTML = '<p style="color:green">身份证号已加密存储到IndexedDB</p>';
} catch (error) {
document.getElementById('output').innerHTML = '<p style="color:red">加密存储失败: ' + error.message + '</p>';
}
});
// 解密并显示身份证号(实际场景中需服务器私钥解密)
document.getElementById('decryptBtn').addEventListener('click', async () => {
try {
const encryptedData = await getFromIndexedDB();
if (!encryptedData) {
document.getElementById('output').innerHTML = '<p>未找到加密的身份证号</p>';
return;
}
// 注意:此处仅为示例,实际解密需服务器私钥(客户端无法解密RSA加密的数据)
document.getElementById('output').innerHTML = '<p style="color:blue">加密数据已获取(解密需服务器私钥): ' + encryptedData.substring(0, 50) + '...</p>';
// 实际场景中,客户端将加密数据发送到服务器,服务器用私钥解密后返回明文
} catch (error) {
document.getElementById('output').innerHTML = '<p style="color:red">获取加密数据失败: ' + error.message + '</p>';
}
});
</script>
</body>
</html>
4.3.2 代码解析
- 加密流程:
- 导入RSA公钥:将服务器下发的RSA公钥(PEM格式)转换为浏览器可用的
CryptoKey
对象(通过crypto.subtle.importKey()
),用于加密操作。 - 加密身份证号:将用户输入的身份证号(明文)转换为
ArrayBuffer
,通过crypto.subtle.encrypt()
使用RSA-OAEP算法(推荐用于加密)加密,生成密文后转换为Base64字符串存储。
- 导入RSA公钥:将服务器下发的RSA公钥(PEM格式)转换为浏览器可用的
- 存储流程:加密后的身份证号通过 IndexedDB 存储(比LocalStorage更安全,适合敏感数据),实际场景中仅存储密文,解密由持有私钥的服务器完成。
- 安全注意事项:
- 密钥分离:RSA公钥用于客户端加密,私钥由服务器持有(客户端无法解密),确保即使密文被非法获取,攻击者也无法还原明文。
- PEM格式转换:浏览器无法直接使用PEM格式的公钥,需移除头尾标记和换行符,转换为Base64的Binary DER格式后再导入为
CryptoKey
。 - 算法选择:优先使用
RSA-OAEP
(结合SHA-256哈希),而非RSA-PKCS#1-v1_5
(安全性较低)。
5. 原理解释
5.1 敏感数据加密存储的核心机制
- 加密/解密流程:
- 加密(存储前):敏感数据(如Token、身份证号)以明文形式存在时,通过加密算法(如AES、RSA)和密钥将其转换为不可读的密文(如Base64字符串),再存储到浏览器本地(LocalStorage/IndexedDB)。
- 解密(读取时):当需要使用敏感数据时,从存储介质中读取密文,通过相同的密钥和算法(或对应的私钥)将其还原为明文,供应用逻辑使用。
- 密钥管理:
- 对称加密(如AES):加密和解密使用同一密钥(如AES-256密钥),密钥需通过安全方式生成(如用户密码派生)并严格保护(不可硬编码、不可存储在明文存储中)。
- 非对称加密(如RSA):使用公钥加密、私钥解密。公钥可公开(用于客户端加密),私钥由服务器持有(用于解密),确保即使密文被非法获取,攻击者无私钥则无法解密。
- 加密算法选择:
- AES(对称加密):性能高,适合加密大量数据(如用户Token、缓存数据),推荐使用 AES-256-GCM(提供加密和完整性校验)。
- RSA(非对称加密):适合加密小数据(如加密对称密钥本身)或密钥交换,推荐使用 RSA-OAEP(结合SHA-256,安全性高于PKCS#1-v1_5)。
5.2 原理流程图(以AES加密Token为例)
[用户登录成功,获取Token(明文)] → [生成AES密钥(或从安全来源获取)]
↓
[使用AES-GCM算法加密Token:明文 + 密钥 + 随机IV → 生成密文(含IV)]
↓
[将密文(Base64格式)存储到LocalStorage/IndexedDB]
↓
[需要使用Token时,从存储中读取密文 → 提取IV和密文 → 使用AES-GCM解密(密钥 + IV) → 还原明文Token]
↓
[应用逻辑使用明文Token(如发起API请求鉴权)]
6. 核心特性
特性 | 说明 | 优势 |
---|---|---|
数据保密性 | 敏感数据以密文形式存储,即使LocalStorage/IndexedDB被非法访问(如XSS攻击、设备丢失),攻击者也无法直接读取原始数据。 | 保护用户隐私,防止信息泄露。 |
合规性支持 | 符合全球数据保护法规(如GDPR、CCPA)对敏感数据加密存储的要求,避免法律风险。 | 满足监管要求,降低企业合规成本。 |
灵活的加密算法 | 支持对称加密(AES)和非对称加密(RSA),开发者可根据数据类型(如大量Token用AES,小数据公钥加密用RSA)选择最优算法。 | 适应不同场景的安全需求。 |
密钥安全管理 | 密钥通过安全方式生成(如用户密码派生、服务器下发),避免硬编码或明文存储,降低密钥泄露风险。 | 确保加密机制的可靠性。 |
透明性 | 加密/解密过程对用户透明(用户无需感知),不影响正常业务流程(如登录、表单提交)。 | 提升用户体验,无需额外操作。 |
性能优化 | 现代浏览器原生支持Web Crypto API,加密/解密操作高效(如AES-GCM在主流设备上耗时小于1ms),对页面性能影响极小。 | 平衡安全性与性能。 |
7. 环境准备
- 开发环境:现代浏览器(Chrome 37+、Firefox 34+、Edge 79+、Safari 11+),支持 Web Crypto API(用于加密/解密)和 LocalStorage/IndexedDB(用于存储)。
- 测试工具:
- 浏览器开发者工具(如Chrome的DevTools)中的 Application > IndexedDB 或 LocalStorage 面板,可查看存储的密文(但无法直接解密,除非拥有密钥)。
- XSS攻击模拟:通过开发者工具的 Console 注入恶意脚本(如
localStorage.getItem('encryptedToken')
),验证密文是否可被直接读取(应只能获取密文,无法还原明文)。
- 代码编辑器:VS Code、Sublime Text等支持HTML/JavaScript的编辑器。
- 依赖库:原生JavaScript(Web Crypto API为浏览器原生提供,无需第三方库)。
8. 实际详细应用代码示例实现(综合案例:用户敏感信息全流程保护)
8.1 需求描述
开发一个Web应用,要求:
- 用户登录后,服务器返回JWT Token,通过AES加密后存储到LocalStorage(防XSS窃取)。
- 用户填写实名认证表单时,身份证号和手机号通过RSA公钥加密后存储到IndexedDB(合规要求)。
- 当用户提交表单时,加密的身份证号和手机号通过API发送到服务器,由服务器使用RSA私钥解密后处理。
8.2 代码实现
(结合AES和RSA,覆盖Token与个人信息的加密存储与传输)
9. 运行结果
- 场景1(Token加密存储):用户登录后,JWT Token被AES加密并存储到LocalStorage,即使通过开发者工具查看LocalStorage,也只能看到密文(无法直接获取Token明文)。
- 场景2(身份证号加密存储):用户填写的身份证号通过RSA公钥加密后存储到IndexedDB,即使设备丢失或浏览器存储被导出,攻击者无私钥则无法解密。
- 场景3(表单提交):加密的身份证号和手机号通过API发送到服务器,服务器使用RSA私钥解密后完成实名认证,确保传输和存储全程加密。
10. 测试步骤及详细代码
- 基础功能测试:
- 加密存储:模拟登录/填写表单,验证敏感数据(Token/身份证号)是否被正确加密并存储到LocalStorage/IndexedDB(通过开发者工具查看存储内容应为密文)。
- 解密还原:在需要使用数据时(如发起API请求),验证是否能正确解密密文并还原明文(如Token用于鉴权、身份证号用于服务器验证)。
- 安全测试:
- XSS攻击模拟:通过开发者工具的 Console 注入脚本(如
localStorage.getItem('encryptedToken')
),确认只能获取密文,无法直接读取明文。 - 密钥泄露测试:模拟密钥被非法获取(如硬编码密钥被泄露),验证加密机制是否仍能保护数据(如使用强随机密钥或用户密码派生密钥)。
- XSS攻击模拟:通过开发者工具的 Console 注入脚本(如
- 性能测试:
- 加密/解密耗时:通过 Performance 面板测量AES/RSA加密和解密操作的耗时(应小于10ms,对用户体验无影响)。
- 边界测试:
- 大数据量加密:测试加密大量敏感数据(如长文本)时是否出现性能问题或存储溢出。
- 密钥轮换:模拟密钥定期更换(如用户修改密码后重新生成AES密钥),验证旧数据是否能通过新密钥解密(或需重新加密)。
11. 部署场景
- 用户认证系统:如Web登录、单点登录(SSO),保护用户Token和会话信息。
- 实名认证服务:如金融、医疗类Web应用,加密存储身份证号、手机号等敏感个人信息。
- 企业内部工具:员工使用的CRM、ERP系统,加密存储客户联系方式、合同附件等商业敏感数据。
- 移动端H5应用:通过WebView嵌入的H5页面(如银行APP内的H5服务),保护用户输入的敏感信息。
12. 疑难解答
- Q1:加密后的数据无法解密(报错“Invalid key”或“Decryption failed”)?
A1:检查密钥是否一致(如AES加密和解密使用同一密钥,RSA公钥加密后必须用对应私钥解密),确认密钥是否被意外修改或丢失。 - Q2:XSS攻击仍能获取敏感数据?
A2:确保密钥未存储在LocalStorage或可被XSS访问的代码中(如全局变量),通过用户密码派生密钥或使用服务器动态下发密钥。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)