找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2806

积分

0

好友

402

主题
发表于 10 小时前 | 查看: 0| 回复: 0

在实际项目开发中,你是否遇到过这样的困扰:身份证号、手机号、银行卡号这类敏感数据,直接明文存储在数据库里总觉得不踏实,担心数据泄露。但如果在业务代码里到处写加解密逻辑,代码又会变得臃肿不堪,维护起来是个噩梦。

别担心,今天我们就来聊聊如何利用 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_timeupdate_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日志和数据库数据,你可以清晰地看到整个过程:

  1. 插入测试:执行后,数据库 mobile 字段存储的是类似 hSCkYj3uNRNNotsSIMb21w== 的密文,而程序中的 user 对象 mobile 是明文。
    数据库插入加密后的手机号字段
  2. 查询测试:控制台打印的 User 对象中,mobile 字段显示为明文 13800000126,说明解密成功。
    查询结果中手机号字段被正确解密
  3. 更新测试:更新后,数据库中的密文被替换,再次查询时,获取到的是新的明文 13900139000
    更新操作同样触发加密

常见问题与解决方案

  1. 解密失败,报“Invalid base64 character”

    • 原因:加密后字符串被截断或存储时出错。可能是数据库字段长度不够。
    • 解决:确保数据库字段(如 varchar)长度足够(例如255)。使用Base64编码本身已能避免大部分特殊字符问题。
  2. TypeHandler不生效,数据未加密

    • 原因1:实体类 @TableName 注解未设置 autoResultMap = true
    • 原因2@TableField 注解的 typeHandler 属性未指定或指定错误。
    • 解决:仔细检查实体类注解配置,确保 autoResultMap = truetypeHandler 指向正确的类。
  3. 密钥安全风险

    • 风险:密钥硬编码在代码中,泄露即导致数据可被解密。
    • 解决:将密钥移至 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的其他高级特性或数据安全实践有更多兴趣,欢迎在技术社区交流探讨。




上一篇:大模型微调(Fine-tuning)实战避坑:从数据、LoRA到超参数的最佳实践与代码示例
下一篇:SpringBoot系统中绕过黑名单过滤的SQL注入漏洞分析与实战
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-28 16:57 , Processed in 0.329295 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表