跳到主要内容

敏感数据加密

对敏感数据采用 AES 加密传输,支持前端到后端和后端到前端的双向加密通信,避免数据泄露

注意

采用 AES 加密传输属于纵深防御,在传输数据时,应该确保优先使用 HTTPS

敏感数据说明

什么是敏感数据

敏感数据是指一旦泄露、篡改或破坏可能对个人、企业或国家造成危害的数据信息。这些数据需要特殊的保护措施来确保其机密性、完整性和可用性。

敏感数据分类

1. 个人身份信息(PII)

  • 身份证号码:18位身份证号码、护照号码等
  • 姓名:真实姓名,特别是与其他信息结合时
  • 联系方式:手机号码、邮箱地址、家庭住址
  • 生物特征:指纹、人脸识别数据、虹膜信息
// 示例:个人身份信息
const personalInfo = {
idCard: "110101199001011234", // 需要加密
name: "张三", // 需要加密
phone: "13800138000", // 需要加密
email: "zhangsan@example.com" // 需要加密
};

2. 金融信息

  • 银行卡号:信用卡号、借记卡号
  • 支付信息:支付密码、交易流水
  • 财务数据:收入、资产、负债信息
// 示例:金融敏感信息
const financialInfo = {
cardNumber: "6225880123456789", // 需要加密
paymentPassword: "pay123456", // 需要加密
balance: 50000.00 // 需要加密
};

3. 认证凭据

  • 密码:登录密码、交易密码
  • 令牌:JWT Token、API Key
  • 会话信息:Session ID、Cookie中的敏感信息
// 示例:认证凭据
const authInfo = {
password: "userPassword123", // 需要加密
apiKey: "sk-1234567890abcdef", // 需要加密
jwtToken: "eyJhbGciOiJIUzI1NiIs..." // 需要加密
};

敏感数据识别

敏感数据识别是数据保护的第一步,通过自动化手段快速定位系统中的敏感信息,确保所有需要保护的数据都能被及时发现并采取相应的安全措施。

自动识别的重要性

  • 全面覆盖:避免遗漏任何可能包含敏感信息的字段
  • 降低风险:及时发现潜在的数据泄露风险点
  • 合规要求:满足 GDPR、CCPA 等法规对数据分类的要求
  • 开发效率:减少人工审查工作量,提高开发效率

常用识别规则

下面的正则表达式可以帮助自动识别常见的敏感数据类型:

// 敏感数据识别正则表达式
const sensitivePatterns = {
// 中国大陆18位身份证号码(含校验位)
idCard: /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/,

// 中国大陆手机号码(1开头的11位数字)
phone: /^1[3-9]\d{9}$/,

// 银行卡号(13-19位数字,支持Luhn算法校验)
bankCard: /^\d{13,19}$/,

// 邮箱地址(标准RFC格式)
email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,

// 密码相关字段名(不区分大小写)
password: /password|pwd|pass|secret|key/i
};

// 简单检测函数 - 判断字段是否为敏感数据
function isSensitiveField(fieldName: string, value: string): boolean {
return Object.entries(sensitivePatterns).some(([type, pattern]) =>
pattern.test(fieldName) || pattern.test(value)
);
}

// 使用示例
const testData = {
username: "zhangsan",
idCard: "110101199001011234",
mobile: "13800138000"
};

Object.entries(testData).forEach(([key, value]) => {
if (isSensitiveField(key, value)) {
console.log(`发现敏感字段: ${key} = ${value}`);
}
});

手动标记方式

对于复杂的业务场景,建议结合手动配置的方式来标记敏感字段:

// 配置敏感字段映射表
const sensitiveFields = {
// 个人身份信息
'user.name': 'PII',
'user.idCard': 'PII',
'user.phone': 'PII',
'user.address': 'PII',

// 金融信息
'payment.cardNumber': 'FINANCIAL',
'payment.accountNo': 'FINANCIAL',
'user.salary': 'FINANCIAL',

// 认证凭据
'auth.password': 'AUTH',
'auth.apiKey': 'AUTH',
'session.token': 'AUTH'
};

// 根据字段路径判断敏感级别
function getSensitiveType(fieldPath: string): string | null {
return sensitiveFields[fieldPath] || null;
}

识别结果处理

// 敏感数据扫描结果
interface SensitiveDataResult {
fieldPath: string; // 字段路径
dataType: string; // 敏感数据类型
value: string; // 原始值
riskLevel: 'LOW' | 'MEDIUM' | 'HIGH'; // 风险级别
}

// 扫描函数示例
function scanSensitiveData(data: any, prefix = ''): SensitiveDataResult[] {
const results: SensitiveDataResult[] = [];

Object.entries(data).forEach(([key, value]) => {
const fieldPath = prefix ? `${prefix}.${key}` : key;

if (typeof value === 'string' && isSensitiveField(key, value)) {
results.push({
fieldPath,
dataType: getSensitiveType(fieldPath) || 'UNKNOWN',
value,
riskLevel: 'HIGH'
});
} else if (typeof value === 'object' && value !== null) {
results.push(...scanSensitiveData(value, fieldPath));
}
});

return results;
}

数据脱敏处理

数据脱敏是在保持数据格式和统计特性基本不变的前提下,对敏感信息进行去标识化处理的技术手段。主要用于测试环境、日志记录、数据分析等场景,既保护了隐私又保证了业务功能的正常运行。

脱敏的应用场景

  • 开发测试:在非生产环境中使用脱敏数据进行开发和测试
  • 日志记录:避免在日志中记录完整的敏感信息
  • 数据分析:在进行统计分析时保护个人隐私
  • 第三方对接:向合作伙伴提供脱敏后的数据样本
  • 演示展示:在产品演示中使用虚假但真实的数据格式

常用脱敏方法

// 常用脱敏方法工具类
class DataMasking {
/**
* 身份证号脱敏
* 保留前6位(地区码)和后4位(校验码),中间8位用*替换
* 示例:110101199001011234 -> 110101********1234
*/
static maskIdCard(idCard: string): string {
if (!idCard || idCard.length !== 18) return idCard;
return idCard.replace(/(\d{6})\d{8}(\d{4})/, '$1********$2');
}

/**
* 手机号脱敏
* 保留前3位和后4位,中间4位用*替换
* 示例:13800138000 -> 138****8000
*/
static maskPhone(phone: string): string {
if (!phone || phone.length !== 11) return phone;
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
}

/**
* 银行卡号脱敏
* 只保留后4位,其他位数用*替换,并保持4位一组的格式
* 示例:6225880123456789 -> **** **** **** 6789
*/
static maskBankCard(cardNumber: string): string {
if (!cardNumber) return cardNumber;
const lastFour = cardNumber.slice(-4);
const maskedLength = Math.max(0, cardNumber.length - 4);
const maskedGroups = Math.ceil(maskedLength / 4);
return '**** '.repeat(maskedGroups) + lastFour;
}

/**
* 邮箱脱敏
* 保留第一个字符和@后的域名,用户名其他部分用*替换
* 示例:zhangsan@example.com -> z*******@example.com
*/
static maskEmail(email: string): string {
if (!email || !email.includes('@')) return email;
const [username, domain] = email.split('@');
if (username.length <= 1) return email;
return username[0] + '*'.repeat(username.length - 1) + '@' + domain;
}

/**
* 姓名脱敏
* 保留姓氏,名字部分用*替换
* 示例:张三 -> 张*,李四五 -> 李**
*/
static maskName(name: string): string {
if (!name || name.length <= 1) return name;
return name[0] + '*'.repeat(name.length - 1);
}

/**
* 地址脱敏
* 保留省市信息,详细地址用*替换
* 示例:北京市朝阳区某某街道123号 -> 北京市朝阳区****
*/
static maskAddress(address: string): string {
if (!address) return address;
// 匹配省市区的正则表达式
const match = address.match(/^(.+?[省市].*?[区县市])/);
if (match) {
return match[1] + '****';
}
// 如果没有匹配到标准格式,保留前几个字符
return address.length > 6 ? address.substring(0, 6) + '****' : address;
}

/**
* 自定义脱敏
* 根据传入的脱敏函数对数据进行处理
* @param data 要脱敏的数据对象
* @param rules 自定义脱敏规则,格式为{字段名: (value) => '脱敏后值'}
*/
static maskCustom(data: any, rules: Record<string, (value: string) => string>): any {
if (!data || typeof data !== 'object') return data;

const result = Array.isArray(data) ? [] : {};

Object.entries(data).forEach(([key, value]) => {
if (typeof value === 'string' && rules[key]) {
result[key] = rules[key](value);
} else if (typeof value === 'object' && value !== null) {
result[key] = this.maskCustom(value, rules);
} else {
result[key] = value;
}
});

return result;
}
}

// 批量脱敏处理
class BatchDataMasking {
private static maskingRules: Record<string, (value: string) => string> = {
'idCard': DataMasking.maskIdCard,
'phone': DataMasking.maskPhone,
'mobile': DataMasking.maskPhone,
'bankCard': DataMasking.maskBankCard,
'cardNumber': DataMasking.maskBankCard,
'email': DataMasking.maskEmail,
'name': DataMasking.maskName,
'realName': DataMasking.maskName,
'address': DataMasking.maskAddress
};

/**
* 自动脱敏对象中的敏感字段
* @param data 要脱敏的数据对象
* @param customRules 自定义脱敏规则
*/
static maskObject(data: any, customRules?: Record<string, (value: string) => string>): any {
if (!data || typeof data !== 'object') return data;

const rules = { ...this.maskingRules, ...customRules };
const result = Array.isArray(data) ? [] : {};

Object.entries(data).forEach(([key, value]) => {
if (typeof value === 'string' && rules[key]) {
result[key] = rules[key](value);
} else if (typeof value === 'object' && value !== null) {
result[key] = this.maskObject(value, customRules);
} else {
result[key] = value;
}
});

return result;
}
}

// 使用示例
const originalData = {
name: "张三",
idCard: "110101199001011234",
phone: "13800138000",
email: "zhangsan@example.com",
address: "北京市朝阳区某某街道123号",
cardNumber: "6225880123456789"
};

// 单个字段脱敏
console.log('姓名脱敏:', DataMasking.maskName(originalData.name));
console.log('身份证脱敏:', DataMasking.maskIdCard(originalData.idCard));

// 批量脱敏
const maskedData = BatchDataMasking.maskObject(originalData);
console.log('批量脱敏结果:', maskedData);

数据加密

为了保护敏感数据在传输过程中的安全,我们采用了基于 RSA 和 AES 的混合加密方案,既保证了数据传输的安全性,又兼顾了系统性能。加密传输分为前端到后端和后端到前端两个方向,通过非对称密钥交换和对称加密相结合的方式,实现了端到端的数据保护。

前端到后端加密

前端生成随机 AES 密钥加密敏感数据,然后使用服务端公钥加密 AES 密钥,服务端接收后用私钥解密得到 AES 密钥,再用此密钥解密业务数据。

前端实现

interface ClientToServerRequest {
encryptedAesKey: string;
encryptedData: string;
iv: string;
}

class ClientEncryption {
private serverPublicKey: CryptoKey | null = null;

async init(): Promise<void> {
const { publicKey } = await fetch("/api/server-public-key").then(res => res.json());
this.serverPublicKey = await crypto.subtle.importKey(
"spki",
this.base64ToArrayBuffer(publicKey),
{ name: "RSA-OAEP", hash: "SHA-256" },
false,
["encrypt"]
);
}

async encryptForServer(data: object): Promise<ClientToServerRequest> {
if (!this.serverPublicKey) {
throw new Error("Client encryption not initialized");
}

// 生成随机AES密钥
const aesKey = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);

// 用服务器RSA公钥加密AES密钥
const exportedAesKey = await crypto.subtle.exportKey("raw", aesKey);
const encryptedAesKey = await crypto.subtle.encrypt(
{ name: "RSA-OAEP" },
this.serverPublicKey,
exportedAesKey
);

// 使用AES密钥加密数据
const iv = crypto.getRandomValues(new Uint8Array(12));
const dataToEncrypt = new TextEncoder().encode(JSON.stringify(data));
const encryptedData = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
aesKey,
dataToEncrypt
);

return {
encryptedAesKey: this.arrayBufferToBase64(encryptedAesKey),
encryptedData: this.arrayBufferToBase64(encryptedData),
iv: this.arrayBufferToBase64(iv)
};
}

async sendEncryptedData(data: object): Promise<any> {
const encryptedPayload = await this.encryptForServer(data);

const response = await fetch("/api/secure-data", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(encryptedPayload)
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

return response.json();
}

private arrayBufferToBase64(buffer: ArrayBuffer): string {
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
}

private base64ToArrayBuffer(base64: string): ArrayBuffer {
const binaryString = atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
}

后端实现

// filepath: src/encryption/encryption.controller.ts
import { Controller, Post, Get, Body, Headers, BadRequestException, InternalServerErrorException } from '@nestjs/common';
import { EncryptionService } from './encryption.service';

@Controller('api')
export class EncryptionController {
constructor(private readonly encryptionService: EncryptionService) {}

@Get('server-public-key')
getServerPublicKey() {
return { publicKey: this.encryptionService.getServerPublicKey() };
}

@Post('secure-data')
receiveEncryptedData(@Body() body: { encryptedAesKey: string; encryptedData: string; iv: string }) {
try {
const { encryptedAesKey, encryptedData, iv } = body;
const businessData = this.encryptionService.decryptClientData(encryptedAesKey, encryptedData, iv);

console.log('接收到前端加密数据:', businessData);
return { success: true, message: '数据接收成功', data: businessData };
} catch (error) {
throw new BadRequestException('数据解密失败');
}
}

@Post('encrypted-response')
sendEncryptedData(@Headers('x-client-public-key') clientPublicKey: string) {
try {
if (!clientPublicKey) {
throw new BadRequestException('缺少客户端公钥');
}

const responseData = {
message: "这是来自服务器的敏感数据",
timestamp: new Date().toISOString(),
userInfo: { id: 123, role: "admin" }
};

const encryptedResponse = this.encryptionService.encryptDataForClient(responseData, clientPublicKey);
console.log('发送加密数据给前端');
return encryptedResponse;
} catch (error) {
throw new InternalServerErrorException('服务器加密失败');
}
}
}

后端到前端加密

后端生成随机 AES 密钥加密响应数据,然后使用前端公钥加密 AES 密钥,前端接收后用私钥解密得到 AES 密钥,再用此密钥解密业务数据。

前端实现

interface ServerToClientResponse {
encryptedAesKey: string;
encryptedData: string;
iv: string;
}

class ClientDecryption {
private clientPrivateKey: CryptoKey | null = null;
private clientPublicKey: CryptoKey | null = null;

async init(): Promise<void> {
// 生成客户端RSA密钥对
const keyPair = await crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256"
},
true,
["encrypt", "decrypt"]
);

this.clientPrivateKey = keyPair.privateKey;
this.clientPublicKey = keyPair.publicKey;
}

async getClientPublicKey(): Promise<string> {
if (!this.clientPublicKey) {
throw new Error("Client keys not initialized");
}

const exported = await crypto.subtle.exportKey("spki", this.clientPublicKey);
return this.arrayBufferToBase64(exported);
}

async decryptFromServer(encryptedResponse: ServerToClientResponse): Promise<any> {
if (!this.clientPrivateKey) {
throw new Error("Client decryption not initialized");
}

// 1. 用客户端私钥解密AES密钥
const encryptedAesKeyBuffer = this.base64ToArrayBuffer(encryptedResponse.encryptedAesKey);
const aesKeyBuffer = await crypto.subtle.decrypt(
{ name: "RSA-OAEP" },
this.clientPrivateKey,
encryptedAesKeyBuffer
);

// 2. 导入AES密钥
const aesKey = await crypto.subtle.importKey(
"raw",
aesKeyBuffer,
{ name: "AES-GCM" },
false,
["decrypt"]
);

// 3. 用AES密钥解密数据
const iv = this.base64ToArrayBuffer(encryptedResponse.iv);
const encryptedData = this.base64ToArrayBuffer(encryptedResponse.encryptedData);

const decryptedData = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
aesKey,
encryptedData
);

const jsonString = new TextDecoder().decode(decryptedData);
return JSON.parse(jsonString);
}

async requestEncryptedData(endpoint: string): Promise<any> {
const publicKey = await this.getClientPublicKey();

const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Client-Public-Key": publicKey
}
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const encryptedResponse: ServerToClientResponse = await response.json();
return this.decryptFromServer(encryptedResponse);
}

private arrayBufferToBase64(buffer: ArrayBuffer): string {
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
}

private base64ToArrayBuffer(base64: string): ArrayBuffer {
const binaryString = atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
}

性能优化

密钥复用策略

  • 在同一会话中复用 AES 密钥减少 RSA 操作
  • 实现密钥缓存机制,避免重复生成
  • 批量数据加密时共享密钥

异步处理

  • 使用 Web Workers 进行大数据量加密
  • 流式加密处理大文件
  • 非阻塞式密钥交换

安全注意事项

  • 密钥管理:RSA 私钥必须安全存储,建议使用 HSM 或密钥管理服务
  • 密钥轮换:定期更新 RSA 密钥对
  • AES 密钥随机性:每次传输都应生成新的随机 AES 密钥
  • IV 唯一性:每次 AES 加密都应使用唯一的初始化向量
  • HTTPS 优先:加密传输仍需配合 HTTPS 使用
  • 身份验证:结合 JWT 或其他认证机制验证通信双方身份
  • 重放攻击防护:可添加时间戳和 nonce 防止重放攻击
  • 错误处理:避免在错误信息中泄露密钥信息

参考资料

加密标准与规范

Web 加密 API

安全最佳实践

工具与调试

法规与合规