基于Spingboot进行 MyBatis Plus 实现数据库字段级加密和模糊搜索
传统的做法是在每个查询和插入的地方手动加解密,但这样做代码会变得很乱,而且容易遗漏。今天分享一个基于注解的自动加解密方案,通过 Spring Boot + MyBatis 实现,让敏感字段自动加密存储,自动解密使用。在数据安全越来越受重视的今天,如何保护用户的敏感信息成为每个开发者都要面对的问题。比如用户的手机号、身份证、银行卡这些信息,如果直接存在数据库里,一旦数据泄露,后果很严重。如果你也有保
基于注解的自动加解密方案:Spring Boot + MyBatis 实现敏感数据安全存储
引言
在数据安全日益受到重视的今天,保护用户敏感信息已成为每个开发者必须面对的核心问题。用户的手机号、身份证号、银行卡号等敏感信息,如果直接以明文形式存储在数据库中,一旦发生数据泄露,将造成不可估量的后果。
传统的加解密方案通常需要在每个查询和插入的地方手动调用加解密方法,这不仅导致代码重复、难以维护,还容易因遗漏处理而造成安全隐患。本文将介绍一种基于注解的自动加解密方案,通过 Spring Boot + MyBatis 框架实现,让敏感字段的加密存储和解密使用完全自动化,极大地简化开发工作并提升系统安全性。
一、方案概述
本方案的核心思想是:通过自定义注解标记需要加密的字段,利用 MyBatis 拦截器在数据持久化层自动进行加解密操作。这样,业务代码完全不需要关心加解密的细节,只需要像处理普通数据一样操作即可。
主要特点:
无侵入性:只需要在实体类字段上添加注解
自动处理:插入时自动加密,查询时自动解密
集中管理:所有加密逻辑统一维护
安全可靠:采用行业标准加密算法
二、实现方案详解
- 定义加密注解
首先定义一个 @Encrypted 注解,用于标记需要自动加密的字段:
import java.lang.annotation.*;
/**
* 加密注解
* 标记该字段需要自动加密存储
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Encrypted {
/**
* 是否支持模糊查询
* 如果为true,会同时存储加密后的值和哈希值用于模糊查询
*/
boolean supportFuzzyQuery() default false;
}
- 加密工具类
使用 AES-GCM 算法进行加密,该算法提供了认证加密功能,能同时保证数据的机密性和完整性。
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
/**
* 加解密工具类
* 使用 AES-GCM 算法
*/
public class CryptoUtil {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int IV_LENGTH = 12; // GCM推荐使用12字节的IV
private static final int TAG_LENGTH = 128; // 认证标签长度
// 在实际项目中,密钥应该从安全的地方获取,如密钥管理系统
private static SecretKey secretKey;
static {
try {
// 这里仅作示例,实际项目中应从配置获取
String keyStr = "你的加密密钥Base64字符串";
byte[] keyBytes = Base64.getDecoder().decode(keyStr);
secretKey = new SecretKeySpec(keyBytes, "AES");
} catch (Exception e) {
throw new RuntimeException("初始化加密密钥失败", e);
}
}
/**
* 加密明文
*/
public static String encrypt(String plaintext) throws Exception {
if (plaintext == null || plaintext.isEmpty()) {
return plaintext;
}
// 检查是否已经加密,避免重复加密
if (isEncrypted(plaintext)) {
return plaintext;
}
// 生成随机IV
byte[] iv = new byte[IV_LENGTH];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(iv);
// 创建密码器并初始化
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec parameterSpec = new GCMParameterSpec(TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
// 加密数据
byte[] ciphertext = cipher.doFinal(plaintext.getBytes("UTF-8"));
// 组合IV和密文
byte[] encryptedData = new byte[iv.length + ciphertext.length];
System.arraycopy(iv, 0, encryptedData, 0, iv.length);
System.arraycopy(ciphertext, 0, encryptedData, iv.length, ciphertext.length);
// 返回Base64编码的字符串
return Base64.getEncoder().encodeToString(encryptedData);
}
/**
* 解密密文
*/
public static String decrypt(String encryptedText) throws Exception {
if (encryptedText == null || encryptedText.isEmpty()) {
return encryptedText;
}
// 检查是否是加密格式
if (!isEncrypted(encryptedText)) {
return encryptedText;
}
try {
// Base64解码
byte[] encryptedData = Base64.getDecoder().decode(encryptedText);
// 提取IV和密文
byte[] iv = Arrays.copyOfRange(encryptedData, 0, IV_LENGTH);
byte[] ciphertext = Arrays.copyOfRange(encryptedData, IV_LENGTH, encryptedData.length);
// 创建密码器并初始化
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec parameterSpec = new GCMParameterSpec(TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
// 解密数据
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext, "UTF-8");
} catch (Exception e) {
// 如果解密失败,可能是数据损坏或密钥错误
throw new RuntimeException("解密失败,可能是数据损坏或密钥不匹配", e);
}
}
/**
* 检查字符串是否已经是加密格式
* 简单的启发式检查:Base64解码后长度大于IV长度
*/
public static boolean isEncrypted(String value) {
if (value == null || value.isEmpty()) {
return false;
}
try {
byte[] decoded = Base64.getDecoder().decode(value);
return decoded.length > IV_LENGTH;
} catch (Exception e) {
// 如果不是有效的Base64,肯定不是加密数据
return false;
}
}
/**
* 生成加密密钥(仅用于测试或初始化)
*/
public static String generateKey() throws Exception {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(256); // AES-256
SecretKey key = keyGenerator.generateKey();
return Base64.getEncoder().encodeToString(key.getEncoded());
}
}
- MyBatis 拦截器
这是整个方案的核心,负责自动拦截数据库操作并进行加解密处理:
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.lang.reflect.Field;
import java.util.*;
/**
* 加密拦截器
* 自动拦截MyBatis的查询和更新操作,对标记了@Encrypted的字段进行加解密
*/
@Slf4j
@Intercepts({
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class EncryptionInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
String methodName = invocation.getMethod().getName();
// 获取SQL操作参数
Object[] args = invocation.getArgs();
Object parameter = null;
if (args.length > 1) {
parameter = args[1];
}
// UPDATE/INSERT 操作:加密输入参数
if ("update".equals(methodName)) {
encryptParameters(parameter);
}
// 执行原始SQL
Object result = invocation.proceed();
// SELECT 操作:解密查询结果
if ("query".equals(methodName)) {
decryptResult(result);
}
return result;
}
/**
* 加密参数中的敏感字段
*/
private void encryptParameters(Object parameter) {
if (parameter == null) {
return;
}
// 处理Map参数(如@Param注解传递的参数)
if (parameter instanceof Map) {
Map<?, ?> paramMap = (Map<?, ?>) parameter;
for (Object value : paramMap.values()) {
encryptObject(value);
}
}
// 处理实体对象
else if (!isBasicType(parameter.getClass())) {
encryptObject(parameter);
}
}
/**
* 加密单个对象的敏感字段
*/
private void encryptObject(Object obj) {
if (obj == null) {
return;
}
Class<?> clazz = obj.getClass();
// 跳过基本类型和集合
if (isBasicType(clazz) || obj instanceof Collection) {
return;
}
// 获取所有字段
List<Field> fields = getAllFields(clazz);
for (Field field : fields) {
// 检查是否有@Encrypted注解
if (field.isAnnotationPresent(Encrypted.class)) {
try {
field.setAccessible(true);
Object value = field.get(obj);
// 只处理String类型的字段
if (value instanceof String) {
String stringValue = (String) value;
// 非空且未加密才进行加密
if (stringValue != null && !stringValue.isEmpty()
&& !CryptoUtil.isEncrypted(stringValue)) {
String encryptedValue = CryptoUtil.encrypt(stringValue);
field.set(obj, encryptedValue);
log.debug("已加密字段: {}.{}", clazz.getSimpleName(), field.getName());
}
}
} catch (Exception e) {
log.error("加密字段失败: {}.{}", clazz.getSimpleName(), field.getName(), e);
// 这里可以选择抛出异常或继续处理其他字段
}
}
}
}
/**
* 解密查询结果
*/
private void decryptResult(Object result) {
if (result == null) {
return;
}
if (result instanceof List) {
// 处理列表结果
List<?> list = (List<?>) result;
for (Object item : list) {
decryptObject(item);
}
} else {
// 处理单个对象结果
decryptObject(result);
}
}
/**
* 解密单个对象
*/
private void decryptObject(Object obj) {
if (obj == null) {
return;
}
Class<?> clazz = obj.getClass();
// 跳过基本类型
if (isBasicType(clazz)) {
return;
}
List<Field> fields = getAllFields(clazz);
for (Field field : fields) {
if (field.isAnnotationPresent(Encrypted.class)) {
try {
field.setAccessible(true);
Object value = field.get(obj);
if (value instanceof String) {
String stringValue = (String) value;
if (stringValue != null && !stringValue.isEmpty()
&& CryptoUtil.isEncrypted(stringValue)) {
String decryptedValue = CryptoUtil.decrypt(stringValue);
field.set(obj, decryptedValue);
log.debug("已解密字段: {}.{}", clazz.getSimpleName(), field.getName());
}
}
} catch (Exception e) {
log.error("解密字段失败: {}.{}", clazz.getSimpleName(), field.getName(), e);
// 解密失败时,可以选择保留加密值或设置为null
// 这里我们保留原值,避免业务中断
}
}
}
}
/**
* 获取类的所有字段(包括父类)
*/
private List<Field> getAllFields(Class<?> clazz) {
List<Field> fields = new ArrayList<>();
while (clazz != null && clazz != Object.class) {
fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
clazz = clazz.getSuperclass();
}
return fields;
}
/**
* 判断是否是基本类型或包装类
*/
private boolean isBasicType(Class<?> clazz) {
return clazz.isPrimitive() ||
clazz == String.class ||
clazz == Integer.class ||
clazz == Long.class ||
clazz == Double.class ||
clazz == Float.class ||
clazz == Boolean.class ||
clazz == Byte.class ||
clazz == Short.class ||
clazz == Character.class;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 可以从配置文件中读取属性
}
}
- 实体类示例
定义实体类时,只需要在需要加密的字段上添加 @Encrypted 注解:
java
import lombok.Data;
/**
-
用户实体类
*/
@Data
public class User {
private Long id;
private String username;@Encrypted // 手机号自动加密
private String phone;@Encrypted // 邮箱自动加密
private String email;@Encrypted(supportFuzzyQuery = true) // 身份证号自动加密,支持模糊查询
private String idCard;private Integer age;
private String address;
}
/**
-
订单实体类
*/
@Data
public class Order {
private Long id;
private String orderNo;@Encrypted // 银行卡号自动加密
private String bankCard;@Encrypted // 持卡人姓名自动加密
private String cardHolder;private BigDecimal amount;
private Date createTime;
}
- 自动配置类
配置 MyBatis 拦截器自动生效:
import org.apache.ibatis.session.Configuration;
import org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 加密自动配置
*/
@Configuration
@ConditionalOnProperty(name = "data.encryption.enabled", havingValue = "true", matchIfMissing = true)
public class EncryptionAutoConfiguration {
@Bean
public ConfigurationCustomizer encryptionConfigurationCustomizer() {
return new ConfigurationCustomizer() {
@Override
public void customize(Configuration configuration) {
// 添加加密拦截器
configuration.addInterceptor(new EncryptionInterceptor());
log.info("数据加密拦截器已启用");
}
};
}
}
- 配置文件
在 application.yml 中配置:
# 应用配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
加密配置
data:
encryption:
enabled: true # 是否启用加密
key: "your-base64-encoded-aes-256-key" # AES-256密钥,Base64编码
MyBatis配置
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.entity
configuration:
map-underscore-to-camel-case: true
三、使用示例
- 业务代码示例
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author weimeilayer@gmail.com ✨
* @date 💓💕 2023年5月20日 🐬🐇 💓💕
*/
@Service
@Slf4j
@AllArgsConstructor
public class UserService {
private final UserMapper userMapper;
/**
* 创建用户 - 字段自动加密
*/
@Transactional
public Long createUser(User user) {
// 业务代码完全不需要关心加密
// 拦截器会自动加密 phone、email、idCard 字段
userMapper.insert(user);
log.info("用户创建成功,ID: {}", user.getId());
return user.getId();
}
/**
* 查询用户 - 字段自动解密
*/
public User getUserById(Long id) {
User user = userMapper.selectById(id);
if (user != null) {
// 这些字段已经被拦截器自动解密
log.info("查询用户: {}, 手机号: {}", user.getUsername(), user.getPhone());
}
return user;
}
/**
* 更新用户信息
*/
@Transactional
public void updateUser(User user) {
// 更新时也会自动加密
userMapper.updateById(user);
log.info("用户信息更新成功,ID: {}", user.getId());
}
/**
* 批量查询
*/
public List<User> listUsers() {
List<User> users = userMapper.selectList(null);
// 列表中的所有敏感字段都已被自动解密
return users;
}
}
- 数据库存储情况
原始数据:
{
"username": "张三",
"phone": "13600000000",
"email": "zhangsan@example.com",
"idCard": "110101199001011234"
}
数据库存储(加密后):
username: 张三
phone: nTuVgMWime1:hFGa9as6JHxLT2vG8dpiRmu4wtxDnkTEr/1x
email: mK7pL9xQ2rS8vN3w:jKxL9mN2pQ7rS8vT3wX4yZ6aB8cD1eF2g
idCard: X1Y2Z3A4B5C6D7E8:F9G0H1I2J3K4L5M6N7O8P9Q0R1S2T3U4V
- 密钥管理实践
在实际生产环境中,密钥管理非常重要:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
@Configuration
public class SecurityConfig {
/**
* 从安全的地方获取密钥
* 优先级:环境变量 > 配置中心 > 配置文件
*/
@Bean
public SecretKey encryptionSecretKey(
@Value("${data.encryption.key:}") String configKey) {
// 1. 首先尝试从环境变量获取(更安全)
String envKey = System.getenv("ENCRYPTION_SECRET_KEY");
// 2. 如果环境变量没有,使用配置中心的密钥
// String configCenterKey = getFromConfigCenter();
// 3. 最后使用配置文件中的密钥(仅限开发环境)
String keyStr = envKey != null ? envKey : configKey;
if (keyStr == null || keyStr.isEmpty()) {
throw new IllegalStateException("未配置加密密钥");
}
try {
byte[] keyBytes = Base64.getDecoder().decode(keyStr);
return new SecretKeySpec(keyBytes, "AES");
} catch (Exception e) {
throw new RuntimeException("初始化加密密钥失败", e);
}
}
}
- 日志脱敏处理
为了避免敏感信息在日志中泄露,需要实现日志脱敏:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class SensitiveDataMasker {
private static final Logger log = LoggerFactory.getLogger(SensitiveDataMasker.class);
/**
* 手机号脱敏
*/
public String maskPhone(String phone) {
if (phone == null || phone.length() != 11) {
return phone;
}
return phone.substring(0, 3) + "****" + phone.substring(7);
}
/**
* 邮箱脱敏
*/
public String maskEmail(String email) {
if (email == null || !email.contains("@")) {
return email;
}
int atIndex = email.indexOf("@");
if (atIndex <= 2) {
return "***" + email.substring(atIndex);
}
return email.substring(0, 2) + "***" + email.substring(atIndex);
}
/**
* 身份证号脱敏
*/
public String maskIdCard(String idCard) {
if (idCard == null || idCard.length() < 8) {
return idCard;
}
return idCard.substring(0, 6) + "********" + idCard.substring(idCard.length() - 4);
}
/**
* 银行卡号脱敏
*/
public String maskBankCard(String bankCard) {
if (bankCard == null || bankCard.length() < 8) {
return bankCard;
}
return bankCard.substring(0, 4) + " **** **** " + bankCard.substring(bankCard.length() - 4);
}
/**
* 安全日志记录
*/
public void logSafely(String message, Object object) {
if (object instanceof User) {
User user = (User) object;
User maskedUser = new User();
maskedUser.setId(user.getId());
maskedUser.setUsername(user.getUsername());
maskedUser.setPhone(maskPhone(user.getPhone()));
maskedUser.setEmail(maskEmail(user.getEmail()));
maskedUser.setIdCard(maskIdCard(user.getIdCard()));
log.info("{}: {}", message, maskedUser);
} else {
log.info("{}: {}", message, object);
}
}
}
四、进阶功能
- 支持模糊查询
对于需要模糊查询的字段(如姓名搜索),可以额外存储哈希值:
/**
* 增强的加密注解
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptedField {
/**
* 是否支持模糊查询
*/
boolean fuzzySearch() default false;
/**
* 模糊查询时使用的哈希算法
*/
String hashAlgorithm() default "SHA-256";
}
/**
* 模糊查询处理组件
*/
@Component
public class FuzzySearchProcessor {
public String generateSearchHash(String value, String algorithm) throws Exception {
MessageDigest digest = MessageDigest.getInstance(algorithm);
byte[] hash = digest.digest(value.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(hash);
}
}
- 批量操作优化
对于批量插入和查询,可以优化加解密性能:
/**
* 批量加密拦截器
*/
@Intercepts({
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class})
})
public class BatchEncryptionInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object parameter = invocation.getArgs()[1];
// 处理批量插入
if (parameter instanceof Map) {
Map<?, ?> paramMap = (Map<?, ?>) parameter;
if (paramMap.containsKey("list")) {
Object list = paramMap.get("list");
if (list instanceof Collection) {
for (Object item : (Collection<?>) list) {
encryptObject(item);
}
}
}
}
return invocation.proceed();
}
}
五、方案优势与注意事项
优势:
开发效率高:只需添加注解,无需修改业务代码
维护方便:加密逻辑集中管理,易于维护和升级
安全性强:使用标准加密算法,安全性有保障
扩展性好:支持自定义加密算法和策略
注意事项:
密钥管理:妥善保管加密密钥,建议使用专业密钥管理系统
性能影响:加解密操作会带来一定的性能开销,对于大数据量场景需要评估
索引问题:加密后的字段无法直接建立有效索引,需要考虑替代方案
数据迁移:已有系统的数据迁移需要特别处理
备份恢复:备份数据同样需要保护,避免备份文件泄露
六、适用场景
推荐使用:
用户个人信息管理系统
金融支付系统
医疗健康信息系统
政府政务系统
任何需要 GDPR、等保合规的系统
不推荐使用:
对性能要求极高的实时交易系统
需要对加密字段进行复杂查询和排序的场景
数据量特别大的日志存储系统
七、总结
本文介绍的基于注解的自动加解密方案,通过 Spring Boot + MyBatis 实现,为敏感数据保护提供了一种优雅且有效的解决方案。该方案将加解密逻辑从业务代码中解耦,让开发者能够专注于业务逻辑的实现,同时确保敏感数据的安全性。
在实际应用中,建议根据具体业务需求进行适当的调整和优化,比如:
对于特别敏感的数据,可以考虑使用硬件加密模块(HSM)
结合数据库透明加密(TDE)提供多层保护
实现密钥轮换机制,定期更新加密密钥
建立完善的数据安全审计机制
数据安全是一个持续的过程,而不是一次性的任务。通过采用合适的加密方案,结合良好的开发实践和安全意识,我们能够为用户的数据安全提供更有力的保障。
加密字段的模糊查询方案
一、需求分析
对于加密字段的模糊查询,核心问题是:密文是随机的,无法直接进行 LIKE 查询。
对于身份证后4位模糊查询的需求,我们采用冗余存储哈希值的方案:
思路:
额外存储一个字段,专门用于模糊查询
这个字段存储的是身份证后4位的加密值
查询时,将用户输入的后4位进行加密,与这个字段匹配
支持精确匹配身份证后4位,也可以扩展为更多位
二、完整实现方案
- 增强的加密注解
import java.lang.annotation.*;
/**
* 增强版加密注解
* 支持模糊查询功能
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptedField {
/**
* 是否启用模糊查询
*/
boolean fuzzySearch() default false;
/**
* 模糊查询类型
*/
FuzzyType fuzzyType() default FuzzyType.NONE;
/**
* 模糊查询位数(如身份证后4位)
*/
int fuzzyLength() default 4;
/**
* 模糊查询字段在数据库中的列名
* 如果为空,则自动生成:fieldName + "_fuzzy"
*/
String fuzzyColumn() default "";
/**
* 模糊查询的加密算法
* 可以使用与主字段不同的算法,以提高查询性能
*/
String fuzzyAlgorithm() default "AES/GCM/NoPadding";
}
/**
* 模糊查询类型枚举
*/
enum FuzzyType {
NONE, // 不支持模糊查询
SUFFIX, // 后缀匹配(如身份证后4位)
PREFIX, // 前缀匹配(如手机号前3位)
BOTH, // 前后缀都支持
FULL // 全字段模糊(需要特殊处理)
}
- 身份证模糊查询专用注解
/**
* 身份证号专用加密注解
* 自动处理身份证后4位的模糊查询
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IdCardEncrypted {
/**
* 是否启用后4位模糊查询
*/
boolean fuzzyLast4() default true;
/**
* 模糊查询字段列名
*/
String fuzzyColumn() default "id_card_last4";
/**
* 是否启用校验位验证
*/
boolean validateCheckCode() default true;
}
- 增强的实体类
import lombok.Data;
/**
* 用户实体类 - 支持身份证模糊查询
*/
@Data
public class User {
private Long id;
private String username;
@Encrypted
private String phone;
@Encrypted
private String email;
// 主加密字段:完整身份证号
@EncryptedField(
fuzzySearch = true,
fuzzyType = FuzzyType.SUFFIX,
fuzzyLength = 4,
fuzzyColumn = "id_card_last4"
)
private String idCard;
// 或者在数据库层面增加这个字段
// 用于存储身份证后4位的加密值,便于查询
private String idCardLast4Encrypted;
// 其他字段...
}
/**
* 或者使用专门的身份证类
*/
@Data
public class UserWithFuzzyIdCard {
private Long id;
private String username;
// 完整身份证加密存储
@IdCardEncrypted(fuzzyLast4 = true)
private String idCard;
// 或者显式声明两个字段
@Encrypted
private String idCard;
@Encrypted
private String idCardLast4;
}
- 模糊查询处理器
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import java.lang.reflect.Field;
import java.security.SecureRandom;
import java.util.*;
/**
* 模糊查询处理器
* 负责处理加密字段的模糊查询逻辑
*/
@Component
@Slf4j
public class FuzzyQueryProcessor {
// 主加密密钥(用于完整字段加密)
private SecretKey mainSecretKey;
// 模糊查询专用密钥(可以使用不同的密钥)
private SecretKey fuzzySecretKey;
// 模糊查询IV长度(可以更短以提高性能)
private static final int FUZZY_IV_LENGTH = 8;
@PostConstruct
public void init() {
// 初始化密钥
// 实际项目中应从密钥管理系统获取
mainSecretKey = loadKey("main");
fuzzySecretKey = loadKey("fuzzy");
}
/**
* 处理实体对象,为支持模糊查询的字段生成模糊查询值
*/
public void processForFuzzySearch(Object entity) {
if (entity == null) return;
Class<?> clazz = entity.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
// 处理 @EncryptedField 注解
if (field.isAnnotationPresent(EncryptedField.class)) {
EncryptedField annotation = field.getAnnotation(EncryptedField.class);
if (annotation.fuzzySearch()) {
try {
processFieldForFuzzySearch(entity, field, annotation);
} catch (Exception e) {
log.error("处理模糊查询字段失败: {}.{}", clazz.getSimpleName(), field.getName(), e);
}
}
}
// 处理 @IdCardEncrypted 注解
if (field.isAnnotationPresent(IdCardEncrypted.class)) {
try {
processIdCardField(entity, field);
} catch (Exception e) {
log.error("处理身份证字段失败: {}.{}", clazz.getSimpleName(), field.getName(), e);
}
}
}
}
/**
* 处理支持模糊查询的字段
*/
private void processFieldForFuzzySearch(Object entity, Field field, EncryptedField annotation)
throws Exception {
field.setAccessible(true);
Object value = field.get(entity);
if (!(value instanceof String) || StringUtils.isEmpty((String) value)) {
return;
}
String stringValue = (String) value;
// 根据模糊查询类型提取需要加密的部分
String fuzzyPart = extractFuzzyPart(stringValue, annotation.fuzzyType(), annotation.fuzzyLength());
if (StringUtils.isEmpty(fuzzyPart)) {
return;
}
// 加密模糊查询部分
String encryptedFuzzyPart = encryptForFuzzyQuery(fuzzyPart);
// 将加密后的模糊查询值设置到对应的字段
String fuzzyFieldName = getFuzzyFieldName(field, annotation);
setFuzzyFieldValue(entity, fuzzyFieldName, encryptedFuzzyPart);
}
/**
* 专门处理身份证字段
*/
private void processIdCardField(Object entity, Field field) throws Exception {
field.setAccessible(true);
Object value = field.get(entity);
if (!(value instanceof String) || StringUtils.isEmpty((String) value)) {
return;
}
String idCard = (String) value;
// 提取身份证后4位
String last4 = extractLastNChars(idCard, 4);
if (StringUtils.isEmpty(last4)) {
return;
}
// 加密后4位
String encryptedLast4 = encryptForFuzzyQuery(last4);
// 设置到模糊查询字段
IdCardEncrypted annotation = field.getAnnotation(IdCardEncrypted.class);
String fuzzyFieldName = annotation.fuzzyColumn();
if (StringUtils.isEmpty(fuzzyFieldName)) {
fuzzyFieldName = field.getName() + "Last4Encrypted";
}
setFuzzyFieldValue(entity, fuzzyFieldName, encryptedLast4);
}
/**
* 提取模糊查询部分
*/
private String extractFuzzyPart(String value, FuzzyType fuzzyType, int length) {
if (StringUtils.isEmpty(value) || value.length() < length) {
return value; // 如果长度不足,返回原值
}
switch (fuzzyType) {
case SUFFIX:
return value.substring(value.length() - length);
case PREFIX:
return value.substring(0, length);
case BOTH:
// 返回前缀+后缀的组合
String prefix = value.substring(0, Math.min(length, value.length() / 2));
String suffix = value.substring(value.length() - Math.min(length, value.length() / 2));
return prefix + suffix;
case FULL:
// 全字段模糊需要特殊处理,这里返回原值
return value;
default:
return "";
}
}
/**
* 提取字符串后N位
*/
private String extractLastNChars(String value, int n) {
if (StringUtils.isEmpty(value) || value.length() < n) {
return value;
}
return value.substring(value.length() - n);
}
/**
* 为模糊查询加密(可以使用性能更好的算法)
*/
public String encryptForFuzzyQuery(String plaintext) throws Exception {
if (StringUtils.isEmpty(plaintext)) {
return plaintext;
}
// 模糊查询可以使用更简单的加密方式,因为安全性要求相对较低
// 这里使用AES-GCM,但可以使用更短的IV提高性能
// 生成随机IV
byte[] iv = new byte[FUZZY_IV_LENGTH];
new SecureRandom().nextBytes(iv);
// 加密
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.ENCRYPT_MODE, fuzzySecretKey, parameterSpec);
byte[] ciphertext = cipher.doFinal(plaintext.getBytes("UTF-8"));
// 组合IV和密文
byte[] encryptedData = new byte[iv.length + ciphertext.length];
System.arraycopy(iv, 0, encryptedData, 0, iv.length);
System.arraycopy(ciphertext, 0, encryptedData, iv.length, ciphertext.length);
return Base64.getEncoder().encodeToString(encryptedData);
}
/**
* 解密模糊查询值
*/
public String decryptFuzzyQuery(String encryptedText) throws Exception {
if (StringUtils.isEmpty(encryptedText)) {
return encryptedText;
}
try {
byte[] encryptedData = Base64.getDecoder().decode(encryptedText);
byte[] iv = Arrays.copyOfRange(encryptedData, 0, FUZZY_IV_LENGTH);
byte[] ciphertext = Arrays.copyOfRange(encryptedData, FUZZY_IV_LENGTH, encryptedData.length);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.DECRYPT_MODE, fuzzySecretKey, parameterSpec);
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext, "UTF-8");
} catch (Exception e) {
log.error("解密模糊查询值失败", e);
return null;
}
}
/**
* 生成模糊查询条件
* 用于构建查询时,将用户输入转换为加密值进行匹配
*/
public String generateFuzzyQueryCondition(String userInput, FuzzyType fuzzyType, int length)
throws Exception {
if (StringUtils.isEmpty(userInput)) {
return null;
}
// 如果用户输入的就是后4位,直接加密
if (userInput.length() <= length) {
return encryptForFuzzyQuery(userInput);
}
// 如果用户输入的是完整值,提取对应部分
String fuzzyPart = extractFuzzyPart(userInput, fuzzyType, length);
return encryptForFuzzyQuery(fuzzyPart);
}
/**
* 专门生成身份证后4位查询条件
*/
public String generateIdCardLast4Query(String idCardOrLast4) throws Exception {
if (StringUtils.isEmpty(idCardOrLast4)) {
return null;
}
// 提取后4位
String last4;
if (idCardOrLast4.length() <= 4) {
last4 = idCardOrLast4;
} else {
last4 = extractLastNChars(idCardOrLast4, 4);
}
return encryptForFuzzyQuery(last4);
}
/**
* 获取模糊查询字段名
*/
private String getFuzzyFieldName(Field field, EncryptedField annotation) {
if (!StringUtils.isEmpty(annotation.fuzzyColumn())) {
return annotation.fuzzyColumn();
}
return field.getName() + "Fuzzy";
}
/**
* 设置模糊查询字段值
*/
private void setFuzzyFieldValue(Object entity, String fieldName, String value)
throws Exception {
try {
Field fuzzyField = entity.getClass().getDeclaredField(fieldName);
fuzzyField.setAccessible(true);
fuzzyField.set(entity, value);
} catch (NoSuchFieldException e) {
// 如果实体类中没有对应的字段,可以动态添加到Map中(如果使用Map接收)
// 或者记录日志,由数据库层面处理
log.debug("实体类中没有找到模糊查询字段: {}", fieldName);
}
}
private SecretKey loadKey(String keyType) {
// 从配置或密钥管理系统加载密钥
// 这里简化为硬编码,实际项目必须从安全的地方获取
return null;
}
}
- 增强的MyBatis拦截器
/**
* 增强的加密拦截器
* 支持模糊查询字段的自动处理
*/
@Slf4j
@Intercepts({
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class})
})
public class EnhancedEncryptionInterceptor implements Interceptor {
@Autowired
private FuzzyQueryProcessor fuzzyQueryProcessor;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
Object parameter = args[1];
// 处理插入和更新操作
if (parameter != null) {
processParameter(parameter);
}
return invocation.proceed();
}
/**
* 处理参数,加密主字段并生成模糊查询值
*/
private void processParameter(Object parameter) {
if (parameter instanceof Map) {
// 处理Map参数
Map<?, ?> paramMap = (Map<?, ?>) parameter;
for (Object value : paramMap.values()) {
if (value != null && !isBasicType(value.getClass())) {
processEntity(value);
}
}
} else if (!isBasicType(parameter.getClass())) {
// 处理实体对象
processEntity(parameter);
}
}
/**
* 处理单个实体对象
*/
private void processEntity(Object entity) {
// 1. 首先使用原始的加密逻辑加密主字段
encryptMainFields(entity);
// 2. 然后处理模糊查询字段
fuzzyQueryProcessor.processForFuzzySearch(entity);
}
/**
* 加密主字段(原有的加密逻辑)
*/
private void encryptMainFields(Object entity) {
// 这里调用原有的加密逻辑
// 可以使用原有的CryptoUtil或反射处理@Encrypted注解的字段
Class<?> clazz = entity.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
// 原有的@Encrypted注解处理
if (field.isAnnotationPresent(Encrypted.class)) {
encryptField(entity, field);
}
// @EncryptedField注解的主字段也需要加密
if (field.isAnnotationPresent(EncryptedField.class)) {
encryptField(entity, field);
}
// @IdCardEncrypted注解的主字段也需要加密
if (field.isAnnotationPresent(IdCardEncrypted.class)) {
encryptField(entity, field);
}
}
}
private void encryptField(Object entity, Field field) {
try {
field.setAccessible(true);
Object value = field.get(entity);
if (value instanceof String && !CryptoUtil.isEncrypted((String) value)) {
String encrypted = CryptoUtil.encrypt((String) value);
field.set(entity, encrypted);
}
} catch (Exception e) {
log.error("加密字段失败: {}", field.getName(), e);
}
}
private boolean isBasicType(Class<?> clazz) {
return clazz.isPrimitive() ||
clazz == String.class ||
clazz == Integer.class ||
clazz == Long.class ||
clazz == Double.class ||
clazz == Float.class ||
clazz == Boolean.class ||
clazz == Byte.class ||
clazz == Short.class ||
clazz == Character.class;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
- MyBatis Mapper和查询示例
/**
* 用户Mapper
*/
@Mapper
public interface UserMapper {
// 插入用户(自动处理加密和模糊查询字段)
@Insert("INSERT INTO user(username, phone, email, id_card, id_card_last4_encrypted) " +
"VALUES(#{username}, #{phone}, #{email}, #{idCard}, #{idCardLast4Encrypted})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
// 根据ID查询
@Select("SELECT * FROM user WHERE id = #{id}")
User selectById(Long id);
// 根据身份证后4位模糊查询(精确匹配加密值)
@Select("SELECT * FROM user WHERE id_card_last4_encrypted = #{encryptedLast4}")
List<User> selectByIdCardLast4(@Param("encryptedLast4") String encryptedLast4);
// 多条件查询,包含模糊查询
@Select("<script>" +
"SELECT * FROM user WHERE 1=1 " +
"<if test='username != null'> AND username LIKE CONCAT('%', #{username}, '%')</if>" +
"<if test='encryptedLast4 != null'> AND id_card_last4_encrypted = #{encryptedLast4}</if>" +
"</script>")
List<User> selectByCondition(@Param("username") String username,
@Param("encryptedLast4") String encryptedLast4);
// 批量根据身份证后4位查询
@Select("<script>" +
"SELECT * FROM user WHERE id_card_last4_encrypted IN " +
"<foreach collection='encryptedLast4List' item='item' open='(' separator=',' close=')'>" +
"#{item}" +
"</foreach>" +
"</script>")
List<User> selectByIdCardLast4List(@Param("encryptedLast4List") List<String> encryptedLast4List);
}
/**
* 用户服务类 - 包含模糊查询功能
*/
@Service
@Slf4j
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private FuzzyQueryProcessor fuzzyQueryProcessor;
/**
* 创建用户(自动处理加密和模糊查询字段)
*/
@Transactional
public Long createUser(User user) {
// 拦截器会自动处理:
// 1. 加密phone、email、idCard字段
// 2. 生成idCardLast4Encrypted字段
userMapper.insert(user);
return user.getId();
}
/**
* 根据身份证后4位查询用户
*/
public List<User> findUsersByIdCardLast4(String idCardLast4) {
try {
// 1. 将用户输入的身份证后4位加密
String encryptedLast4 = fuzzyQueryProcessor.generateIdCardLast4Query(idCardLast4);
if (encryptedLast4 == null) {
return Collections.emptyList();
}
// 2. 使用加密后的值查询数据库
List<User> users = userMapper.selectByIdCardLast4(encryptedLast4);
// 3. 查询结果会自动解密(由查询拦截器处理)
return users;
} catch (Exception e) {
log.error("根据身份证后4位查询用户失败", e);
return Collections.emptyList();
}
}
/**
* 根据完整身份证号查询用户
*/
public List<User> findUsersByIdCard(String idCard) {
try {
// 即使输入完整身份证号,我们也只使用后4位进行查询
String encryptedLast4 = fuzzyQueryProcessor.generateIdCardLast4Query(idCard);
if (encryptedLast4 == null) {
return Collections.emptyList();
}
return userMapper.selectByIdCardLast4(encryptedLast4);
} catch (Exception e) {
log.error("根据身份证查询用户失败", e);
return Collections.emptyList();
}
}
/**
* 批量查询:根据多个身份证后4位查询
*/
public List<User> findUsersByIdCardLast4List(List<String> idCardLast4List) {
try {
List<String> encryptedList = new ArrayList<>();
for (String last4 : idCardLast4List) {
String encrypted = fuzzyQueryProcessor.generateIdCardLast4Query(last4);
if (encrypted != null) {
encryptedList.add(encrypted);
}
}
if (encryptedList.isEmpty()) {
return Collections.emptyList();
}
return userMapper.selectByIdCardLast4List(encryptedList);
} catch (Exception e) {
log.error("批量查询用户失败", e);
return Collections.emptyList();
}
}
/**
* 综合查询示例
*/
public List<User> searchUsers(String username, String idCardLast4) {
try {
String encryptedLast4 = null;
if (StringUtils.hasText(idCardLast4)) {
encryptedLast4 = fuzzyQueryProcessor.generateIdCardLast4Query(idCardLast4);
}
return userMapper.selectByCondition(username, encryptedLast4);
} catch (Exception e) {
log.error("综合查询用户失败", e);
return Collections.emptyList();
}
}
}
- 数据库表设计
sql
-- 用户表(支持身份证模糊查询)
CREATE TABLE user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL COMMENT '用户名',
-- 加密字段
phone VARCHAR(100) NOT NULL COMMENT '手机号(加密)',
email VARCHAR(200) NOT NULL COMMENT '邮箱(加密)',
id_card VARCHAR(200) NOT NULL COMMENT '身份证号(加密)',
-- 模糊查询字段(存储身份证后4位的加密值)
id_card_last4_encrypted VARCHAR(100) NOT NULL COMMENT '身份证后4位加密值,用于模糊查询',
-- 普通字段
age INT COMMENT '年龄',
address VARCHAR(200) COMMENT '地址',
-- 索引
INDEX idx_username (username),
INDEX idx_id_card_last4 (id_card_last4_encrypted), -- 模糊查询字段建立索引
INDEX idx_phone (phone(20)), -- 加密字段也可以建立前缀索引
INDEX idx_email (email(20))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- 或者更灵活的设计,支持多种模糊查询
CREATE TABLE user_enhanced (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
-- 主加密字段
id_card_encrypted VARCHAR(200) NOT NULL COMMENT '完整身份证加密',
-- 多种模糊查询字段(根据业务需求选择)
id_card_last2_encrypted VARCHAR(50) COMMENT '后2位加密(高冲突率,谨慎使用)',
id_card_last4_encrypted VARCHAR(50) COMMENT '后4位加密',
id_card_last6_encrypted VARCHAR(50) COMMENT '后6位加密',
id_card_prefix_encrypted VARCHAR(50) COMMENT '前6位加密(地区码)',
-- 哈希字段(用于更高效的精确匹配)
id_card_hash CHAR(64) COMMENT '身份证SHA-256哈希值',
-- 创建时间
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- 复合索引
INDEX idx_fuzzy_search (id_card_last4_encrypted, id_card_prefix_encrypted),
INDEX idx_hash (id_card_hash)
) COMMENT='增强版用户表';
- 控制器示例
import lombok.AllArgsConstructor;
/**
* 用户控制器
*/
@RestController
@RequestMapping("/api/users")
@Slf4j
@AllArgsConstructor
public class UserController {
private final UserService userService;
private final SensitiveDataMasker dataMasker;
/**
* 创建用户
*/
@PostMapping
public ApiResponse<Long> createUser(@RequestBody @Valid UserCreateRequest request) {
User user = convertToEntity(request);
Long userId = userService.createUser(user);
return ApiResponse.success(userId);
}
/**
* 根据身份证后4位查询用户
*/
@GetMapping("/search/by-idcard-last4")
public ApiResponse<List<UserResponse>> searchByIdCardLast4(
@RequestParam String idCardLast4) {
// 验证输入(必须是4位数字)
if (!isValidIdCardLast4(idCardLast4)) {
return ApiResponse.error("请输入4位数字的身份证后4位");
}
List<User> users = userService.findUsersByIdCardLast4(idCardLast4);
// 脱敏处理后再返回
List<UserResponse> responses = users.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
return ApiResponse.success(responses);
}
/**
* 综合查询
*/
@GetMapping("/search")
public ApiResponse<List<UserResponse>> searchUsers(
@RequestParam(required = false) String username,
@RequestParam(required = false) String idCardLast4) {
List<User> users = userService.searchUsers(username, idCardLast4);
List<UserResponse> responses = users.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
return ApiResponse.success(responses);
}
/**
* 验证身份证后4位格式
*/
private boolean isValidIdCardLast4(String last4) {
if (last4 == null || last4.length() != 4) {
return false;
}
// 必须是4位数字
return last4.matches("\\d{4}");
}
/**
* 转换为响应对象(脱敏处理)
*/
private UserResponse convertToResponse(User user) {
UserResponse response = new UserResponse();
response.setId(user.getId());
response.setUsername(user.getUsername());
// 脱敏显示
response.setPhone(dataMasker.maskPhone(user.getPhone()));
response.setEmail(dataMasker.maskEmail(user.getEmail()));
response.setIdCard(dataMasker.maskIdCard(user.getIdCard()));
return response;
}
private User convertToEntity(UserCreateRequest request) {
User user = new User();
user.setUsername(request.getUsername());
user.setPhone(request.getPhone());
user.setEmail(request.getEmail());
user.setIdCard(request.getIdCard());
return user;
}
}
/**
* API响应封装
*/
@Data
class ApiResponse<T> {
private boolean success;
private String message;
private T data;
public static <T> ApiResponse<T> success(T data) {
ApiResponse<T> response = new ApiResponse<>();
response.setSuccess(true);
response.setMessage("success");
response.setData(data);
return response;
}
public static <T> ApiResponse<T> error(String message) {
ApiResponse<T> response = new ApiResponse<>();
response.setSuccess(false);
response.setMessage(message);
return response;
}
}
- 性能优化建议
/**
* 模糊查询性能优化配置
*/
@Configuration
public class FuzzyQueryOptimizationConfig {
/**
* 使用更快的加密算法进行模糊查询加密
* 因为模糊查询字段的安全性要求相对较低
*/
@Bean("fuzzyQueryCipher")
public Cipher fuzzyQueryCipher() throws Exception {
// 使用AES/ECB/PKCS5Padding,虽然ECB模式不安全,但用于模糊查询可以接受
// 或者使用AES/CTR/NoPadding提高性能
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
// 使用固定的IV(或计数器初始值),这样相同明文加密结果相同
// 这对于模糊查询是必要的,否则无法进行等值匹配
byte[] fixedIV = new byte[16];
Arrays.fill(fixedIV, (byte) 0);
SecretKeySpec key = new SecretKeySpec(
Base64.getDecoder().decode("你的模糊查询密钥"),
"AES"
);
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(fixedIV));
return cipher;
}
/**
* 模糊查询缓存
* 缓存加密结果,避免重复加密相同内容
*/
@Bean
public CacheManager fuzzyQueryCacheManager() {
return new ConcurrentMapCacheManager("fuzzyQueryCache");
}
}
import lombok.AllArgsConstructor;
/**
* 带缓存的模糊查询处理器
*/
@Component
@Slf4j
@AllArgsConstructor
public class CachedFuzzyQueryProcessor extends FuzzyQueryProcessor {
private final CacheManager cacheManager;
@Override
public String encryptForFuzzyQuery(String plaintext) throws Exception {
if (StringUtils.isEmpty(plaintext)) {
return plaintext;
}
// 先尝试从缓存获取
Cache cache = cacheManager.getCache("fuzzyQueryCache");
Cache.ValueWrapper cached = cache != null ? cache.get(plaintext) : null;
if (cached != null) {
return (String) cached.get();
}
// 缓存未命中,执行加密
String encrypted = super.encryptForFuzzyQuery(plaintext);
// 放入缓存
if (cache != null) {
cache.put(plaintext, encrypted);
}
return encrypted;
}
}
三、其他方案思路
方案一:数据库函数方案(MySQL示例)
sql
-- 1. 创建加密函数(需要在数据库中实现)
DELIMITER $$
CREATE FUNCTION aes_encrypt_fuzzy(plaintext VARCHAR(100))
RETURNS VARCHAR(200) DETERMINISTIC
BEGIN
-- 使用数据库内置的AES加密函数
-- 注意:需要确保数据库密钥安全
RETURN TO_BASE64(AES_ENCRYPT(plaintext, 'your-secret-key'));
END$$
DELIMITER ;
-- 2. 查询时使用函数
SELECT * FROM user
WHERE id_card_last4_encrypted = aes_encrypt_fuzzy('1234');
方案二:哈希 + 加盐方案
java
/**
* 哈希 + 加盐方案
* 用于高并发场景,性能更好
*/
@Component
public class HashFuzzyQueryProcessor {
private final String SALT = "your-fuzzy-query-salt";
/**
* 生成模糊查询值(使用哈希,不可逆但可匹配)
*/
public String generateFuzzyHash(String plaintext) throws Exception {
if (StringUtils.isEmpty(plaintext)) {
return null;
}
// 加盐哈希,防止彩虹表攻击
String saltedText = plaintext + SALT;
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(saltedText.getBytes("UTF-8"));
// 取前16字节作为模糊查询值(可以更短)
byte[] truncatedHash = Arrays.copyOf(hash, 16);
return Base64.getEncoder().encodeToString(truncatedHash);
}
/**
* 批量生成哈希,提高性能
*/
public List<String> batchGenerateFuzzyHash(List<String> plaintexts) throws Exception {
List<String> hashes = new ArrayList<>(plaintexts.size());
MessageDigest digest = MessageDigest.getInstance("SHA-256");
for (String plaintext : plaintexts) {
if (StringUtils.isEmpty(plaintext)) {
hashes.add(null);
continue;
}
String saltedText = plaintext + SALT;
byte[] hash = digest.digest(saltedText.getBytes("UTF-8"));
byte[] truncatedHash = Arrays.copyOf(hash, 16);
hashes.add(Base64.getEncoder().encodeToString(truncatedHash));
// 重置digest
digest.reset();
}
return hashes;
}
}
方案三:布隆过滤器方案(用于快速排除)
/**
* 布隆过滤器方案
* 用于海量数据下的快速过滤
*/
@Component
public class BloomFilterFuzzyQuery {
private BloomFilter<String> bloomFilter;
@PostConstruct
public void init() {
// 初始化布隆过滤器
// 预计元素数量:100万,误判率:0.01%
bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
1000000,
0.0001
);
// 从数据库加载已有数据的模糊查询值到布隆过滤器
loadExistingDataToBloomFilter();
}
/**
* 检查是否可能存在
*/
public boolean mightContain(String idCardLast4) throws Exception {
String fuzzyValue = generateFuzzyValue(idCardLast4);
return bloomFilter.mightContain(fuzzyValue);
}
/**
* 添加新的模糊查询值
*/
public void put(String idCardLast4) throws Exception {
String fuzzyValue = generateFuzzyValue(idCardLast4);
bloomFilter.put(fuzzyValue);
}
private String generateFuzzyValue(String plaintext) throws Exception {
// 可以使用哈希或加密生成
MessageDigest digest = MessageDigest.getInstance("MD5");
byte[] hash = digest.digest(plaintext.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(hash);
}
private void loadExistingDataToBloomFilter() {
// 从数据库加载数据
// 这里需要实现数据加载逻辑
}
}
四、总结
本方案的核心优势:
安全性:主字段使用强加密(AES-GCM),模糊查询字段使用专门的加密
性能:模糊查询字段可以建立索引,查询效率高
灵活性:支持多种模糊查询类型(后缀、前缀等)
易用性:通过注解自动处理,业务代码无感知
使用建议:
根据业务需求选择合适的模糊查询位数:
2位:冲突率高,查询快,适合内部系统
4位:平衡选择,推荐使用
6位:冲突率低,但存储和查询成本略高
密钥管理:
主加密密钥和模糊查询密钥分开管理
定期轮换密钥
使用硬件安全模块(HSM)存储密钥
性能考虑:
为模糊查询字段建立索引
考虑使用缓存减少重复加密
大数据量时考虑分库分表
安全性考虑:
模糊查询字段虽然安全性要求较低,但仍需加密
避免使用ECB模式等不安全加密方式
定期审计和更新加密策略
更多推荐

所有评论(0)