这是一个典型的前后端安全数据处理方案设计,需要同时解决数据脱敏展示 、安全传输 和完整数据编辑的问题。以下是完整的解决方案:
一、整体架构设计
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 前端 │ │ 后端 │ │ 数据库 │
├─────────────┤ ├─────────────┤ ├─────────────┤
│ 1.获取掩码数据 │◄──│ 3.返回掩码数据 │◄──│ 4.存储完整数据│
│ 2.提交编辑请求 │──►│ 5.处理编辑请求 │──►│ 6.更新完整数据│
└─────────────┘ └─────────────┘ └─────────────┘
二、核心方案:双ID + 密文传输
方案要点:
-
每个敏感字段使用两个版本:
- 完整版:存储在数据库,用于业务处理
- 掩码版:用于前端展示和大部分API返回
-
安全传输机制:
- 敏感数据使用非对称加密传输
- 前端持有公钥,后端持有私钥
三、详细实施方案
1. 数据库设计
sql
CREATE TABLE users (
id BIGINT PRIMARY KEY,
-- 原始敏感数据(加密存储)
phone_encrypted VARCHAR(128) NOT NULL,
email_encrypted VARCHAR(256),
-- 脱敏数据(明文存储,避免每次计算)
phone_masked VARCHAR(20) DEFAULT NULL, -- 如: 138****1234
email_masked VARCHAR(100) DEFAULT NULL, -- 如: a***@gmail.com
-- 唯一标识符(用于关联编辑)
phone_hash VARCHAR(64) UNIQUE, -- 原始手机号的hash
email_hash VARCHAR(64) UNIQUE, -- 邮箱的hash
-- 版本控制
data_version INT DEFAULT 1,
updated_at TIMESTAMP
);
2. 接口设计
2.1 查询接口(返回脱敏数据)
javascript
// GET /api/user/profile
{
"code": 200,
"data": {
"id": 12345,
"phone": "138****1234", // 掩码版
"email": "a***@gmail.com", // 掩码版
"phone_hash": "a1b2c3d4...", // 用于标识此条数据
"email_hash": "e5f6g7h8...",
"data_version": 3
}
}
2.2 编辑接口(安全提交完整数据)
javascript
// POST /api/user/update-phone
{
"hash": "a1b2c3d4...", // 标识要修改的是哪个字段的哪条数据
"encrypted_data": "RSA_AES_ENCRYPTED_PAYLOAD", // 加密的完整数据
"data_version": 3, // 当前数据版本(防并发冲突)
"signature": "..." // 请求签名
}
3. 加密传输流程
前端处理:
javascript
// 1. 生成临时对称密钥
const aesKey = CryptoJS.lib.WordArray.random(32); // 256-bit AES key
// 2. 用AES加密完整数据
const encryptedData = CryptoJS.AES.encrypt(
JSON.stringify({
phone: '13812345678', // 完整手机号
timestamp: Date.now(), // 防重放
nonce: '随机字符串'
}),
aesKey
).toString();
// 3. 用后端RSA公钥加密AES密钥
const encryptedKey = RSA.encrypt(aesKey.toString(), PUBLIC_KEY);
// 4. 提交到后端
const payload = {
hash: 'a1b2c3d4...',
encrypted_key: encryptedKey,
encrypted_data: encryptedData,
data_version: currentVersion,
signature: signRequest(encryptedData)
};
后端解密:
java
@Service
public class SensitiveDataService {
@Autowired
private RSAKeyService rsaKeyService;
public UpdateResult handleEncryptedUpdate(UpdateRequest request) {
try {
// 1. 验证请求签名
verifySignature(request);
// 2. RSA解密获取AES密钥
String aesKeyStr = rsaKeyService.decrypt(request.getEncryptedKey());
SecretKey aesKey = decodeAESKey(aesKeyStr);
// 3. AES解密数据
String decryptedJson = AES.decrypt(request.getEncryptedData(), aesKey);
UpdateData data = parseUpdateData(decryptedJson);
// 4. 验证数据有效性
validateData(data, request.getDataVersion());
// 5. 更新数据库(完整数据重新加密存储)
updateDatabase(data, request.getHash());
// 6. 生成新的掩码数据和hash
MaskedData masked = generateMaskedData(data.getPhone());
return UpdateResult.success(masked);
} catch (Exception e) {
return UpdateResult.error("解密或验证失败");
}
}
private MaskedData generateMaskedData(String phone) {
return MaskedData.builder()
.phone(maskPhone(phone)) // 138****1234
.phoneHash(hashPhone(phone)) // 新的hash值
.dataVersion(incrementVersion())
.build();
}
}
4. 高级安全措施
4.1 防重放攻击
java
@Component
public class ReplayAttackProtection {
// 使用Redis存储已使用的nonce
@Autowired
private RedisTemplate<String, String> redisTemplate;
public boolean isValidRequest(String nonce, long timestamp) {
// 1. 检查时间戳(5分钟内有效)
if (Math.abs(System.currentTimeMillis() - timestamp) > 5 * 60 * 1000) {
return false;
}
// 2. 检查nonce是否已使用
String key = "nonce:" + nonce;
if (redisTemplate.hasKey(key)) {
return false;
}
// 3. 存储nonce,设置5分钟过期
redisTemplate.opsForValue().set(key, "used", 5, TimeUnit.MINUTES);
return true;
}
}
4.2 数据版本控制(乐观锁)
sql
-- 更新时检查版本
UPDATE users
SET phone_encrypted = ?,
phone_masked = ?,
phone_hash = ?,
data_version = data_version + 1,
updated_at = NOW()
WHERE id = ?
AND phone_hash = ?
AND data_version = ? -- 版本必须匹配
4.3 审计日志
java
@Entity
@Table(name = "sensitive_data_audit_log")
public class SensitiveDataAuditLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long userId;
private String dataType; // "PHONE", "EMAIL"
private String operation; // "VIEW", "UPDATE"
private String oldHash; // 修改前的数据hash
private String newHash; // 修改后的数据hash
private String ipAddress;
private String userAgent;
private LocalDateTime createdAt;
// 不存储实际数据,只存储hash
}
5. 前端完整实现示例
javascript
// sensitiveDataService.js
class SensitiveDataService {
constructor() {
this.publicKey = null;
this.cache = new Map(); // 缓存hash和版本
}
// 初始化获取公钥
async init() {
const response = await fetch('/api/security/public-key');
this.publicKey = await response.text();
}
// 获取脱敏数据
async fetchMaskedData() {
const response = await fetch('/api/user/profile');
const data = await response.json();
// 缓存hash和版本
this.cache.set('phone', {
hash: data.phone_hash,
version: data.data_version,
masked: data.phone
});
return data;
}
// 提交完整数据编辑
async updatePhone(newPhone) {
const cached = this.cache.get('phone');
// 准备加密数据
const payload = {
original_phone: cached.masked, // 原始掩码值(用于验证)
new_phone: newPhone, // 新完整手机号
timestamp: Date.now(),
nonce: this.generateNonce()
};
// 加密传输
const encryptedRequest = await this.encryptRequest(
cached.hash,
payload,
cached.version
);
// 发送请求
const response = await fetch('/api/user/update-phone', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(encryptedRequest)
});
const result = await response.json();
if (result.success) {
// 更新缓存
this.cache.set('phone', {
hash: result.data.phone_hash,
version: result.data.data_version,
masked: result.data.phone
});
}
return result;
}
// 加密请求数据
async encryptRequest(hash, data, version) {
// 生成AES密钥
const aesKey = await window.crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
// 加密数据
const encodedData = new TextEncoder().encode(JSON.stringify(data));
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const encryptedData = await window.crypto.subtle.encrypt(
{ name: "AES-GCM", iv: iv },
aesKey,
encodedData
);
// 导出AES密钥并用RSA加密
const exportedKey = await window.crypto.subtle.exportKey("raw", aesKey);
const encryptedKey = await this.rsaEncrypt(exportedKey);
return {
hash: hash,
encrypted_key: this.arrayBufferToBase64(encryptedKey),
encrypted_data: this.arrayBufferToBase64(encryptedData),
iv: this.arrayBufferToBase64(iv),
data_version: version,
timestamp: Date.now()
};
}
// RSA加密
async rsaEncrypt(data) {
// 使用Web Crypto API进行RSA加密
const publicKey = await this.importPublicKey(this.publicKey);
return await window.crypto.subtle.encrypt(
{ name: "RSA-OAEP" },
publicKey,
data
);
}
// 生成随机nonce
generateNonce() {
return window.crypto.getRandomValues(new Uint32Array(4)).join('');
}
// 工具函数
arrayBufferToBase64(buffer) {
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
}
}
6. 性能优化建议
6.1 缓存脱敏数据
java
@Cacheable(value = "userMaskedData", key = "#userId")
public MaskedData getMaskedData(Long userId) {
// 从数据库查询,避免重复计算掩码
return userRepository.findMaskedData(userId);
}
6.2 批量处理支持
javascript
// 批量更新多个敏感字段
async updateMultipleFields(updates) {
const requests = updates.map(update => ({
field_type: update.type, // 'PHONE', 'EMAIL'
field_hash: update.hash,
encrypted_data: await this.encryptFieldData(update.value)
}));
// 批量提交
return await fetch('/api/user/batch-update', {
method: 'POST',
body: JSON.stringify({ updates: requests })
});
}
四、方案优势总结
-
安全性:
- 传输过程全加密(RSA+AES双保险)
- 数据库不存储明文
- 支持防重放攻击和版本控制
-
实用性:
- 前端可以正常展示脱敏数据
- 编辑时可提交完整数据
- 支持并发修改控制
-
可扩展性:
- 方案适用于各种敏感数据(身份证、银行卡号等)
- 可轻松集成到现有系统
-
合规性:
- 符合数据最小化原则
- 完善的审计日志
- 支持数据脱敏展示
这个方案平衡了安全性、用户体验和系统性能,可以根据实际业务需求进行调整和优化。