保障埋点数据安全:加密技术在数据上报中的实践与应用

116 阅读8分钟

在互联网业务中,埋点数据上报是一个关键环节,通过收集用户行为数据,帮助企业了解用户行为,优化产品。然而,埋点数据上报涉及到用户敏感信息的传输与存储,因此数据安全显得尤为重要。本文将介绍埋点数据上报的基本流程,如何通过前端与后端的结合,利用加密技术来保护数据安全,避免数据在传输过程中被恶意窃取或篡改。

一、埋点数据安全上报的基本流程

image.png

埋点数据上报通常包括以下几个步骤:

  1. 数据收集:在前端应用(如 H5 页面)中,通过埋点技术采集用户行为数据。
  2. 数据加密:为了保证数据的安全性,在数据上报前需要使用 SDK 对其进行加密处理,防止敏感信息泄露。
  3. 数据传输:将加密后的数据通过网络传输到后端服务器(也可以是云服务)。
  4. 数据解密与存储:后端服务器接收到加密数据后,进行解密操作并存储到数据库中。
  5. 数据分析:从云服务中拉取业务埋点数据,利用数分服务进行清洗、分析,然后存储数据。
  6. 数据展示:对分析完的业务数据,生成业务报表,为业务决策提供依据。

当然,存储和解密的顺序根据实际项目决定,一般业务场景是业务数据先上报到云服务中,然后数分服务拉取数据进行清洗分析,然后将分析完的数据存储到后台数据库中。

当然,对于一些实时性比较高的业务埋点数据,需要专门设计一套架构,不在此文的讨论范围之内,本文只探讨业务埋点数据的安全上报问题。

二、数据加密技术选型

常见的埋点数据上报方式包括HTTP、GIF、Beacon以及Native SDK等,之前有一篇文章介绍过如何确保在浏览器关闭时完成最后请求:技术和策略分析,这里不再赘述,数据上报方式主要取决于业务需求、数据实时性要求、网络环境等因素。

我们需要考虑的是如何处理敏感的业务数据安全上报,而不会被破解。

有人说,直接使用对称加密就好,的确如此,这种方式最简单直接。

但这里有一个明显的缺点,若黑客拿到对称密钥和加密算法,这一切都白费了。

究竟怎样才能安全加密传输呢?

目前流行的加密算法主要有两种,对称加密(AES)和非对称加密(RSA)。

  1. 对称加密(AES):对称加密算法速度快,适合加密大量数据。AES(Advanced Encryption Standard)是一种常用的对称加密算法。
  2. 非对称加密(RSA):非对称加密相对较慢,但安全性更高,适合加密小数据。RSA 是一种常用的非对称加密算法。

由于我们的埋点业务数据频繁,且数据量较大,因此我们利用这两种方式各自的适用场景,使用混合加密的方式进行加密。

为了在传输过程中保护数据,我们采用对称加密非对称加密相结合的方式,技术方案如下:

  • 对称加密(AES):用于加密实际的数据。
  • 非对称加密(RSA):用于加密对称加密的密钥。

这样采用混合的双重加密方案,基本能保障埋点数据的安全。当然,互联网没有绝对安全的东西,因此这也只能提高破解难度,增加破解成本而已,但这对日常的 App 应用已经足够了。

三、前端数据加密与上报

还是那句话Talk is cheap show me the code,下面就让我们用一个简单的例子来演示一下前端敏感数据如何安全上报。

3.1 SDK 封装

我们可以封装一个前端 SDK,进行数据加密与上报。该 SDK 使用 AES 对采集的数据进行加密,并使用 RSA 公钥加密 AES 密钥,同时对称密钥 AES 每一次上报都不相同,进一步提高破解难度。

import CryptoJS from 'crypto-js';
import JSEncrypt from 'jsencrypt';

class EncryptSDK {
    constructor() {
       // 这里帖的是你的publicKey,下面会介绍如何生成它
        const publicKey = `-----BEGIN PUBLIC KEY-----
            YOUR_RSA_PUBLIC_KEY_HERE
            -----END PUBLIC KEY-----`;
        this.publicKey = publicKey;
    }

    // 每次都生成随机的对称密钥(AES 密钥)
    generateSymmetricKey() {
        return CryptoJS.lib.WordArray.random(32).toString();
    }

    // 使用 AES 对数据进行加密
    encryptData(data, symmetricKey) {
        const dataString = JSON.stringify(data);
        const iv = CryptoJS.lib.WordArray.random(16);
        const encryptedData = CryptoJS.AES.encrypt(dataString, CryptoJS.enc.Hex.parse(symmetricKey), {
            iv: iv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        }).toString();

        return {
            encryptedData,
            iv: iv.toString()
        };
    }

    // 使用 RSA 公钥对对称密钥进行加密
    encryptKeyWithRSA(symmetricKey) {
        const encryptor = new JSEncrypt();
        encryptor.setPublicKey(this.publicKey);
        return encryptor.encrypt(symmetricKey);
    }

    // 加密并上报数据(GIF 请求)
    report(data) {
        // 对称密钥每一次都不相同
        const symmetricKey = this.generateSymmetricKey();
        const encrypted = this.encryptData(data, symmetricKey);
        const encryptedSymmetricKey = this.encryptKeyWithRSA(symmetricKey);

        // 将加密后的数据拼接成查询参数
        const queryParams = new URLSearchParams({
            data: encrypted.encryptedData,
            key: encryptedSymmetricKey,
            iv: encrypted.iv
        });
        
        // TODO: 本地的缓存和防丢失处理,先存在本地,然后再上报
        // ...
        
        // 服务器上报地址,实际业务替换成sls云服务地址
        const reportUrl = 'http://localhost:3000/report';

        // 构造 GIF 请求 URL
        const img = new Image();
        img.src = `${reportUrl}?${queryParams.toString()}`;

        // 使用 Image 对象的 onload/onerror 事件监听请求完成
        img.onload = () => {
            console.log('Data reported successfully via GIF.');
        };

        img.onerror = (error) => {
            console.error('Error reporting data via GIF:', error);
        };
    }
}

export default EncryptSDK;

3.2 前端使用示例

使用封装好的 SDK 并通过 GIF 请求上报数据,创建一个 index.html 文件如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Data Encryption and Reporting SDK with GIF</title>
    <script type="module">
        import EncryptSDK from './EncryptSDK.js';

        function reportData() {
            // 初始化 SDK,实际业务中还会有项目名称、版本等一些列配置
            const sdk = new EncryptSDK();

            // 要上报的数据
            const data = { event: 'page_view', timestamp: Date.now() };

            // 通过 GIF 请求上报数据
            sdk.report(data);
        }
    </script>
</head>
<body>
    <button onclick="reportData()">Report Data</button>
</body>
</html>

四、后端数据解密与处理

实际业务会更复杂,这里为了演示,简化了架构,直接使用一个 Node 服务进行处理。

4.1 Koa 服务器

后端使用 Koa 服务器接收并处理前端的加密数据。它将解密 RSA 加密的对称密钥,然后使用该密钥解密实际数据。

const Koa = require('koa');
const crypto = require('crypto');
const fs = require('fs');

const app = new Koa();

// 服务器存储的私钥文件路径
const privateKeyPath = 'xxx/private_key.pem';

// 加载 RSA 私钥
const privateKey = fs.readFileSync(privateKeyPath, 'utf8');

// 使用 RSA 私钥解密对称密钥
function decryptKeyWithRSA(encryptedKey) {
    return crypto.privateDecrypt(privateKey, Buffer.from(encryptedKey, 'base64'));
}

// 使用 AES 解密数据
function decryptData(encryptedData, symmetricKey, iv) {
    const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(symmetricKey, 'hex'), Buffer.from(iv, 'hex'));
    let decrypted = decipher.update(encryptedData, 'base64', 'utf8');
    decrypted += decipher.final('utf8');
    return decrypted;
}

// 处理 GET 请求(GIF 上报)
app.use(async (ctx) => {
    if (ctx.path === '/report' && ctx.method === 'GET') {
        try {
            const { data, key, iv } = ctx.query;

            // 解密对称密钥
            const decryptedSymmetricKey = decryptKeyWithRSA(key);

            // 解密数据
            const decryptedData = decryptData(data, decryptedSymmetricKey, iv);

            console.log('Decrypted Data:', decryptedData);

            // 返回一个 1x1 的透明 GIF
            ctx.status = 200;
            ctx.type = 'image/gif';
            ctx.body = Buffer.from([
                0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00,
                0x80, 0xff, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x2c,
                0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02,
                0x02, 0x44, 0x01, 0x00, 0x3b
            ]);
        } catch (error) {
            console.error('Error decrypting data:', error);
            ctx.status = 500;
        }
    } else {
        ctx.status = 404;
    }
});

// 启动服务器
app.listen(3000, () => {
    console.log('Koa server is running on http://localhost:3000');
});

4.2 运行步骤

  1. 准备 RSA 密钥对

    • 生成 RSA 私钥和公钥:
    openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
    openssl rsa -pubout -in private_key.pem -out public_key.pem
    
    • public_key.pem 内容复制到前端代码的 publicKey 变量中。
    • private_key.pem 文件复制到后端项目的目录之中。
  2. 启动服务

    cd 进入项目目录之中,在命令窗口中执行如下代码,启动服务:

    // 前端服务
    open index.html
    
    // 后端服务,server为后端服务目录
    cd server
    node server.js
    
  3. 在浏览器中运行前端代码

    • 打开前端 HTML 文件,在浏览器中点击 "Report Data" 按钮。
    • 数据会被加密后以 GIF 请求的方式发送到后端,后端解密数据并打印出来。

4.3 注意事项

  1. 数据大小限制:由于数据通过查询参数传输,URL 长度受限制。加密后的数据应尽量精简,防止超出浏览器和服务器对 URL 长度的限制,因此是否考虑对数据进行压缩传输,要结合实际业务进行综合考量。
  2. 密钥管理:应定期更换 RSA 密钥对,并对密钥进行安全管理,防止泄露。
  3. 通信安全:强烈建议使用 HTTPS 进行通信,防止数据在传输过程中被窃听或中间人攻击。

五、总结

通过上述方式,我们实现了一个前端到后端的业务敏感数据加密上报流程。在这个流程中:

  1. 前端使用 AES 对数据进行加密,保证数据传输过程中的安全性。
  2. 使用 RSA 对 AES 密钥进行加密,防止对称密钥被窃取。
  3. 数据通过 GIF 请求方式上报,绕过了跨域限制,并且不会阻塞页面的其他操作。
  4. 后端使用 Koa 服务器接收并解密数据,保证数据在服务器端的完整性和可读性。

通过这些措施,可以有效地保护业务埋点数据的安全,防止敏感信息泄露,为用户提供更好的数据隐私保障。