在实际项目开发中,你是否遇到过这样的困扰:身份证号、手机号、银行卡号这类敏感数据,直接明文存储在数据库里总觉得不踏实,担心数据泄露。但如果在业务代码里到处写加解密逻辑,代码又会变得臃肿不堪,维护起来是个噩梦。
别担心,今天我们就来聊聊如何利用 MyBatis-Plus(以下简称MP)的类型处理器(TypeHandler),以一种优雅、对业务层透明的方式,实现单列敏感数据的自动加解密。学完本篇,你就能轻松给敏感字段穿上“防护衣”。
核心实现思路
思路其实很清晰:我们利用MyBatis-Plus的类型处理器(TypeHandler) 机制,在数据“进”“出”数据库的关口进行拦截和处理。
- 数据入库(插入/更新)时:自动对目标字段进行加密,将密文存入数据库。
- 数据出库(查询)时:自动对数据库中取出的密文进行解密,将明文返回给业务层。
整个过程对上层Service和Controller完全透明,你操作的实体对象字段始终是明文,仿佛加解密从未发生。
技术栈与准备
开始动手前,请确保你的环境包含以下技术栈:
- 开发工具:IDEA
- JDK:17+
- Spring Boot:3.1.x
- MyBatis-Plus:3.5.x
- 数据库:MySQL 8.0
- 加密算法:AES(对称加密,适合此场景,密钥管理相对简单)
第一步:项目搭建与配置
1.1 引入Maven依赖
在你的 pom.xml 文件中引入必要的依赖。
<!-- Spring Boot父依赖 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.7</version>
<relativePath/>
</parent>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.14</version>
</dependency>
<!-- Druid连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>1.2.27</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Hutool工具包(内含加解密等工具) -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.22</version>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
1.2 配置数据库连接
在 application.yml 中配置数据库和MyBatis-Plus。
spring:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/my_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
username: root
password: 123456
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
mybatis-plus:
mapper-locations: classpath:/mapper/**.xml
type-aliases-package: com.xkl.domain
configuration:
map-underscore-to-camel-case: true # 下划线转驼峰
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL,方便调试
1.3 创建数据库表
我们创建一个用户表,其中 mobile(手机号)字段将作为我们的加解密目标字段。注意,因为加密后的字符串会变长,所以字段长度要给足。
CREATE DATABASE IF NOT EXISTS my_demo DEFAULT CHARSET utf8mb4;
USE my_demo;
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户名',
`password` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '密码',
`mobile` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '手机号',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
第二步:核心功能实现
2.1 编写AES加解密工具类
首先,我们需要一个可靠的加解密工具。这里使用AES算法,并利用Hutool简化代码。切记,密钥在生产环境中必须从配置中心或环境变量读取,绝不能硬编码!
package com.xkl.utils;
import cn.hutool.core.codec.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
/**
* AES加解密工具类
*/
public class AesEncryptUtil {
// 密钥(16/24/32位),此处为示例,实际应配置在application.yml中
private static final String SECRET_KEY = "my_secret_key_16";
// 加密算法/模式/填充方式
private static final String ALGORITHM = "AES/ECB/PKCS5Padding";
/**
* AES加密
* @param content 明文
* @return Base64编码的密文
*/
public static String encrypt(String content) {
try {
SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(), "AES");
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encrypted = cipher.doFinal(content.getBytes());
return Base64.encode(encrypted); // Base64编码避免乱码
} catch (Exception e) {
throw new RuntimeException("AES加密失败", e);
}
}
/**
* AES解密
* @param encryptedContent Base64编码的密文
* @return 明文
*/
public static String decrypt(String encryptedContent) {
try {
SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(), "AES");
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] decoded = Base64.decode(encryptedContent);
byte[] decrypted = cipher.doFinal(decoded);
return new String(decrypted);
} catch (Exception e) {
throw new RuntimeException("AES解密失败", e);
}
}
}
2.2 编写自定义TypeHandler
这是最核心的一步。我们继承MyBatis的 BaseTypeHandler,重写其方法,在 setParameter(设置SQL参数)时加密,在 getResult(获取结果)时解密。
package com.xkl.handler;
import cn.hutool.core.util.StrUtil;
import com.xkl.utils.AesEncryptUtil;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 自定义字符串加解密TypeHandler
* 处理 Java String <-> 数据库 VARCHAR 的转换
*/
@MappedJdbcTypes(JdbcType.VARCHAR) // 声明处理的JDBC类型
public class CryptoTypeHandler extends BaseTypeHandler<String> {
// 插入/更新时调用:将明文加密后设置到PreparedStatement
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
if (StrUtil.isNotBlank(parameter)) {
String encrypt = AesEncryptUtil.encrypt(parameter);
ps.setString(i, encrypt);
} else {
ps.setString(i, parameter);
}
}
// 查询时调用:从ResultSet取出密文,解密后返回
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
return decrypt(rs.getString(columnName));
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return decrypt(rs.getString(columnIndex));
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return decrypt(cs.getString(columnIndex));
}
// 统一的解密方法
private String decrypt(String value) {
if (StrUtil.isNotBlank(value)) {
return AesEncryptUtil.decrypt(value);
} else {
return value;
}
}
}
2.3 编写实体类并应用TypeHandler
在实体类的目标字段上,使用 @TableField 注解指定我们刚写的 CryptoTypeHandler。关键点:@TableName 注解必须设置 autoResultMap = true,否则TypeHandler在查询映射时可能不生效。
package com.xkl.domain;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.xkl.handler.CryptoTypeHandler;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 用户实体类
*/
@Data
@TableName(value = "t_user", autoResultMap = true) // 关键配置:开启自动结果映射
public class User {
private Long id;
private String username;
private String password;
// 在此字段上应用自定义类型处理器
@TableField(value = "mobile", typeHandler = CryptoTypeHandler.class)
private String mobile;
@TableField(value = "create_time", fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", mobile='" + mobile + '\'' +
", createTime=" + createTime +
", updateTime=" + updateTime +
'}';
}
}
2.4 (可选)编写自动填充处理器
用于自动填充 create_time 和 update_time 字段,非加解密核心,但很实用。
package com.xkl.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}
2.5 编写Mapper接口
非常简单,直接继承MP的 BaseMapper 即可获得全套CRUD方法。
package com.xkl.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xkl.domain.User;
import org.springframework.stereotype.Repository;
@Repository
public interface UserMapper extends BaseMapper<User> {
}
第三步:测试验证
写个测试类,看看我们的加解密“魔法”是否生效。
package com.xkl;
import com.xkl.domain.User;
import com.xkl.mapper.UserMapper;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
class MpEncryptionApplicationTests {
@Resource
UserMapper userMapper;
/**
* 测试插入:明文 -> 数据库存密文
*/
@Test
void testInsert() {
User user = new User();
user.setUsername("王五");
user.setPassword("123456");
user.setMobile("13800000126"); // 这里是明文
int rows = userMapper.insert(user);
System.out.println("插入行数:" + rows);
// 此时查看数据库,mobile字段应为加密后的Base64字符串
}
/**
* 测试查询:数据库密文 -> 返回明文对象
*/
@Test
void testSelectAll() {
List<User> list = userMapper.selectList(null);
for (User user : list) {
System.out.println("查询结果:" + user);
// 控制台打印的user对象中,mobile字段应是明文 "13800000126"
}
}
/**
* 测试更新:新明文 -> 数据库更新为新的密文
*/
@Test
void testUpdate() {
User user = new User();
user.setId(1L); // 假设存在的ID
user.setMobile("13900139000"); // 新的明文手机号
int rows = userMapper.updateById(user);
System.out.println("更新行数:" + rows);
// 数据库对应记录的mobile字段会更新为新的加密字符串
}
}
测试结果分析
运行上述测试,结合控制台SQL日志和数据库数据,你可以清晰地看到整个过程:
- 插入测试:执行后,数据库
mobile 字段存储的是类似 hSCkYj3uNRNNotsSIMb21w== 的密文,而程序中的 user 对象 mobile 是明文。

- 查询测试:控制台打印的
User 对象中,mobile 字段显示为明文 13800000126,说明解密成功。

- 更新测试:更新后,数据库中的密文被替换,再次查询时,获取到的是新的明文
13900139000。

常见问题与解决方案
-
解密失败,报“Invalid base64 character”
- 原因:加密后字符串被截断或存储时出错。可能是数据库字段长度不够。
- 解决:确保数据库字段(如
varchar)长度足够(例如255)。使用Base64编码本身已能避免大部分特殊字符问题。
-
TypeHandler不生效,数据未加密
- 原因1:实体类
@TableName 注解未设置 autoResultMap = true。
- 原因2:
@TableField 注解的 typeHandler 属性未指定或指定错误。
- 解决:仔细检查实体类注解配置,确保
autoResultMap = true 且 typeHandler 指向正确的类。
-
密钥安全风险
- 风险:密钥硬编码在代码中,泄露即导致数据可被解密。
- 解决:将密钥移至
application.yml 配置文件中,使用 @Value 注入。生产环境应使用配置中心(如Nacos),并实施严格的密钥轮换和权限管理策略。
进阶优化建议
- 多字段支持:只需在其他敏感字段(如
id_card)上添加同样的 @TableField(typeHandler = CryptoTypeHandler.class) 注解即可。
- 算法升级:示例中使用的是AES/ECB模式,对于更高安全要求,建议使用AES/CBC或GCM模式,需要额外管理初始化向量(IV)。
- 批量操作:MyBatis-Plus的批量插入(
insertBatch)、批量查询等操作,此方案同样生效,无需额外处理。
- 监控与日志:可以在
CryptoTypeHandler 中加入简单的日志记录,方便跟踪加解密过程,便于排查问题。
总结
通过本篇教程,我们实现了一种基于 MyBatis-Plus 类型处理器的、优雅的敏感数据加解密方案。它将加解密逻辑从业务代码中彻底解耦,下沉到数据持久层,通过配置即可实现,大大提升了代码的可维护性和安全性。
这种 架构设计 思路不仅适用于加解密,还可以扩展用于数据脱敏、格式转换等任何需要在Java对象与数据库之间进行自定义转换的场景。希望这个实践对你在处理数据安全时有所启发。
如果你对MyBatis-Plus的其他高级特性或数据安全实践有更多兴趣,欢迎在技术社区交流探讨。