MyBatis-Plus 优雅实现数据库单字段加密存储
本文将基于Mybatis-Plus讲述如何在数据的源头存储层保障其安全。我们都知道一些核心私密字段,比如说密码,手机号等在数据库层存储就不能明文存储,必须加密存储保证即使数据库泄露了也不会轻易曝光数据。
一、数据库字段加解密实现
1. 定义加密类型枚举
默认提供基于base64和AES加密算法,当然也可以自定义加密算法。
public enum Algorithm {
BASE64,
AES
}
2. 定义AES密钥和偏移量
/**
* EncryptProperties
*
* @author mtx
* @date: 2025/03/20 11:53
*/
@Data
@Component
@ConfigurationProperties(prefix = "field.encrypt")
public class EncryptProperties {
/**
* 加密算法
*/
private String algorithm = "base64";
/**
* aes算法需要秘钥key
*/
private String key = "8iUJAD805IHO2vog";
/**
* aes算法需要一个偏移量
* AES算法的偏移量长度必须为16字节(128位)
*/
private String iv = "cUTd1U+yxk8Dl6Cg";
}
3. 配置定义使用的加密类型
这里我们使用aes加密算法:
- application.yml
field:
encrypt:
algorithm: AES
4. 加密解密接口
/**
* IEncryptService
*
* @author mtx
* @date: 2025/03/20 13:48
*/
public interface IEncryptService {
/**
* 加密算法
*
* @param content 内容
* @return 加密后的内容
*/
String encrypt(String content);
/**
* 解密算法
*
* @param content 内容
* @return 解密后的内容
*/
String decrypt(String content);
}
5. 加密解密实现类
5.1 AES加密解密实现类
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.eitc.common.properties.EncryptProperties;
import com.eitc.modules.algorithm.service.IEncryptService;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Resource;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
@Slf4j
public class AESEncryptServiceImpl implements IEncryptService {
@Resource
private EncryptProperties encryptProperties;
/**
* 加密算法
*
* @param content 内容
* @return 加密后的内容
*/
@Override
public String encrypt(String content) {
try {
SecretKeySpec secretKey = new SecretKeySpec(encryptProperties.getKey().getBytes(StandardCharsets.UTF_8), Constants.AES);
byte[] enCodeFormat = secretKey.getEncoded();
SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, Constants.AES);
IvParameterSpec iv = new IvParameterSpec(encryptProperties.getIv().getBytes(StandardCharsets.UTF_8));
Cipher cipher = Cipher.getInstance(Constants.AES_CBC_CIPHER);
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, iv);
byte[] valueByte = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(valueByte);
} catch (Exception e) {
log.error("加密失败:", e);
// throw new IllegalArgumentException("加密失败");
return content;
}
}
/**
* 解密算法
*
* @param content 内容
* @return 解密后的内容
*/
@Override
public String decrypt(String content) {
try {
byte[] originalData = Base64.getDecoder().decode(content.getBytes(StandardCharsets.UTF_8));
SecretKeySpec secretKey = new SecretKeySpec(encryptProperties.getKey().getBytes(StandardCharsets.UTF_8), Constants.AES);
byte[] enCodeFormat = secretKey.getEncoded();
SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, Constants.AES);
IvParameterSpec iv = new IvParameterSpec(encryptProperties.getIv().getBytes(StandardCharsets.UTF_8));
Cipher cipher = Cipher.getInstance(Constants.AES_CBC_CIPHER);
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, iv);
byte[] valueByte = cipher.doFinal(originalData);
return new String(valueByte);
} catch (Exception e) {
log.error("解密失败:", e);
// throw new IllegalArgumentException("解密失败");
return content;
}
}
}
5.2 Base64加密解密实现类
import com.eitc.modules.algorithm.service.IEncryptService;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* Base64EncryptServiceImpl
*
* @author mtx
* @date: 2025/03/20 14:02
*/
public class Base64EncryptServiceImpl implements IEncryptService {
/**
* 加密算法
*
* @param content 内容
* @return 加密后的内容
*/
@Override
public String encrypt(String content) {
try {
return Base64.getEncoder().encodeToString(content.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
throw new RuntimeException("encrypt fail!", e);
}
}
/**
* 解密算法
*
* @param content 内容
* @return 解密后的内容
*/
@Override
public String decrypt(String content) {
try {
byte[] asBytes = Base64.getDecoder().decode(content);
return new String(asBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("decrypt fail!", e);
}
}
}
6. 实现数据库的字段保存加密与查询解密处理类
接下来就可以基于加密算法,扩展 MyBatis 的 BaseTypeHandler 对实体字段数据进行加密解密
import cn.hutool.core.util.StrUtil;
import com.eitc.modules.algorithm.service.IEncryptService;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* EncryptTypeHandler
*
* @author mtx
* @date: 2025/03/20 14:04
*/
@Component
public class MyEncryptTypeHandler<T> extends BaseTypeHandler<T> {
@Resource
private IEncryptService encryptService;
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, encryptService.encrypt((String)parameter));
}
@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
String columnValue = rs.getString(columnName);
//有一些可能是空字符
return StrUtil.isBlank(columnValue) ? (T)columnValue : (T)encryptService.decrypt(columnValue);
}
@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String columnValue = rs.getString(columnIndex);
return StrUtil.isBlank(columnValue) ? (T)columnValue : (T)encryptService.decrypt(columnValue);
}
@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String columnValue = cs.getString(columnIndex);
return StrUtil.isBlank(columnValue) ? (T)columnValue : (T)encryptService.decrypt(columnValue);
}
}
StrUtil.isBlank使用的是cn.hutool.core.util.StrUtil,需要hutool-all依赖,或者自己写一个判断是否为null或者空字符的方法
7. MybatisPlus配置类
把EncryptService实现类注入到容器中
import com.eitc.common.handler.MyEncryptTypeHandler;
import com.eitc.common.properties.EncryptProperties;
import com.eitc.modules.algorithm.service.IEncryptService;
import com.eitc.modules.algorithm.service.impl.AESEncryptServiceImpl;
import com.eitc.modules.algorithm.service.impl.Base64EncryptServiceImpl;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
/**
* mybatis-plus配置
*
* @author Mark eitc@163.com
* @since 1.0.0
*/
@Configuration
public class MybatisPlusConfig {
@Resource
private EncryptProperties encryptProperties;
@Bean
public MyEncryptTypeHandler encryptTypeHandler() {
return new MyEncryptTypeHandler();
}
@Bean
@ConditionalOnMissingBean(IEncryptService.class)
public IEncryptService encryptService() {
String algorithm = encryptProperties.getAlgorithm();
IEncryptService encryptService;
switch (algorithm) {
case "BASE64":
encryptService = new Base64EncryptServiceImpl();
break;
case "AES":
encryptService = new AESEncryptServiceImpl();
break;
default:
encryptService = null;
}
return encryptService;
}
}
二、数据库字段加密解密的使用
- 在实体类上加上 @TableName(value = “表名”, autoResultMap = true)
- 在需要加密的属性上加上 @TableField(value = “字段”, typeHandler = EncryptTypeHandler.class)
1. 创建实体类
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.eitc.common.entity.EitcBaseTenantEntity;
import com.eitc.common.handler.MyEncryptTypeHandler;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* AppUser
*
* @author mtx
* @date: 2024年6月11日 17:26:33
*/
@EqualsAndHashCode(callSuper = true)
@Data
@ApiModel("app用户")
@TableName(value = "app_user", autoResultMap = true)
public class AppUser extends EitcBaseTenantEntity {
@TableField(value = "phone_number", typeHandler = MyEncryptTypeHandler.class)
@ApiModelProperty(value = "手机号码", name = "phoneNumber")
private String phoneNumber;
@TableField(value = "password")
@ApiModelProperty(value = "密码", name = "password")
private String password;
@TableField(value = "identification_card", typeHandler = MyEncryptTypeHandler.class)
@ApiModelProperty(value = "身份证号", name = "identificationCard")
private String identificationCard;
@TableField(value = "avatar")
@ApiModelProperty(value = "用户头像", name = "avatar")
private String avatar;
}
2. 数据保存
@Test
public void appUserTest() {
List<AppUser> list = appUserService.list();
List<AppUser> updateList = new ArrayList<>();
list.forEach(li -> {
AppUser p = new AppUser();
p.setId(li.getId());
p.setIdentificationCard(li.getIdentificationCard());
updateList.add(p);
});
appUserService.updateBatchById(updateList);
}
引用:https://blog.csdn.net/xiaohuihui1400/article/details/135986224