每当项目进入安全合规阶段,总会听到这样的需求:"数据库里的身份证、手机号必须加密存储!"而且往往是业务已经开发了一半,突然被告知要改造,顿时头大。尤其使用 MyBatis Plus 这样的 ORM 框架时,如何在不影响现有代码的情况下实现加密存储、同时在前端展示时又要做脱敏,成了很多开发者的痛点。本文将分享一套实用的解决方案,帮你优雅地解决这一难题。
加密方案设计
加密算法选择
在选择加密算法时,我们需要综合考虑安全性、性能和易用性:
算法 | 类型 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
AES | 对称加密 | 速度快、实现简单 | 密钥管理挑战 | 大批量敏感数据 |
RSA | 非对称加密 | 安全性高 | 加解密速度慢 | 少量关键数据 |
SM4 | 对称加密 | 国密算法、安全合规 | 库支持有限 | 政务/金融系统 |
对于 MyBatis Plus 环境,我推荐使用AES-GCM 模式,它同时提供了加密和数据完整性验证,性能也相对较好。
以下是优化后的 AES-GCM 加密工具类实现:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class AESEncryptor {
private static final Logger log = LoggerFactory.getLogger(AESEncryptor.class);
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 16;
// 检查密钥长度是否合法(AES要求16/24/32字节)
private static void validateKey(String key) {
int keyLength = key.getBytes().length;
if (keyLength != 16 && keyLength != 24 && keyLength != 32) {
throw new IllegalArgumentException("AES密钥长度必须为16/24/32字节");
}
}
public static String encrypt(String plainText, String key) {
if (plainText == null || plainText.isEmpty()) {
return plainText;
}
try {
validateKey(key);
// 生成随机IV
byte[] iv = new byte[GCM_IV_LENGTH];
new java.security.SecureRandom().nextBytes(iv);
// 初始化加密器
SecretKey secretKey = new SecretKeySpec(key.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
// 加密
byte[] cipherText = cipher.doFinal(plainText.getBytes());
// 组合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);
} catch (Exception e) {
log.error("数据加密失败", e);
throw new DataEncryptException("加密操作异常", e);
}
}
public static String decrypt(String encryptedText, String key) {
if (encryptedText == null || encryptedText.isEmpty()) {
return encryptedText;
}
try {
validateKey(key);
// Base64解码
byte[] encryptedData = Base64.getDecoder().decode(encryptedText);
// 提取IV
byte[] iv = new byte[GCM_IV_LENGTH];
System.arraycopy(encryptedData, 0, iv, 0, iv.length);
// 提取密文
byte[] cipherText = new byte[encryptedData.length - GCM_IV_LENGTH];
System.arraycopy(encryptedData, GCM_IV_LENGTH, cipherText, 0, cipherText.length);
// 初始化解密器
SecretKey secretKey = new SecretKeySpec(key.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
// 解密
byte[] plainText = cipher.doFinal(cipherText);
return new String(plainText);
} catch (Exception e) {
log.error("数据解密失败", e);
throw new DataEncryptException("解密操作异常", e);
}
}
}
// 自定义加解密异常
class DataEncryptException extends RuntimeException {
public DataEncryptException(String message, Throwable cause) {
super(message, cause);
}
}
支持多算法的加密接口
为了支持不同加密算法(如政务系统需要的 SM4 国密算法),我们可以设计一个通用接口:
public interface Encryptor {
String encrypt(String plainText, String key);
String decrypt(String cipherText, String key);
}
// AES实现
public class AESEncryptor implements Encryptor {
@Override
public String encrypt(String plainText, String key) {
// 调用前面定义的AES加密方法
return AESEncryptor.encrypt(plainText, key);
}
@Override
public String decrypt(String cipherText, String key) {
return AESEncryptor.decrypt(cipherText, key);
}
}
// SM4国密实现
public class SM4Encryptor implements Encryptor {
@Override
public String encrypt(String plainText, String key) {
// 实现SM4加密算法
// ...
return encryptedText;
}
@Override
public String decrypt(String cipherText, String key) {
// 实现SM4解密算法
// ...
return plainText;
}
}
// 加密算法工厂
public class EncryptorFactory {
public static Encryptor getEncryptor(String algorithm) {
switch (algorithm.toUpperCase()) {
case "AES":
return new AESEncryptor();
case "SM4":
return new SM4Encryptor();
default:
throw new IllegalArgumentException("不支持的加密算法: " + algorithm);
}
}
}
密钥管理策略
密钥管理是安全系统的核心。以下是一个改进后的密钥管理器,支持密钥轮换:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
@Component
public class KeyManager {
private final Map<String, String> currentKeys = new HashMap<>();
private final Map<String, String> oldKeys = new HashMap<>();
@Value("${encryption.algorithm:AES}")
private String algorithm;
@Value("${encryption.key.idcard}")
private String idCardKey;
@Value("${encryption.key.phone}")
private String phoneKey;
@Value("${encryption.key.bankcard}")
private String bankCardKey;
// 旧密钥(用于密钥轮换过渡期)
@Value("${encryption.old.key.idcard:}")
private String oldIdCardKey;
@Value("${encryption.old.key.phone:}")
private String oldPhoneKey;
@Value("${encryption.old.key.bankcard:}")
private String oldBankCardKey;
private Encryptor encryptor;
@PostConstruct
public void init() {
// 校验密钥是否配置
if (StringUtils.isEmpty(idCardKey)) {
throw new IllegalArgumentException("身份证加密密钥未配置");
}
if (StringUtils.isEmpty(phoneKey)) {
throw new IllegalArgumentException("手机号加密密钥未配置");
}
if (StringUtils.isEmpty(bankCardKey)) {
throw new IllegalArgumentException("银行卡加密密钥未配置");
}
// 初始化当前密钥
currentKeys.put(KeyType.ID_CARD.name(), idCardKey);
currentKeys.put(KeyType.PHONE.name(), phoneKey);
currentKeys.put(KeyType.BANK_CARD.name(), bankCardKey);
// 初始化旧密钥(如果存在)
if (!StringUtils.isEmpty(oldIdCardKey)) {
oldKeys.put(KeyType.ID_CARD.name(), oldIdCardKey);
}
if (!StringUtils.isEmpty(oldPhoneKey)) {
oldKeys.put(KeyType.PHONE.name(), oldPhoneKey);
}
if (!StringUtils.isEmpty(oldBankCardKey)) {
oldKeys.put(KeyType.BANK_CARD.name(), oldBankCardKey);
}
// 初始化加密算法
this.encryptor = EncryptorFactory.getEncryptor(algorithm);
}
// 获取当前密钥
public String getCurrentKey(KeyType keyType) {
return currentKeys.get(keyType.name());
}
// 获取旧密钥(如果存在)
public String getOldKey(KeyType keyType) {
return oldKeys.get(keyType.name());
}
// 加密方法
public String encrypt(String plainText, KeyType keyType) {
if (plainText == null || plainText.isEmpty()) {
return plainText;
}
return encryptor.encrypt(plainText, getCurrentKey(keyType));
}
// 解密方法(先尝试当前密钥,失败则尝试旧密钥)
public String decrypt(String cipherText, KeyType keyType) {
if (cipherText == null || cipherText.isEmpty()) {
return cipherText;
}
try {
// 先用当前密钥解密
return encryptor.decrypt(cipherText, getCurrentKey(keyType));
} catch (Exception e) {
// 当前密钥解密失败,尝试旧密钥
String oldKey = getOldKey(keyType);
if (oldKey != null) {
return encryptor.decrypt(cipherText, oldKey);
}
throw e; // 无旧密钥或旧密钥也解密失败,抛出异常
}
}
// 密钥类型枚举
public enum KeyType {
ID_CARD, PHONE, BANK_CARD
}
}
在实际生产环境中,密钥应当从专业的密钥管理系统(KMS)获取,而非直接存储在配置文件中。
基于注解的 TypeHandler 设计
自定义加密注解
首先,我们定义一个注解来标记需要加密的字段:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标记需要加密的字段
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Encrypt {
KeyManager.KeyType value();
}
通用加密 TypeHandler
然后实现统一的 TypeHandler,通过 Spring 获取 KeyManager:
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@Component
public class EncryptTypeHandler extends BaseTypeHandler<String> implements ApplicationContextAware {
private static ApplicationContext applicationContext;
private KeyManager keyManager;
private KeyManager.KeyType keyType;
public EncryptTypeHandler() {
// 默认构造函数,由MyBatis初始化
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
EncryptTypeHandler.applicationContext = applicationContext;
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
throws SQLException {
ensureKeyManager();
try {
// 加密后存入数据库
ps.setString(i, keyManager.encrypt(parameter, keyType));
} catch (Exception e) {
throw new SQLException("字段加密失败", e);
}
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
String value = rs.getString(columnName);
return decryptValue(value);
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
return decryptValue(value);
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
return decryptValue(value);
}
private String decryptValue(String value) throws SQLException {
if (value == null) {
return null;
}
ensureKeyManager();
try {
// 从数据库读取后解密
return keyManager.decrypt(value, keyType);
} catch (Exception e) {
throw new SQLException("字段解密失败", e);
}
}
// 确保KeyManager和keyType已初始化
private void ensureKeyManager() {
if (keyManager == null) {
keyManager = applicationContext.getBean(KeyManager.class);
}
}
// 由MyBatis Plus配置调用,设置当前处理的字段属性
public void setConfiguration(Field field) {
if (!field.isAnnotationPresent(Encrypt.class)) {
throw new IllegalArgumentException("字段[" + field.getName() + "]未配置@Encrypt注解");
}
Encrypt annotation = field.getAnnotation(Encrypt.class);
this.keyType = annotation.value();
}
}
自定义 TypeHandler 配置类
为了将 TypeHandler 与 MyBatis Plus 集成,我们需要一个配置类:
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
import java.lang.reflect.Field;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisSqlSessionFactoryBean sqlSessionFactory(DataSource dataSource, EncryptTypeHandler encryptTypeHandler) throws Exception {
MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
// 配置mapper位置
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(resolver.getResources("classpath*:/mapper/**/*.xml"));
// 配置全局TypeHandler
MybatisConfiguration configuration = new MybatisConfiguration();
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
// 注册通用加密TypeHandler
typeHandlerRegistry.register(String.class, encryptTypeHandler);
factoryBean.setConfiguration(configuration);
return factoryBean;
}
/**
* 自定义MyBatis Plus处理器,用于拦截实体类字段处理
*/
@Bean
public EncryptFieldProcessor encryptFieldProcessor(EncryptTypeHandler encryptTypeHandler) {
return new EncryptFieldProcessor(encryptTypeHandler);
}
/**
* 实体字段加密处理器,拦截带有@Encrypt注解的字段
*/
public static class EncryptFieldProcessor {
private final EncryptTypeHandler encryptTypeHandler;
public EncryptFieldProcessor(EncryptTypeHandler encryptTypeHandler) {
this.encryptTypeHandler = encryptTypeHandler;
}
// 处理实体类的加密字段
public void processEncryptFields(Object entity) {
if (entity == null) {
return;
}
MetaObject metaObject = SystemMetaObject.forObject(entity);
Class<?> entityClass = entity.getClass();
// 扫描实体类中的@Encrypt注解
for (Field field : entityClass.getDeclaredFields()) {
if (field.isAnnotationPresent(Encrypt.class)) {
String fieldName = field.getName();
Object fieldValue = metaObject.getValue(fieldName);
if (fieldValue instanceof String) {
// 设置当前处理的字段属性
encryptTypeHandler.setConfiguration(field);
// 执行加密操作
String encryptedValue = encryptTypeHandler.encrypt((String) fieldValue);
metaObject.setValue(fieldName, encryptedValue);
}
}
}
}
}
}
实体类配置
在实体类中,我们只需要使用@Encrypt
注解标记需要加密的字段:
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("user_info")
public class UserInfo {
private Long id;
private String name;
@Encrypt(KeyManager.KeyType.ID_CARD)
private String idCard;
@Encrypt(KeyManager.KeyType.PHONE)
private String phoneNumber;
@Encrypt(KeyManager.KeyType.BANK_CARD)
private String bankCardNo;
// 用于模糊查询的辅助字段(存储部分明文)
private String phoneSearchKey;
}
加密数据查询问题
加密后的数据最大的挑战是如何查询。下面提供两种方案:
1. 服务层加密参数
@Service
public class UserService extends ServiceImpl<UserMapper, UserInfo> {
@Autowired
private KeyManager keyManager;
/**
* 通过手机号精确查询用户
*/
public UserInfo findByPhone(String phone) {
if (phone == null || phone.isEmpty()) {
return null;
}
// 加密查询参数
String encryptedPhone = keyManager.encrypt(phone, KeyManager.KeyType.PHONE);
return lambdaQuery().eq(UserInfo::getPhoneNumber, encryptedPhone).one();
}
}
2. 辅助搜索字段
对于需要模糊查询的场景,单纯的加密字段是无法满足的,因为加密后的数据无法使用 LIKE 操作。解决方案是添加额外的搜索字段:
@Service
public class UserService extends ServiceImpl<UserMapper, UserInfo> {
/**
* 插入或更新用户时,自动生成搜索键
*/
@Transactional
public boolean saveOrUpdateUser(UserInfo user) {
// 生成手机号搜索键(例如保留前3位和后4位)
String phone = user.getPhoneNumber();
if (phone != null && phone.length() >= 7) {
// 存储部分明文用于模糊查询
user.setPhoneSearchKey(phone.substring(0, 3) + "*" + phone.substring(phone.length() - 4));
}
return this.saveOrUpdate(user);
}
/**
* 模糊查询手机号
*/
public List<UserInfo> findByPhoneLike(String phonePattern) {
return lambdaQuery()
.like(UserInfo::getPhoneSearchKey, phonePattern)
.list();
}
}
接口返回脱敏实现
在返回前端数据时,即使已经从数据库中读取并解密了敏感信息,我们也通常需要进行脱敏处理。
自定义脱敏注解
首先定义脱敏注解和策略:
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 敏感数据脱敏注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveDataSerializer.class)
public @interface SensitiveData {
// 脱敏类型
SensitiveType type();
// 保留前几位(默认值-1表示使用类型默认设置)
int prefixLength() default -1;
// 保留后几位(默认值-1表示使用类型默认设置)
int suffixLength() default -1;
// 脱敏类型枚举
enum SensitiveType {
ID_CARD, // 身份证号
PHONE, // 手机号
BANK_CARD, // 银行卡号
NAME, // 姓名
EMAIL, // 邮箱
ADDRESS // 地址
}
}
脱敏序列化器
然后实现对应的序列化器:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class SensitiveDataSerializer extends JsonSerializer<String> implements ContextualSerializer {
private SensitiveData.SensitiveType type;
private int prefixLength = -1;
private int suffixLength = -1;
// 默认脱敏规则
private static final Map<SensitiveData.SensitiveType, MaskRule> DEFAULT_RULES = new ConcurrentHashMap<>();
static {
// 初始化默认脱敏规则
DEFAULT_RULES.put(SensitiveData.SensitiveType.ID_CARD, new MaskRule(3, 4)); // 身份证前3后4
DEFAULT_RULES.put(SensitiveData.SensitiveType.PHONE, new MaskRule(3, 4)); // 手机号前3后4
DEFAULT_RULES.put(SensitiveData.SensitiveType.BANK_CARD, new MaskRule(4, 4)); // 银行卡前4后4
DEFAULT_RULES.put(SensitiveData.SensitiveType.NAME, new MaskRule(1, 0)); // 姓名保留首字
DEFAULT_RULES.put(SensitiveData.SensitiveType.EMAIL, new MaskRule(3, 0)); // 邮箱前3位
DEFAULT_RULES.put(SensitiveData.SensitiveType.ADDRESS, new MaskRule(6, 0)); // 地址前6位
}
public SensitiveDataSerializer() {
// 默认构造函数
}
public SensitiveDataSerializer(SensitiveData.SensitiveType type, int prefixLength, int suffixLength) {
this.type = type;
this.prefixLength = prefixLength;
this.suffixLength = suffixLength;
}
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value == null) {
gen.writeNull();
return;
}
String maskedValue = mask(value, type, prefixLength, suffixLength);
gen.writeString(maskedValue);
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
throws JsonMappingException {
if (property != null) {
SensitiveData annotation = property.getAnnotation(SensitiveData.class);
if (annotation != null) {
// 获取注解中的参数
SensitiveData.SensitiveType type = annotation.type();
int prefixLength = annotation.prefixLength();
int suffixLength = annotation.suffixLength();
return new SensitiveDataSerializer(type, prefixLength, suffixLength);
}
}
return prov.findValueSerializer(property.getType(), property);
}
private String mask(String value, SensitiveData.SensitiveType type, int customPrefixLength, int customSuffixLength) {
if (value == null || value.isEmpty()) {
return value;
}
// 获取默认规则
MaskRule rule = DEFAULT_RULES.get(type);
if (rule == null) {
return value; // 没有对应规则,返回原值
}
// 使用自定义长度或默认长度
int prefixLen = (customPrefixLength >= 0) ? customPrefixLength : rule.prefixLength;
int suffixLen = (customSuffixLength >= 0) ? customSuffixLength : rule.suffixLength;
switch (type) {
case ID_CARD:
case PHONE:
case BANK_CARD:
// 保留前缀和后缀,中间用*替代
return maskMiddle(value, prefixLen, suffixLen);
case NAME:
// 姓名: 仅显示姓,其他用*代替
return value.substring(0, prefixLen) + "*".repeat(Math.max(0, value.length() - prefixLen));
case EMAIL:
// 邮箱: 分开处理@前后的部分
int atIndex = value.indexOf('@');
if (atIndex > 0) {
int localPartLen = Math.min(prefixLen, atIndex);
return value.substring(0, localPartLen) +
"*".repeat(atIndex - localPartLen) +
value.substring(atIndex);
}
return maskMiddle(value, prefixLen, suffixLen);
case ADDRESS:
// 地址: 保留前缀,其余用*替代
int len = Math.min(prefixLen, value.length());
return value.substring(0, len) + "***";
default:
return value;
}
}
private String maskMiddle(String value, int prefixLen, int suffixLen) {
int len = value.length();
if (len <= prefixLen + suffixLen) {
return value;
}
String prefix = value.substring(0, prefixLen);
String suffix = value.substring(len - suffixLen);
return prefix + "*".repeat(len - prefixLen - suffixLen) + suffix;
}
// 脱敏规则
private static class MaskRule {
private final int prefixLength;
private final int suffixLength;
public MaskRule(int prefixLength, int suffixLength) {
this.prefixLength = prefixLength;
this.suffixLength = suffixLength;
}
}
}
从配置中心加载脱敏规则
实际项目中,脱敏规则通常需要从配置中心动态加载:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Map;
/**
* 从配置中心加载脱敏规则
*/
@Component
@RefreshScope // 支持配置热更新
public class SensitiveDataConfig {
@Autowired(required = false)
private ConfigCenterClient configCenter; // 配置中心客户端
@PostConstruct
public void loadMaskRules() {
if (configCenter != null) {
try {
// 从配置中心加载脱敏规则
Map<String, MaskRule> rules = configCenter.getMaskRules();
if (rules != null && !rules.isEmpty()) {
// 更新默认规则
for (Map.Entry<String, MaskRule> entry : rules.entrySet()) {
try {
SensitiveData.SensitiveType type = SensitiveData.SensitiveType.valueOf(entry.getKey());
DEFAULT_RULES.put(type, entry.getValue());
} catch (IllegalArgumentException e) {
// 忽略不支持的类型
}
}
}
} catch (Exception e) {
// 加载失败时使用默认规则
}
}
}
}
在实体类中使用脱敏注解
在实体类中结合加密和脱敏注解:
@Data
@TableName("user_info")
public class UserInfo {
private Long id;
@SensitiveData(type = SensitiveData.SensitiveType.NAME)
private String name;
@Encrypt(KeyManager.KeyType.ID_CARD)
@SensitiveData(type = SensitiveData.SensitiveType.ID_CARD)
private String idCard;
@Encrypt(KeyManager.KeyType.PHONE)
@SensitiveData(type = SensitiveData.SensitiveType.PHONE)
private String phoneNumber;
@Encrypt(KeyManager.KeyType.BANK_CARD)
@SensitiveData(type = SensitiveData.SensitiveType.BANK_CARD, prefixLength = 6, suffixLength = 4) // 自定义银行卡脱敏规则
private String bankCardNo;
// 用于模糊查询的辅助字段
private String phoneSearchKey;
}
密钥轮换处理
在生产环境中,密钥需要定期轮换以提高安全性。以下是一个密钥轮换的处理流程:
@Service
public class KeyRotationService {
@Autowired
private KeyManager keyManager;
@Autowired
private UserMapper userMapper;
/**
* 执行密钥轮换
* @param keyType 密钥类型
* @param newKey 新密钥
*/
@Transactional
public void rotateKey(KeyManager.KeyType keyType, String newKey) {
// 1. 备份旧密钥
String oldKey = keyManager.getCurrentKey(keyType);
// 2. 更新KeyManager中的密钥(当前密钥 -> 新密钥,旧密钥 -> 当前密钥)
keyManager.updateKeys(keyType, newKey, oldKey);
// 3. 安排后台任务迁移历史数据(异步执行)
asyncMigrateData(keyType, oldKey, newKey);
}
/**
* 异步迁移历史数据
*/
private void asyncMigrateData(KeyManager.KeyType keyType, String oldKey, String newKey) {
CompletableFuture.runAsync(() -> {
try {
int batchSize = 100;
int offset = 0;
boolean hasMore = true;
while (hasMore) {
// 分批查询需要迁移的数据
List<UserInfo> users = userMapper.findForMigration(keyType.name(), batchSize, offset);
if (users.isEmpty()) {
hasMore = false;
continue;
}
// 批量更新
for (UserInfo user : users) {
// 根据keyType决定要处理的字段
String encryptedValue = null;
String plainValue = null;
switch (keyType) {
case ID_CARD:
encryptedValue = user.getIdCard();
break;
case PHONE:
encryptedValue = user.getPhoneNumber();
break;
case BANK_CARD:
encryptedValue = user.getBankCardNo();
break;
}
if (encryptedValue != null) {
// 用旧密钥解密
plainValue = AESEncryptor.decrypt(encryptedValue, oldKey);
// 用新密钥加密
String newEncryptedValue = AESEncryptor.encrypt(plainValue, newKey);
// 更新数据库
userMapper.updateEncryptedField(user.getId(), keyType.name(), newEncryptedValue);
}
}
offset += batchSize;
}
} catch (Exception e) {
// 记录错误日志,可考虑重试机制
}
});
}
}
性能优化建议
加解密操作会带来一定的性能开销,可以采取以下措施优化:
1. 缓存机制
使用 Spring Cache 减少频繁加解密:
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, UserInfo> implements UserService {
@Cacheable(value = "userCache", key = "#id")
@Override
public UserInfo getById(Long id) {
return super.getById(id);
}
}
2. 批量操作与异步处理
对大量数据的加解密操作,可以使用异步处理:
@Service
public class BulkProcessService {
@Autowired
private UserService userService;
public CompletableFuture<Void> processBulkData(List<UserInfo> users) {
return CompletableFuture.runAsync(() -> {
int batchSize = 100;
int total = users.size();
for (int i = 0; i < total; i += batchSize) {
int end = Math.min(i + batchSize, total);
List<UserInfo> batch = users.subList(i, end);
// 批量插入(会触发TypeHandler加密)
userService.saveBatch(batch);
}
});
}
}
3. 只加密真正敏感的字段
不要过度加密,只对真正需要保护的敏感字段使用加密:
@Data
@TableName("user_info")
public class UserInfo {
private Long id;
private String name; // 普通姓名无需加密,仅脱敏
@Encrypt(KeyManager.KeyType.ID_CARD) // 身份证需加密
private String idCard;
@Encrypt(KeyManager.KeyType.PHONE) // 手机号需加密
private String phoneNumber;
private String address; // 普通地址无需加密
private String email; // 邮箱可根据需求决定是否加密
}
生产环境部署检查清单
在部署到生产环境前,请确认以下事项:
- [ ] 密钥是否使用 KMS 或密钥管理服务,而非直接存储在配置文件中
- [ ] 加解密操作是否添加了性能监控指标(如 Prometheus)
- [ ] 是否实现了完整的密钥轮换机制
- [ ] 敏感字段是否添加了适当的字段级权限控制
- [ ] 脱敏规则是否符合企业合规要求
- [ ] 是否处理了查询性能问题(如合理使用索引、缓存等)
- [ ] 是否有完整的异常处理机制
总结
本文详细讲解了在 MyBatis Plus 框架下实现敏感字段加解密和脱敏的完整方案。通过表格总结关键点:
阶段 | 技术方案 | 关键组件 | 优点 |
---|---|---|---|
加密存储 | AES-GCM 加密+注解驱动 TypeHandler | @Encrypt 注解+EncryptTypeHandler | 对业务代码无侵入,支持字段级密钥配置 |
多密钥管理 | 密钥管理器+配置注入 | KeyManager | 支持密钥轮换,兼容旧密钥解密 |
查询处理 | 参数预处理/辅助搜索字段 | 自定义 Service 方法 | 保证加密数据可查询,支持模糊搜索 |
接口脱敏 | Jackson 序列化+注解配置 | @SensitiveData 注解 | 与持久层解耦,支持自定义脱敏规则 |
性能优化 | 缓存+批量处理 | Spring Cache+异步处理 | 减少加解密开销,提高系统吞吐量 |
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。