H5 敏感数据加密存储方案

举报
William 发表于 2025/08/29 09:28:26 2025/08/29
【摘要】 ​​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),并通过安全方式(如用户密码派生、服务器下发)传递密钥。
  • ​注意事项​​:
    • ​密钥安全​​:加密密钥(如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 代码解析​

  • ​加密流程​​:
    1. ​生成AES密钥​​:使用 crypto.subtle.generateKey() 生成一个256位的AES对称密钥( AES-GCM 算法,适合Web环境)。
    2. ​加密数据​​:将明文Token(如JWT)转换为 ArrayBuffer,生成随机初始化向量(IV,12字节),通过 crypto.subtle.encrypt() 加密数据,合并IV和密文后转换为Base64字符串存储(便于LocalStorage保存)。
  • ​解密流程​​:
    1. ​提取密文​​:从LocalStorage获取Base64密文,还原为 Uint8Array,分离IV(前12字节)和密文(剩余部分)。
    2. ​解密数据​​:使用相同的AES密钥和IV,通过 crypto.subtle.decrypt() 解密密文,还原为明文Token。
  • ​安全注意事项​​:
    • ​密钥管理​​:示例中密钥是临时生成的(仅用于演示),实际场景中密钥应通过 ​​用户密码派生(如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 代码解析​

  • ​加密流程​​:
    1. ​导入RSA公钥​​:将服务器下发的RSA公钥(PEM格式)转换为浏览器可用的 CryptoKey 对象(通过 crypto.subtle.importKey()),用于加密操作。
    2. ​加密身份证号​​:将用户输入的身份证号(明文)转换为 ArrayBuffer,通过 crypto.subtle.encrypt() 使用RSA-OAEP算法(推荐用于加密)加密,生成密文后转换为Base64字符串存储。
  • ​存储流程​​:加密后的身份证号通过 ​​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应用,要求:

  1. 用户登录后,服务器返回JWT Token,通过AES加密后存储到LocalStorage(防XSS窃取)。
  2. 用户填写实名认证表单时,身份证号和手机号通过RSA公钥加密后存储到IndexedDB(合规要求)。
  3. 当用户提交表单时,加密的身份证号和手机号通过API发送到服务器,由服务器使用RSA私钥解密后处理。

​8.2 代码实现​

(结合AES和RSA,覆盖Token与个人信息的加密存储与传输)


​9. 运行结果​

  • ​场景1(Token加密存储)​​:用户登录后,JWT Token被AES加密并存储到LocalStorage,即使通过开发者工具查看LocalStorage,也只能看到密文(无法直接获取Token明文)。
  • ​场景2(身份证号加密存储)​​:用户填写的身份证号通过RSA公钥加密后存储到IndexedDB,即使设备丢失或浏览器存储被导出,攻击者无私钥则无法解密。
  • ​场景3(表单提交)​​:加密的身份证号和手机号通过API发送到服务器,服务器使用RSA私钥解密后完成实名认证,确保传输和存储全程加密。

​10. 测试步骤及详细代码​

  1. ​基础功能测试​​:
    • ​加密存储​​:模拟登录/填写表单,验证敏感数据(Token/身份证号)是否被正确加密并存储到LocalStorage/IndexedDB(通过开发者工具查看存储内容应为密文)。
    • ​解密还原​​:在需要使用数据时(如发起API请求),验证是否能正确解密密文并还原明文(如Token用于鉴权、身份证号用于服务器验证)。
  2. ​安全测试​​:
    • ​XSS攻击模拟​​:通过开发者工具的 ​​Console​​ 注入脚本(如 localStorage.getItem('encryptedToken')),确认只能获取密文,无法直接读取明文。
    • ​密钥泄露测试​​:模拟密钥被非法获取(如硬编码密钥被泄露),验证加密机制是否仍能保护数据(如使用强随机密钥或用户密码派生密钥)。
  3. ​性能测试​​:
    • ​加密/解密耗时​​:通过 ​​Performance​​ 面板测量AES/RSA加密和解密操作的耗时(应小于10ms,对用户体验无影响)。
  4. ​边界测试​​:
    • ​大数据量加密​​:测试加密大量敏感数据(如长文本)时是否出现性能问题或存储溢出。
    • ​密钥轮换​​:模拟密钥定期更换(如用户修改密码后重新生成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

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。