MyBatis-Plus 类型处理器实现单列数据加解密
一、教程前言
在实际项目开发中,敏感数据(如手机号、身份证号、银行卡号等)的安全存储至关重要。直接明文存储会带来数据泄露风险,因此需要对这类数据进行加密存储,查询时再解密展示。
MyBatis-Plus(简称MP)是MyBatis的增强工具,提供了丰富的功能简化开发。本教程将手把手教你如何基于MyBatis-Plus实现单列数据的自动加解密,无需手动在业务代码中处理加解密逻辑,实现对业务层透明的敏感数据保护。
核心实现思路
利用MyBatis-Plus的字段自动填充和**类型处理器(TypeHandler)**结合,实现:
-
数据插入/更新时,自动对目标字段进行加密后存储到数据库;
-
数据查询时,自动对目标字段进行解密后返回给业务层。
技术栈准备
-
开发工具:IDEA
-
JDK:17+
-
Spring Boot:3.1.x
-
MyBatis-Plus:3.5.x
-
数据库:MySQL 8.0
-
加密算法:AES(对称加密,适合敏感数据加解密,密钥管理简单)
二、环境搭建
2.1 引入依赖
在Spring Boot项目的pom.xml文件中引入以下依赖(已包含MyBatis-Plus、MySQL驱动、AES加密工具类依赖):
<!-- 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>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<!-- Hutool工具类库依赖 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.22</version>
</dependency>
<!-- Spring Boot测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2.2 配置数据库连接
在application.yml(或application.properties)中配置数据库连接信息和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 # mapper文件路径(若用注解开发可省略)
type-aliases-package: com.xkl.domain # 实体类别名包路径
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名映射(数据库字段下划线命名转驼峰命名)
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志(方便调试)
2.3 创建数据库表
创建一个用户表(user),其中手机号(mobile)作为敏感字段,需要进行加解密存储。SQL语句如下:
CREATE DATABASE IF NOT EXISTS my_demo DEFAULT CHARSET utf8mb4;
USE mp_encryption;
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 '更新时间',
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
注意:mobile字段长度建议设置大一些(如100),因为加密后的字符串长度会比原明文长。
三、核心功能实现
3.1 编写AES加密工具类
AES是对称加密算法,需要一个密钥(key),加密和解密使用同一个密钥。编写工具类实现AES的加密和解密方法,注意密钥长度必须是16位、24位或32位(对应AES-128、AES-192、AES-256)。
package com.xkl.utils;
import cn.hutool.core.codec.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
/**
* 加解密工具类(基于AES,使用Hutool简化加密代码,也可手动实现)
* 若不使用Hutool,需手动实现AES的加密/解密逻辑
*/
public class AesEncryptUtil {
// 密钥(必须是16位、24位或32位,这里用16位示例,实际项目中需妥善保管,建议配置在配置文件中)
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);
// 加密并转为Base64字符串(避免乱码)
byte[] encrypted = cipher.doFinal(content.getBytes());
return Base64.encode(encrypted);
} 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);
// 解码Base64并解密
byte[] decoded = Base64.decode(encryptedContent);
byte[] decrypted = cipher.doFinal(decoded);
return new String(decrypted);
} catch (Exception e) {
throw new RuntimeException("AES解密失败", e);
}
}
}
注意:实际项目中,密钥(SECRET_KEY)不能硬编码在代码中,建议配置在application.yml中,通过@Value注解注入。同时要做好密钥的安全管理,避免泄露。
3.2 编写自定义TypeHandler
MyBatis的TypeHandler用于处理Java类型和数据库类型之间的转换。我们自定义一个TypeHandler,在数据写入数据库时调用加密方法,读取数据库时调用解密方法。
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/CHAR的加解密转换
*/
@MappedJdbcTypes(JdbcType.VARCHAR)
public class CryptoTypeHandler extends BaseTypeHandler<String> {
/**
* 插入/更新时,设置参数(加密)
*/
@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);
}
}
/**
* 从结果集获取数据(解密)
*/
@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)) {
String encrypt = AesEncryptUtil.decrypt(value);
return encrypt;
} else {
return value;
}
}
}
3.3 编写实体类并指定TypeHandler
创建User实体类,在需要加解密的字段(mobile)上通过@TableField注解指定自定义的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
// 重要:autoResultMap 开启自动结果映射,适配TypeHandler
@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 +
'}';
}
}
3.4 编写MyBatis-Plus填充器(可选)
为了自动填充createTime和updateTime字段,编写一个填充器类(若不需要自动填充时间,可省略)。
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());
}
}
3.5 编写Mapper接口
创建UserMapper接口,继承MyBatis-Plus的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 add() {
User user = new User();
user.setUsername("王五");
user.setPassword("123456");
user.setMobile("13800000126"); // 明文手机号
int rows = userMapper.insert(user);
System.out.println("插入行数:" + rows);
// 查看数据库中mobile字段是否为加密后的字符串
}
/**
* 测试查询数据(自动解密)
*/
@Test
void selectAll() {
List<User> list = userMapper.selectList(null);
for (User user : list) {
System.out.println("查询结果:" + 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字段是否更新为新的加密字符串,查询后是否为明文13900139000
}
}
测试结果说明
-
插入测试后,数据库中mobile字段存储的是加密后的Base64字符串(如:
hSCkYj3uNRNNotsSIMb21w==);
-
查询测试时,控制台打印的user对象中mobile字段是明文(13800000126),说明解密成功;

-
更新测试后,数据库中mobile字段更新为新的加密字符串,查询后仍为明文(13900139000),说明更新时加密生效。

五、常见问题及解决方案
5.1 解密失败报错“Invalid base64 character”
原因:
-
加密后的字符串包含特殊字符,存储时被转义或截断;
-
数据库字段长度不足,导致加密后的字符串被截断。
解决方案:
-
确保数据库敏感字段类型为VARCHAR,长度设置足够大(如100、255);
-
加密后使用Base64编码存储,避免特殊字符问题(本教程已实现)。
5.2 TypeHandler不生效,数据未加密
原因:
-
未配置TypeHandler扫描路径,MyBatis-Plus无法识别自定义TypeHandler;
-
实体类字段未正确添加@TableField(typeHandler = AesTypeHandler.class)注解;
-
TypeHandler类未添加@MappedTypes和@MappedJdbcTypes注解。
解决方案:
-
检查application.yml中是否配置了type-handlers-package;
-
核对实体类注解是否正确;
-
确保TypeHandler注解配置正确,映射的Java类型和数据库类型匹配。
5.3 密钥泄露风险
原因:
密钥硬编码在代码中,若代码泄露,攻击者可获取密钥解密数据。
解决方案:
-
将密钥配置在application.yml中,通过@Value注入工具类;
-
生产环境中,使用配置中心(如Nacos、Apollo)管理密钥,同时做好配置中心的权限控制;
-
定期更换密钥,降低泄露风险。
六、进阶优化建议
-
多字段加解密
若多个字段需要加解密,无需重复编写TypeHandler,只需在对应字段上添加@TableField(typeHandler = AesTypeHandler.class)注解,同时确保实体类开启autoResultMap即可。
-
加密算法优化
本教程使用AES-ECB模式,该模式安全性较低。生产环境建议使用AES-CBC模式,需额外维护一个初始化向量(IV),进一步提升安全性。
-
批量操作支持
MyBatis-Plus的批量插入、更新操作同样支持TypeHandler自动加解密,无需额外处理,autoResultMap配置对批量查询同样生效。
-
日志记录
在TypeHandler中添加日志记录,记录加解密过程,便于排查问题(可使用Slf4j日志框架)。
七、总结
本教程基于MyBatis-Plus的TypeHandler实现了单列数据的自动加解密,核心是通过自定义TypeHandler拦截数据的插入、更新、查询过程,自动完成加解密操作,对业务层完全透明。
通过这种方式,既能保证敏感数据的安全存储,又无需在业务代码中编写大量加解密逻辑,简化了开发流程。实际项目中,需结合密钥管理、算法优化等措施,进一步提升数据安全性。
更多推荐




所有评论(0)