敏感数据加密和模糊匹配

银行等金融机构在处理敏感数据(如身份证号、银行卡号、手机号、客户姓名等)时,有非常严格的合规要求和高安全需求,通常会综合使用多种加密/脱敏/加密查询技术来保障数据安全,同时兼顾性能和查询能力。

下面分为 加密存储方案加密后的查询策略(尤其是模糊匹配) 两部分详细说明:

一、银行对敏感数据的加密存储常见方案

1. 对称加密(如 AES)

  • 用途:存储身份证号、银行卡号、手机号等。

  • 特点

    • 加解密速度快,适合大批量数据。
    • 使用统一密钥或密钥管理系统(KMS)进行集中密钥管理。
  • 例子

    AES.encrypt("6222021234567890", secretKey);
    

2. 不可逆哈希(如 SHA-256)

  • 用途:存储登录密码、身份校验标识(如设备指纹)。

  • 特点

    • 无法解密,只能比对。
    • 一般使用加盐(salt)处理,防止彩虹表攻击。

3. 数据脱敏(masking)

  • 用途:展示时隐藏部分信息,非加密,但提升信息安全。

  • 原数据:6222021234567890
    脱敏显示:6222********7890
    

4. 同态加密(部分银行实验性使用)

  • 可以在加密数据上执行计算,但极慢
  • 多用于高安全分析系统、联邦学习、反洗钱等场景。

5. 专业加密库 + HSM(硬件安全模块)/KMS

  • 银行核心系统会把密钥托管在 HSM 硬件模块 中,禁止明文泄露密钥。
  • 应用只能通过 API 调用加密服务。

二、加密后的查询方式(含模糊匹配)

常规加密(如 AES)不能直接支持模糊查询(like ‘%xxx%’),这是核心挑战!

1. 精确匹配的解决方式

  • 使用加密数据做精确匹配是 可行的,前提是加密算法是确定性加密(deterministic encryption)

    SELECT * FROM customer WHERE phone_enc = AES_ENCRYPT('13888888888', key);
    

2. 模糊查询的解决方案

方案一:加密前建索引字段
  • 拆分数据,建立明文索引字段用于模糊匹配,敏感字段仍加密。

  • 例子

    phone_plainphone_enc
    1388888AES(“13888888888”)

    查询:

    SELECT * FROM customer WHERE phone_plain LIKE '138%';
    
方案二:搜索专用字段(如 hash 前缀)
  • 对敏感字段提取关键字或前缀,单独存储。
  • 查询时比对前缀或特征,减少暴力解密。
方案三:ES(Elasticsearch)做脱敏搜索索引
  • 明文或部分脱敏字段构建专用的搜索引擎索引。
  • 实现模糊查询、拼音匹配、同义词等。
  • 存储结构仍为加密,搜索返回 ID,再从数据库取密文数据解密。
方案四:局部加密 / 结构化加密
  • 对银行卡、手机号做部分加密(如中间几位),支持前/后模糊匹配。

    phone_masked = CONCAT(SUBSTR(phone, 1, 3), '****', SUBSTR(phone, 8, 4))
    
方案五:使用模糊匹配安全方案(FFX 模式)
  • Format Preserving Encryption(格式保持加密)如 FPE/FFX:

    • 加密结果仍是数字或固定格式,可用 LIKE 查询。
    • 示例:Visa 卡号 4567-****-8910 加密后仍为数字串,可搜索前缀。

例如:FPE 加密“6222021234567890” → “6513984838291883”,仍可做前缀模糊匹配。


三、银行实际部署的组合策略

类型使用情况应用场景
AES 加密常用数据库存储、服务接口数据加解密
FPE 加密部分使用卡号、手机号保留结构后支持模糊匹配
哈希 + 加盐常用密码、设备指纹、身份校验标识
脱敏字段常用明文存字段前缀用于查询、日志展示等
HSM / KMS必备加密服务集中管理、密钥生命周期管理
ES 辅助搜索常用名字、地址、模糊匹配、拼音等场景

四、建议做法(实际开发中)

存储设计:

CREATE TABLE customer_info (
    id BIGINT PRIMARY KEY,
    name_enc VARBINARY(256),
    name_search VARCHAR(64), -- 脱敏字段或前缀
    phone_enc VARBINARY(256),
    phone_prefix VARCHAR(10), -- 支持 like "138%"
    ...
)

应用逻辑:

  1. 插入时:加密敏感字段 + 提取前缀字段存入搜索字段。

  2. 查询时:

    • 模糊:查前缀字段。
    • 精确:先加密,再匹配密文字段。
<think>我们有一个需求:在数据库中存储加密敏感数据(如手机号、身份证号),但同时需要支持对这些加密数据的模糊查询(例如,查询手机号包含"123"的用户)。 根据引用内容,我们有以下几种解决方案: 1. 内存解密方案:将整个表的数据加载到内存中,然后解密,再在内存中进行模糊查询。但这样效率低下,不适合大数据量。 2. 明文映射方案:在数据库中存储加密数据的同时,另外存储一个明文的映射表(比如将手机号拆分为前缀、中缀、后缀等),但这样安全性降低。 3. 加密函数方案:在数据库查询时,使用数据库加密函数(如AES_DECRYPT)对加密字段进行解密,然后进行模糊查询。但这样无法利用索引,且每次查询都要解密整个表或大量数据,效率不高。 4. 分片加密方案(推荐):将原始数据按照固定长度分片,然后对每个分片进行加密存储,同时建立分片索引。查询时,将查询条件也分片加密,然后利用索引进行查询。 这里我们重点讨论第四种方案(分片加密)的代码实现,因为它在安全性性能上相对平衡。 步骤: 1. 数据分片:将敏感数据(如手机号)按照固定长度(比如3位)进行分片。例如,手机号"13812345678"可以分片为:138, 381, 812, 123, 234, 345, 456, 567, 678(注意:这里我们使用3位分片,步长为1,即每个分片移动一位)。 2. 加密每个分片:使用加密算法(如AES)对每个分片进行加密,并将加密后的分片存储在另一个表中(我们称之为分片索引表)。 3. 存储原始加密数据:同时,将整个敏感数据加密后存储在原始表中。 4. 查询时:将查询条件同样按照分片规则进行分片,然后加密每个分片,在分片索引表中查询匹配的分片,从而得到原始数据的ID,再根据ID到原始表中获取完整记录。 注意:由于分片索引表存储的是加密后的分片,所以即使被泄露,也无法直接还原出完整的原始数据(除非攻击者知道分片规则加密密钥)。 代码实现(以JavaMySQL为例): 假设我们有一个用户表`user`,其中包含加密的手机号字段`encrypted_phone`,另外我们有一个分片索引表`phone_shard_index`,结构如下: - `user_id`:用户ID - `shard`:分片加密后的字符串 - `shard_index`:分片在原始字符串中的位置(可选) 步骤1:添加用户时,对手机号进行加密存储,并生成分片索引。 步骤2:模糊查询时,将查询字符串分片加密,然后在分片索引表中查找,再根据匹配的user_id获取用户。 具体代码: 1. 加密工具类(使用AES): ```java import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; public class AESUtil { private static final String ALGORITHM = "AES"; private static final String TRANSFORMATION = "AES/ECB/PKCS5Padding"; public static String encrypt(String data, String key) throws Exception { SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), ALGORITHM); Cipher cipher = Cipher.getInstance(TRANSFORMATION); cipher.init(Cipher.ENCRYPT_MODE, secretKey); byte[] encryptedBytes = cipher.doFinal(data.getBytes()); return Base64.getEncoder().encodeToString(encryptedBytes); } public static String decrypt(String encryptedData, String key) throws Exception { SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), ALGORITHM); Cipher cipher = Cipher.getInstance(TRANSFORMATION); cipher.init(Cipher.DECRYPT_MODE, secretKey); byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData)); return new String(decryptedBytes); } } ``` 2. 分片处理工具类: ```java import java.util.ArrayList; import java.util.List; public class ShardUtil { // 生成分片,例如:shardLength=3, step=1 public static List<String> generateShards(String data, int shardLength, int step) { List<String> shards = new ArrayList<>(); if (data == null || data.length() < shardLength) { // 如果数据长度小于分片长度,则整个数据作为一个分片 shards.add(data); return shards; } for (int i = 0; i <= data.length() - shardLength; i += step) { shards.add(data.substring(i, i + shardLength)); } return shards; } } ``` 3. 在存储用户时,同时存储分片索引: ```java // 假设我们有一个用户服务 public class UserService { private static final String AES_KEY = "my-secret-key-123"; // 16,24,32 bytes key private static final int SHARD_LENGTH = 3; private static final int STEP = 1; public void addUser(User user) { // 加密整个手机号 String encryptedPhone = AESUtil.encrypt(user.getPhone(), AES_KEY); user.setEncryptedPhone(encryptedPhone); // 保存用户到数据库,假设userDao.save(user)会返回生成的用户ID userDao.save(user); // 生成分片 List<String> shards = ShardUtil.generateShards(user.getPhone(), SHARD_LENGTH, STEP); for (String shard : shards) { // 加密分片 String encryptedShard = AESUtil.encrypt(shard, AES_KEY); // 保存到分片索引表 phoneShardIndexDao.save(user.getId(), encryptedShard); } } } ``` 4. 模糊查询的实现: ```java public List<User> findUsersByPhoneShard(String query) { // 如果查询字符串长度小于分片长度,则无法使用分片索引,这里我们直接返回空或者采用其他方式(比如内存过滤)但效率低 if (query.length() < SHARD_LENGTH) { // 这里可以抛异常或返回空,或者采用其他方案(如内存解密后过滤,但数据量大时不推荐) return Collections.emptyList(); } // 生成查询条件的分片(注意:我们只需要生成一个分片,因为查询条件可能很长,但我们只需要一个长度等于SHARD_LENGTH的分片就可以利用索引) // 但是,为了确保查询的准确性,我们可能需要将查询条件中所有长度为SHARD_LENGTH的分片都生成出来,然后分别查询,再取并集。 // 这里为了简化,我们取第一个分片(因为模糊查询通常是从头开始匹配,但用户也可能输入中间部分,所以最好遍历所有分片,然后取匹配的user_id的并集) List<String> queryShards = ShardUtil.generateShards(query, SHARD_LENGTH, STEP); Set<Long> userIds = new HashSet<>(); for (String shard : queryShards) { String encryptedShard = AESUtil.encrypt(shard, AES_KEY); // 从分片索引表中查询包含该分片的用户ID List<Long> ids = phoneShardIndexDao.findUserIdsByShard(encryptedShard); userIds.addAll(ids); } // 根据userIds获取用户列表 return userDao.findByIds(userIds); } ``` 注意:以上代码是简化版,实际应用中需要考虑更多细节,比如: - 分片长度步长的选择:长度太短则分片过多,索引表大;长度太长则模糊查询的灵活性降低。 - 查询条件长度小于分片长度时的处理:可以忽略,或者采用其他备用方案(如内存过滤,但数据量小的时候才可行)。 - 性能优化:分片索引表需要建立索引,提高查询效率。 - 安全性:确保密钥的安全存储。 另外,引用[2]中提到PG数据库实现AES加密模糊查询,其思路是在查询时先解密再模糊查询,但这样无法利用索引,性能差。而我们的分片方案可以在分片索引表上建立索引,提高查询效率。 总结:分片加密方案是一种折中的方案,在保证安全性的同时,通过空间换时间(存储分片索引)来实现模糊查询。但要注意,它并不能完全支持所有模糊查询场景(比如查询条件长度小于分片长度时),且增加了存储写入的开销。 §§相关问题§§ 1. 分片加密方案中,分片长度应该如何选择? 2. 除了分片加密方案,还有哪些支持加密数据模糊查询的方案? 3. 如何保证加密密钥的安全性? 4. 分片加密方案在写入时性能影响如何?如何优化?</think>### 数据加密与模糊查询的代码实现方法 #### 一、核心实现方案 1. **加密存储 + 解密查询方案**(推荐) 在查询时对加密字段进行解密后再模糊匹配: ```java // Java示例:使用AES加密 public class AESUtil { private static final String AES_KEY = "your-256-bit-secret-key"; public static String encrypt(String data) throws Exception { Cipher cipher = Cipher.getInstance("AES"); cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(AES_KEY.getBytes(), "AES")); return Base64.getEncoder().encodeToString(cipher.doFinal(data.getBytes())); } public static String decrypt(String encryptedData) throws Exception { Cipher cipher = Cipher.getInstance("AES"); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(AES_KEY.getBytes(), "AES")); return new String(cipher.doFinal(Base64.getDecoder().decode(encryptedData))); } } // SQL查询示例(MySQL) String sql = "SELECT * FROM users WHERE AES_DECRYPT(phone_encrypted, ?) LIKE ?"; PreparedStatement stmt = conn.prepareStatement(sql); stmt.setString(1, AES_KEY); // 密钥 stmt.setString(2, "%123%"); // 模糊查询条件 ``` 2. **分片加密方案**(高性能方案) 将数据分片后加密存储,建立分片索引: ```python # Python分片加密示例 import hashlib def shard_encrypt(data, shard_size=3): shards = [data[i:i+shard_size] for i in range(0, len(data), shard_size)] return [hashlib.sha256(s.encode()).hexdigest() for s in shards] # 存储时 phone = "13800138000" encrypted_shards = shard_encrypt(phone) # 存储到索引表 # 查询时 query_shards = shard_encrypt("800") # 生成查询分片 sql = f"SELECT user_id FROM phone_shard_index WHERE shard IN ({','.join(['?']*len(query_shards))})" ``` #### 二、完整实现流程 1. **数据加密存储** ```java // Java写入加密数据 public void saveUser(User user) throws Exception { String encryptedPhone = AESUtil.encrypt(user.getPhone()); String sql = "INSERT INTO users (name, phone_encrypted) VALUES (?, ?)"; try (PreparedStatement stmt = conn.prepareStatement(sql)) { stmt.setString(1, user.getName()); stmt.setString(2, encryptedPhone); stmt.executeUpdate(); } } ``` 2. **模糊查询处理** ```java // Java解密查询 public List<User> searchByPhone(String partialPhone) throws Exception { List<User> results = new ArrayList<>(); String sql = "SELECT name, phone_encrypted FROM users WHERE AES_DECRYPT(phone_encrypted, ?) LIKE ?"; try (PreparedStatement stmt = conn.prepareStatement(sql)) { stmt.setString(1, AES_KEY); stmt.setString(2, "%" + partialPhone + "%"); ResultSet rs = stmt.executeQuery(); while (rs.next()) { String decryptedPhone = AESUtil.decrypt(rs.getString("phone_encrypted")); results.add(new User(rs.getString("name"), decryptedPhone)); } } return results; } ``` #### 三、安全增强措施 1. **密钥管理** ```java // 从安全存储获取密钥(如AWS KMS/Hashicorp Vault) private static String getKeyFromVault() { // 实现密钥管理系统集成 return System.getenv("ENCRYPTION_KEY"); } ``` 2. **索引优化** ```sql -- 创建函数索引(PostgreSQL示例) CREATE INDEX idx_decrypted_phone ON users USING gin (aes_decrypt(phone_encrypted, 'key') gin_trgm_ops); ``` #### 四、方案对比 | 方案 | 安全性 | 查询性能 | 实现复杂度 | 适用场景 | |---------------------|--------|----------|------------|------------------| | 解密查询 | ★★★★☆ | ★★☆☆☆ | ★★☆☆☆ | 中小数据量 | | 分片加密 | ★★★☆☆ | ★★★★☆ | ★★★★☆ | 大数据量高频查询 | | 数据库原生加密函数 | ★★★★☆ | ★★★☆☆ | ★★☆☆☆ | 兼容数据库功能 | > **重要提示**:模糊查询会暴露部分数据特征,建议结合访问控制、查询日志审计等措施增强安全性[^1][^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

思静鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值