一、教程前言

在实际项目开发中,敏感数据(如手机号、身份证号、银行卡号等)的安全存储至关重要。直接明文存储会带来数据泄露风险,因此需要对这类数据进行加密存储,查询时再解密展示。

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
    }
}

测试结果说明

  1. 插入测试后,数据库中mobile字段存储的是加密后的Base64字符串(如:hSCkYj3uNRNNotsSIMb21w==);在这里插入图片描述

  2. 查询测试时,控制台打印的user对象中mobile字段是明文(13800000126),说明解密成功;
    在这里插入图片描述

  3. 更新测试后,数据库中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)管理密钥,同时做好配置中心的权限控制;

  • 定期更换密钥,降低泄露风险。

六、进阶优化建议

  1. 多字段加解密

    若多个字段需要加解密,无需重复编写TypeHandler,只需在对应字段上添加@TableField(typeHandler = AesTypeHandler.class)注解,同时确保实体类开启autoResultMap即可。

  2. 加密算法优化

    本教程使用AES-ECB模式,该模式安全性较低。生产环境建议使用AES-CBC模式,需额外维护一个初始化向量(IV),进一步提升安全性。

  3. 批量操作支持

    MyBatis-Plus的批量插入、更新操作同样支持TypeHandler自动加解密,无需额外处理,autoResultMap配置对批量查询同样生效。

  4. 日志记录

    在TypeHandler中添加日志记录,记录加解密过程,便于排查问题(可使用Slf4j日志框架)。

七、总结

本教程基于MyBatis-Plus的TypeHandler实现了单列数据的自动加解密,核心是通过自定义TypeHandler拦截数据的插入、更新、查询过程,自动完成加解密操作,对业务层完全透明。

通过这种方式,既能保证敏感数据的安全存储,又无需在业务代码中编写大量加解密逻辑,简化了开发流程。实际项目中,需结合密钥管理、算法优化等措施,进一步提升数据安全性。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐