⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 夏壹分享 「夏壹分享」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

在数据安全合规要求日益严苛的当下,如何妥善保护用户敏感信息,是每个后端开发者绕不开的核心问题。

比如用户的手机号、身份证号、银行卡号等核心数据,若以明文形式存储在数据库中,一旦发生数据泄露,不仅会侵犯用户隐私,还可能给企业带来严重的法律风险和声誉损失。

传统的手动加解密方式,不仅让代码变得臃肿混乱,还极易出现遗漏场景;而基于注解+MyBatis拦截器的自动化加解密方案,能让敏感字段的加密存储、解密使用全程自动化,彻底解放业务代码。

传统加解密方式的痛点

代码冗余严重

手动在业务代码中嵌入加解密逻辑,会让代码充斥大量重复操作,可读性和可维护性大幅下降:

// 传统查询解密示例:每个字段都要手动解密,代码重复且繁琐
User user = userMapper.findById(id);
// 手动解密手机号
user.setPhone(decrypt(user.getPhone()));
// 手动解密邮箱
user.setEmail(decrypt(user.getEmail()));
// 手动解密身份证号
user.setIdCard(decrypt(user.getIdCard()));
return user;

// 传统新增加密示例:每个敏感字段都要手动加密
User newUser = new User();
newUser.setPhone(encrypt(phone));
newUser.setEmail(encrypt(email));
userMapper.insert(newUser);

扩展与维护成本高

  • 新增加密字段时,需要修改所有涉及该字段的增删改查代码;
  • 容易遗漏部分查询场景的解密操作,导致前端展示密文;
  • 加解密逻辑分散在各个业务方法中,出问题后难以定位排查;
  • 新人接手项目时,需要花费大量时间梳理整套加解密逻辑。

理想的加解密方案

我们期望的方案应满足以下核心诉求:

  • 极简接入: 仅需在敏感字段上添加注解,无需修改业务代码;
  • 全程自动化: 加解密过程由框架自动完成,业务层无感知;
  • 低侵入性: 不影响原有业务逻辑和MyBatis使用习惯;
  • 高性能: 加解密逻辑轻量化,不显著影响系统响应速度。

解决方案设计思路

本次方案的核心是「注解标记+拦截器自动处理」,在业务层与数据库之间搭建一层透明的加解密桥梁,整体流程如下:

业务代码 → MyBatis Mapper → 自定义拦截器(自动加解密) → 数据库

具体设计逻辑:

  1. 注解标记: 自定义@Encrypted注解,标记需要加解密的实体类字段;
  2. 拦截器处理: 通过MyBatis拦截器拦截SQL执行过程,在入库前加密、出库后解密;
  3. 透明化操作: 业务代码完全无需关注加解密逻辑,像使用明文一样操作敏感数据。

核心实现步骤

定义加密标记注解

首先自定义一个注解,用于标记需要自动加解密的字段,支持扩展模糊查询配置:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 字段加密注解:标记需要自动加解密的实体类字段
* @author 架构师笔记
*/
@Target(ElementType.FIELD) // 注解仅作用于字段
@Retention(RetentionPolicy.RUNTIME) // 运行时保留,便于反射获取
public @interface Encrypted {
/**
* 是否支持模糊查询(默认不支持)
* 支持模糊查询时,加密逻辑需做特殊处理(本文暂不展开)
*/
boolean supportFuzzyQuery() default false;
}

注解使用示例

在实体类的敏感字段上添加@Encrypted注解,标记该字段需要自动加解密:

/**
* 用户实体类
* 包含敏感字段:手机号、邮箱、身份证号
*/
public class User {
// 主键ID
private Long id;
// 用户名(非敏感,无需加密)
private String username;

// 手机号(敏感字段,自动加解密)
@Encrypted
private String phone;

// 邮箱(敏感字段,自动加解密)
@Encrypted
private String email;

// 身份证号(敏感字段,自动加解密)
@Encrypted
private String idCard;

// 省略getter/setter
}

加解密工具类实现

采用AES-GCM算法实现加解密(GCM模式自带完整性校验,安全性更高),工具类封装核心加解密逻辑:

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Arrays;

/**
* AES-GCM加解密工具类
* 核心逻辑:加密时生成随机IV,IV+密文拼接后Base64编码;解密时拆分IV和密文后解密
*/
public class CryptoUtil {
// 加密算法:AES/GCM模式,无需填充
private static final String ALGORITHM = "AES/GCM/NoPadding";
// IV(初始化向量)长度:12字节(GCM模式推荐长度)
private static final int IV_LENGTH = 12;
// AES密钥(实际项目中需从配置中心/环境变量读取,切勿硬编码)
private static final SecretKey SECRET_KEY = new SecretKeySpec(
"1234567890123456".getBytes(StandardCharsets.UTF_8), "AES");

/**
* 加密方法:明文→IV+密文→Base64编码
* @param plaintext 待加密的明文字符串
* @return 加密后的Base64字符串(格式:IV+密文的Base64拼接)
*/
public static String encrypt(String plaintext) {
// 空值直接返回,避免空指针
if (plaintext == null || plaintext.isEmpty()) {
return plaintext;
}

try {
// 1. 生成随机IV(初始化向量)
byte[] iv = new byte[IV_LENGTH];
new SecureRandom().nextBytes(iv);

// 2. 初始化加密器
Cipher cipher = Cipher.getInstance(ALGORITHM);
// GCM参数:128位认证标签长度 + 随机IV
GCMParameterSpec gcmParams = new GCMParameterSpec(128, iv);
cipher.init(Cipher.ENCRYPT_MODE, SECRET_KEY, gcmParams);

// 3. 执行加密
byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));

// 4. 拼接IV和密文(IV在前,密文在后)
byte[] encryptedData = new byte[iv.length + ciphertext.length];
System.arraycopy(iv, 0, encryptedData, 0, iv.length);
System.arraycopy(ciphertext, 0, encryptedData, iv.length, ciphertext.length);

// 5. Base64编码返回
return Base64.getEncoder().encodeToString(encryptedData);
} catch (Exception e) {
throw new RuntimeException("AES加密失败", e);
}
}

/**
* 解密方法:Base64字符串→拆分IV和密文→明文
* @param encryptedText 加密后的Base64字符串
* @return 解密后的明文字符串
*/
public static String decrypt(String encryptedText) {
// 空值直接返回
if (encryptedText == null || encryptedText.isEmpty()) {
return encryptedText;
}

try {
// 1. Base64解码
byte[] encryptedData = Base64.getDecoder().decode(encryptedText);

// 2. 拆分IV和密文(前12字节是IV,剩余是密文)
byte[] iv = Arrays.copyOfRange(encryptedData, 0, IV_LENGTH);
byte[] ciphertext = Arrays.copyOfRange(encryptedData, IV_LENGTH, encryptedData.length);

// 3. 初始化解密器
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec gcmParams = new GCMParameterSpec(128, iv);
cipher.init(Cipher.DECRYPT_MODE, SECRET_KEY, gcmParams);

// 4. 执行解密并返回明文
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("AES解密失败", e);
}
}

/**
* 判断字符串是否为加密后的密文
* (简单判断:加密后的字符串是Base64编码,且长度大于IV_LENGTH+8)
* @param value 待判断的字符串
* @return true=密文,false=明文/空值
*/
public static boolean isEncrypted(String value) {
if (value == null) {
return false;
}
// 加密后的字符串长度至少大于IV_LENGTH+8(密文最小长度)
return value.length() > (IV_LENGTH + 8) && isBase64(value);
}

/**
* 辅助方法:判断字符串是否为Base64编码
*/
private static boolean isBase64(String value) {
try {
Base64.getDecoder().decode(value);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
}

MyBatis拦截器实现(核心)

通过MyBatis拦截器拦截SQL执行过程,自动处理加解密逻辑:

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Map;
import java.util.Properties;

/**
* MyBatis加解密拦截器
* 拦截Executor的update/query方法:
* 1. update方法(新增/修改):加密实体类中标记@Encrypted的字段
* 2. query方法(查询):解密返回结果中标记@Encrypted的字段
*/
@Intercepts({
// 拦截update方法(insert/update/delete)
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
// 拦截query方法(select)
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, org.apache.ibatis.session.RowBounds.class, org.apache.ibatis.session.ResultHandler.class})
})
public class EncryptionInterceptor implements Interceptor {
private static final Logger log = LoggerFactory.getLogger(EncryptionInterceptor.class);

/**
* 核心拦截方法:处理加解密逻辑
* @param invocation 拦截器调用上下文
* @return 执行结果(加密/解密后的数据)
* @throws Throwable 执行异常
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取被拦截的方法名(update/query)
String methodName = invocation.getMethod().getName();

// 1. 处理新增/修改:加密敏感字段
if ("update".equals(methodName)) {
// 获取方法参数(实体类/参数Map)
Object parameter = getParameter(invocation);
// 判断是否需要加密
if (shouldEncrypt(parameter)) {
encryptFields(parameter);
}
}

// 执行原方法(SQL操作)
Object result = invocation.proceed();

// 2. 处理查询:解密敏感字段
if ("query".equals(methodName)) {
decryptResult(result);
}

return result;
}

/**
* 获取MyBatis方法的实际参数
*/
private Object getParameter(Invocation invocation) {
Object[] args = invocation.getArgs();
if (args.length >= 2) {
return args[1];
}
return null;
}

/**
* 判断参数是否需要加密
* 排除基础类型、Map、集合(本文仅处理实体类)
*/
private boolean shouldEncrypt(Object parameter) {
if (parameter == null) {
return false;
}
Class<?> clazz = parameter.getClass();
// 基础类型/包装类/字符串/Map/集合 不加密
return !isBasicType(clazz) && !(parameter instanceof Map) && !(parameter instanceof Collection);
}

/**
* 判断是否为基础数据类型
*/
private boolean isBasicType(Class<?> clazz) {
return clazz.isPrimitive() || clazz == String.class || clazz == Integer.class ||
clazz == Long.class || clazz == Double.class || clazz == Boolean.class;
}

/**
* 加密实体类中标记@Encrypted的字段
* @param obj 待加密的实体类对象
*/
private void encryptFields(Object obj) {
if (obj == null) {
return;
}

// 获取实体类所有字段(包括私有字段)
Field[] fields = obj.getClass().getDeclaredFields();
for (Field field : fields) {
// 仅处理标记@Encrypted的字段
if (field.isAnnotationPresent(Encrypted.class)) {
try {
// 开启私有字段访问权限
field.setAccessible(true);
// 获取字段原值
Object value = field.get(obj);

// 仅对字符串类型且未加密的值进行加密
if (value instanceof String && !CryptoUtil.isEncrypted((String) value)) {
String encryptedValue = CryptoUtil.encrypt((String) value);
// 设置加密后的值
field.set(obj, encryptedValue);
log.debug("字段{}加密完成", field.getName());
}
} catch (Exception e) {
log.error("加密字段{}失败", field.getName(), e);
}
}
}
}

/**
* 解密查询结果(支持单个对象/集合)
* @param result 查询返回的结果
*/
private void decryptResult(Object result) {
if (result instanceof Collection) {
// 处理集合类型结果(List/Set等)
for (Object item : (Collection<?>) result) {
decryptFields(item);
}
} else if (result != null) {
// 处理单个对象
decryptFields(result);
}
}

/**
* 解密实体类中标记@Encrypted的字段
* @param obj 待解密的实体类对象
*/
private void decryptFields(Object obj) {
if (obj == null) {
return;
}

Field[] fields = obj.getClass().getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Encrypted.class)) {
try {
field.setAccessible(true);
Object value = field.get(obj);

// 仅对字符串类型且已加密的值进行解密
if (value instanceof String && CryptoUtil.isEncrypted((String) value)) {
String decryptedValue = CryptoUtil.decrypt((String) value);
field.set(obj, decryptedValue);
log.debug("字段{}解密完成", field.getName());
}
} catch (Exception e) {
log.error("解密字段{}失败", field.getName(), e);
}
}
}
}

/**
* 插件初始化:设置插件属性(本文暂无需配置)
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {
// 可通过配置文件传入自定义属性(如加密开关)
}
}

拦截器自动配置

通过Spring Boot自动配置类,让拦截器生效:

import org.apache.ibatis.session.Configuration;
import org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* 加解密自动配置类
* 通过配置文件控制是否启用加解密功能
*/
@Configuration
// 仅当配置文件中encryption.enabled=true时生效
@ConditionalOnProperty(prefix = "encryption", name = "enabled", havingValue = "true")
public class EncryptionAutoConfiguration {

/**
* 注册MyBatis配置自定义器,添加加解密拦截器
*/
@Bean
public ConfigurationCustomizer encryptionConfigurationCustomizer() {
return new ConfigurationCustomizer() {
@Override
public void customize(Configuration configuration) {
// 添加自定义加解密拦截器
configuration.addInterceptor(new EncryptionInterceptor());
log.info("MyBatis加解密拦截器已启用");
}
};
}
}

配置文件启用

在application.yml中添加配置,启用加解密功能:

# 加解密配置
encryption:
enabled: true # 启用加解密拦截器

使用效果演示

原始业务代码(无加解密逻辑)

业务层代码完全无需关注加解密,像操作明文一样使用敏感字段:

import org.springframework.stereotype.Service;

/**
* 用户业务服务类
* 业务代码完全无需关注加解密逻辑
*/
@Service
public class UserService {
// 注入UserMapper(MyBatis映射器)
private final UserMapper userMapper;

// 构造器注入
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}

/**
* 创建用户(自动加密敏感字段)
*/
public void createUser() {
User user = new User();
user.setUsername("张三");
user.setPhone("13812345678"); // 明文设置
user.setEmail("zhangsan@example.com"); // 明文设置
user.setIdCard("110101199001011234"); // 明文设置
// 插入数据库时,拦截器自动加密敏感字段
userMapper.insert(user);
}

/**
* 查询用户(自动解密敏感字段)
*/
public User getUser(Long id) {
// 查询结果返回时,拦截器自动解密敏感字段
User user = userMapper.findById(id);
// 直接获取明文数据
System.out.println("手机号:" + user.getPhone()); // 输出:13812345678
System.out.println("邮箱:" + user.getEmail()); // 输出:zhangsan@example.com
return user;
}
}

数据库存储效果

数据库中敏感字段以密文形式存储,非敏感字段仍为明文:

字段名 存储值(示例)
id 1
username 张三
phone nTuVgMWime1:hFGa9as6JHxLT2vG8dpiRmu4wtxDnkTE
email mK7pL9xQ2rS8vN3w:jKxL9mN2pQ7rS8vT3wX4yZ6aB8
idCard X1Y2Z3A4B5C6D7E8:F9G0H1I2J3K4L5M6N7O8P9Q0R1

生产环境安全建议

密钥安全管理

严禁将密钥硬编码在代码中,建议从配置中心(如Nacos/Apollo)、环境变量或加密文件中读取:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
* 加密密钥配置类
* 从配置文件读取Base64编码的密钥,避免硬编码
*/
@Configuration
public class EncryptionConfig {
// 从配置文件读取密钥(Base64编码)
@Value("${encryption.key}")
private String encryptionKeyBase64;

/**
* 获取AES密钥
*/
@Bean
public SecretKey secretKey() {
// Base64解码密钥
byte[] keyBytes = Base64.getDecoder().decode(encryptionKeyBase64);
// 生成AES密钥(密钥长度需为16/24/32字节)
return new SecretKeySpec(keyBytes, "AES");
}
}

配置文件中配置密钥(Base64编码):

encryption:
enabled: true
key: MTIzNDU2Nzg5MDEyMzQ1Ng== # Base64编码的16字节密钥

敏感数据日志脱敏

避免在日志中打印明文敏感数据,需对日志中的敏感字段进行脱敏处理:

import org.apache.commons.lang3.StringUtils;

/**
* 敏感数据脱敏工具类
* 避免日志/接口返回中泄露明文敏感数据
*/
public class SensitiveDataMaskUtil {
/**
* 手机号脱敏:保留前3位和后4位,中间用****代替
*/
public static String maskPhone(String phone) {
if (StringUtils.isBlank(phone) || phone.length() != 11) {
return phone;
}
return phone.substring(0, 3) + "****" + phone.substring(7);
}

/**
* 邮箱脱敏:保留前2位和域名,中间用***代替
*/
public static String maskEmail(String email) {
if (StringUtils.isBlank(email)) {
return email;
}
int atIndex = email.indexOf("@");
if (atIndex <= 2) {
return "***" + email.substring(atIndex);
}
return email.substring(0, 2) + "***" + email.substring(atIndex);
}

/**
* 身份证号脱敏:保留前6位和后4位,中间用********代替
*/
public static String maskIdCard(String idCard) {
if (StringUtils.isBlank(idCard) || idCard.length() != 18) {
return idCard;
}
return idCard.substring(0, 6) + "********" + idCard.substring(14);
}
}

方案总结

核心优势

  • 接入简单: 仅需在敏感字段添加@Encrypted注解,零侵入业务代码;
  • 维护成本低: 加解密逻辑集中在拦截器和工具类中,便于统一管理和修改;
  • 安全性高: 采用标准AES-GCM加密算法,支持密钥安全管理;
  • 透明化: 业务层无感知,开发体验与操作明文一致。

适用场景

  • 用户中心系统(手机号、身份证、邮箱加密);
  • 支付系统(银行卡号、手机号加密);
  • 医疗信息系统(患者隐私数据加密);
  • 任何需要合规存储敏感数据的业务系统。

不适用场景

  • 对性能要求极致的高频读写系统(加解密会带来轻微性能损耗);
  • 需要对加密字段进行模糊查询/排序/聚合的场景(密文无法直接查询);
  • 超大规模数据存储系统(需结合数据库原生加密功能)。
文章目录
  1. 1. 传统加解密方式的痛点
    1. 1.1. 代码冗余严重
    2. 1.2. 扩展与维护成本高
    3. 1.3. 理想的加解密方案
  2. 2. 解决方案设计思路
  3. 3. 核心实现步骤
    1. 3.1. 定义加密标记注解
    2. 3.2. 注解使用示例
    3. 3.3. 加解密工具类实现
    4. 3.4. MyBatis拦截器实现(核心)
    5. 3.5. 拦截器自动配置
    6. 3.6. 配置文件启用
  4. 4. 使用效果演示
    1. 4.1. 原始业务代码(无加解密逻辑)
    2. 4.2. 数据库存储效果
  5. 5. 生产环境安全建议
    1. 5.1. 密钥安全管理
    2. 5.2. 敏感数据日志脱敏
  6. 6. 方案总结
    1. 6.1. 核心优势
    2. 6.2. 适用场景
    3. 6.3. 不适用场景