文章目录
前言
本文将基于Mybatis-Plus讲述如何在数据的源头存储层保障其安全。我们都知道一些核心私密字段,比如说密码,手机号等在数据库层存储就不能明文存储,必须加密存储保证即使数据库泄露了也不会轻易曝光数据。
本文实现效果参考 plasticene-boot-starter-parent,更多信息在下面链接。
Github地址:https://github.com/plasticene/plasticene-boot-starter-parent
Gitee地址:https://gitee.com/plasticene3/plasticene-boot-starter-parent
当然也可以参考 mybatis-mate 为 mp 企业级模块,旨在更敏捷优雅处理数据。
Gitee地址:https://gitee.com/baomidou/mybatis-mate-examples
一、数据库字段加解密实现
1. 定义加密类型枚举
默认提供基于base64和AES加密算法,当然也可以自定义加密算法。
public enum Algorithm {
BASE64,
AES
}
2. 定义AES密钥和偏移量
@Data
@ConfigurationProperties(prefix = "ptc.encrypt")
public class EncryptProperties {
/**
* 加密算法 {@link Algorithm}
*/
private Algorithm algorithm = Algorithm.BASE64;
/**
* aes算法需要秘钥key
*/
private String key = "8iUJAD805IHO2vog";
/**
* aes算法需要一个偏移量
* AES算法的偏移量长度必须为16字节(128位)
*/
private String iv = "cUTd1U+yxk8Dl6Cg";
}
AES的密钥和偏移量的生成可访问:AES 密钥在线生成器
若使用RSA,密钥对的生成可访问:在线生成非对称加密公钥私钥对
3. 配置定义使用的加密类型
这里我们使用aes加密算法:
- application.yml
ptc:
encrypt:
algorithm: aes
4. 加密解密接口
public interface EncryptService {
/**
* 加密算法
* @param content
* @return
*/
String encrypt(String content);
/**
* 解密算法
* @param content
* @return
*/
String decrypt(String content);
}
5. 加密解密异常类
- BizException.java
/**
* 业务异常类
*/
@Data
public class BizException extends RuntimeException {
private Integer code;
public BizException() {
super();
}
public BizException(String message) {
super(message);
}
public BizException(Integer code, String message) {
super(message);
this.code = code;
}
}
6. 加密解密实现类
6.1 AES加密解密实现类
import com.baomidou.mybatisplus.core.toolkit.Constants;
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 AESEncryptService implements EncryptService {
@Resource
private EncryptProperties encryptProperties;
@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 BizException("加密失败");
}
}
@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 BizException("解密失败");
}
}
}
6.2 Base64加密解密实现类
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class Base64EncryptService implements EncryptService {
@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);
}
}
@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);
}
}
}
7. 实现数据库的字段保存加密与查询解密处理类
接下来就可以基于加密算法,扩展 MyBatis 的 BaseTypeHandler 对实体字段数据进行加密解密
import cn.hutool.core.util.StrUtil;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import javax.annotation.Resource;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class EncryptTypeHandler<T> extends BaseTypeHandler<T> {
@Resource
private EncryptService 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或者空字符的方法
8. MybatisPlus配置类
把EncryptService实现类注入到容器中
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
@Configuration
@MapperScan("com.chh.mapper")
@EnableConfigurationProperties({EncryptProperties.class})
public class MybatisPlusConfig {
@Resource
private EncryptProperties encryptProperties;
// 分页插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL));
return interceptor;
}
@Bean
public EncryptTypeHandler encryptTypeHandler() {
return new EncryptTypeHandler();
}
@Bean
@ConditionalOnMissingBean(EncryptService.class)
public EncryptService encryptService() {
Algorithm algorithm = encryptProperties.getAlgorithm();
EncryptService encryptService;
switch (algorithm) {
case BASE64:
encryptService = new Base64EncryptService();
break;
case AES:
encryptService = new AESEncryptService();
break;
default:
encryptService = null;
}
return encryptService;
}
}
二、数据库字段加密解密的使用
1. 创建实体类
- 在实体类上加上
@TableName(value = "表名", autoResultMap = true)
- 在需要加密的属性上加上
@TableField(value = "字段", typeHandler = EncryptTypeHandler.class)
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName(value = "app_account_login", autoResultMap = true)
public class AppAccountLogin implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 名称
*/
@TableField("name")
private String name;
/**
* 登录账号
*/
@TableField("login_account")
private String loginAccount;
/**
* 登录密码
*/
@TableField(value = "login_password", typeHandler = EncryptTypeHandler.class)
private String loginPassword;
/**
* 激活状态
*/
@TableField("enabled")
private Boolean enabled;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(fill = FieldFill.INSERT)
private Date createTime;
}
2. 数据保存示例
AppAccountLogin appAccountLogin = new AppAccountLogin();
appAccountLogin.setName("test");
appAccountLogin.setLoginAccount("123456789");
appAccountLogin.setLoginPassword("abc123456");
appAccountLoginService.save(appAccountLogin);
保存结果:
3. 数据查询示例
System.out.println(appAccountLoginService.getById(4));
查询结果:
AppAccountLogin(id=4, name=test, loginAccount=123456789, loginPassword=abc123456, enabled=true, createTime=Fri Feb 02 10:02:41 CST 2024)