Automate Sensitive Data Encryption in MyBatis with Annotations and Interceptors
This article explains how to replace manual encryption code with a lightweight, annotation‑driven solution that uses a custom MyBatis interceptor to automatically encrypt and decrypt sensitive fields such as phone numbers, emails, and ID cards, improving code readability, maintainability, and security compliance.
Problem Overview
Strict data‑security compliance requires backend developers to protect sensitive user information (e.g., phone numbers, ID numbers, bank cards). Storing such data in plaintext can cause privacy breaches, legal liabilities, and reputation damage.
Drawbacks of Manual Encryption
Code redundancy: each sensitive field must be encrypted before insert and decrypted after query.
High maintenance cost: adding a new encrypted field requires changes in all CRUD code; missing decryption can expose ciphertext; encryption logic is scattered, making troubleshooting difficult.
// Traditional query decryption example
User user = userMapper.findById(id);
user.setPhone(decrypt(user.getPhone()));
user.setEmail(decrypt(user.getEmail()));
user.setIdCard(decrypt(user.getIdCard()));
return user;
// Traditional insert encryption example
User newUser = new User();
newUser.setPhone(encrypt(phone));
newUser.setEmail(encrypt(email));
userMapper.insert(newUser);Desired Solution Characteristics
Minimal Integration: Add an annotation on sensitive fields; no business‑code changes.
Full Automation: Encryption and decryption are performed transparently by the framework.
Low Intrusiveness: Does not affect existing MyBatis usage patterns.
High Performance: Lightweight logic with negligible impact on response time.
Solution Design
The core idea is "annotation + MyBatis interceptor". A custom interceptor sits between the business layer and the database, handling encryption before writes and decryption after reads.
业务代码 → MyBatis Mapper → 自定义拦截器(自动加解密) → 数据库Annotation Marking: Define @Encrypted to mark fields that need encryption.
Interceptor Processing: Intercept MyBatis update and query methods to encrypt or decrypt marked fields.
Transparent Operation: Business code works with plaintext as if no encryption exists.
Core Implementation Steps
Define Encryption Annotation
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Field encryption annotation: marks entity fields that require automatic encryption/decryption.
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Encrypted {
/**
* Whether fuzzy query is supported (default false).
*/
boolean supportFuzzyQuery() default false;
}Annotation Usage Example
/**
* User entity class containing sensitive fields.
*/
public class User {
private Long id;
private String username; // non‑sensitive
@Encrypted
private String phone; // sensitive
@Encrypted
private String email; // sensitive
@Encrypted
private String idCard; // sensitive
// getters & setters omitted
}AES‑GCM Encryption Utility
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 encryption/decryption helper.
*/
public class CryptoUtil {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int IV_LENGTH = 12; // GCM recommended length
private static final SecretKey SECRET_KEY = new SecretKeySpec("1234567890123456".getBytes(StandardCharsets.UTF_8), "AES");
public static String encrypt(String plaintext) {
if (plaintext == null || plaintext.isEmpty()) return plaintext;
try {
byte[] iv = new byte[IV_LENGTH];
new SecureRandom().nextBytes(iv);
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.ENCRYPT_MODE, SECRET_KEY, spec);
byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
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);
return Base64.getEncoder().encodeToString(encryptedData);
} catch (Exception e) {
throw new RuntimeException("AES encryption failed", e);
}
}
public static String decrypt(String encryptedText) {
if (encryptedText == null || encryptedText.isEmpty()) return encryptedText;
try {
byte[] encryptedData = Base64.getDecoder().decode(encryptedText);
byte[] iv = Arrays.copyOfRange(encryptedData, 0, IV_LENGTH);
byte[] ciphertext = Arrays.copyOfRange(encryptedData, IV_LENGTH, encryptedData.length);
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.DECRYPT_MODE, SECRET_KEY, spec);
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("AES decryption failed", e);
}
}
public static boolean isEncrypted(String value) {
if (value == null) return false;
return value.length() > (IV_LENGTH + 8) && isBase64(value);
}
private static boolean isBase64(String value) {
try {
Base64.getDecoder().decode(value);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
}MyBatis Interceptor (Core)
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;
/**
* Intercepts MyBatis update/query to encrypt/decrypt fields marked with @Encrypted.
*/
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@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);
@Override
public Object intercept(Invocation invocation) throws Throwable {
String methodName = invocation.getMethod().getName();
if ("update".equals(methodName)) {
Object parameter = getParameter(invocation);
if (shouldEncrypt(parameter)) {
encryptFields(parameter);
}
}
Object result = invocation.proceed();
if ("query".equals(methodName)) {
decryptResult(result);
}
return result;
}
private Object getParameter(Invocation invocation) {
Object[] args = invocation.getArgs();
return args.length >= 2 ? args[1] : null;
}
private boolean shouldEncrypt(Object parameter) {
if (parameter == null) return false;
Class<?> clazz = parameter.getClass();
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;
}
private void encryptFields(Object obj) {
if (obj == null) return;
for (Field field : obj.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(Encrypted.class)) {
try {
field.setAccessible(true);
Object value = field.get(obj);
if (value instanceof String && !CryptoUtil.isEncrypted((String) value)) {
String encrypted = CryptoUtil.encrypt((String) value);
field.set(obj, encrypted);
log.debug("Field {} encrypted", field.getName());
}
} catch (Exception e) {
log.error("Encryption failed for field {}", field.getName(), e);
}
}
}
}
private void decryptResult(Object result) {
if (result instanceof Collection) {
for (Object item : (Collection<?>) result) {
decryptFields(item);
}
} else if (result != null) {
decryptFields(result);
}
}
private void decryptFields(Object obj) {
if (obj == null) return;
for (Field field : obj.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(Encrypted.class)) {
try {
field.setAccessible(true);
Object value = field.get(obj);
if (value instanceof String && CryptoUtil.isEncrypted((String) value)) {
String decrypted = CryptoUtil.decrypt((String) value);
field.set(obj, decrypted);
log.debug("Field {} decrypted", field.getName());
}
} catch (Exception e) {
log.error("Decryption failed for field {}", field.getName(), e);
}
}
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// No properties needed for this example
}
}Spring Boot Auto‑Configuration
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;
/**
* Registers the EncryptionInterceptor when encryption.enabled=true.
*/
@Configuration
@ConditionalOnProperty(prefix = "encryption", name = "enabled", havingValue = "true")
public class EncryptionAutoConfiguration {
@Bean
public ConfigurationCustomizer encryptionConfigurationCustomizer() {
return configuration -> {
configuration.addInterceptor(new EncryptionInterceptor());
};
}
}Enable in application.yml
encryption:
enabled: trueUsage Demonstration
Business Service (No Encryption Logic)
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final UserMapper userMapper;
public UserService(UserMapper userMapper) { this.userMapper = userMapper; }
// Create user – interceptor encrypts sensitive fields automatically
public void createUser() {
User user = new User();
user.setUsername("张三");
user.setPhone("13812345678"); // plain text
user.setEmail("[email protected]"); // plain text
user.setIdCard("110101199001011234"); // plain text
userMapper.insert(user);
}
// Retrieve user – interceptor decrypts automatically
public User getUser(Long id) {
User user = userMapper.findById(id);
System.out.println("手机号:" + user.getPhone());
System.out.println("邮箱:" + user.getEmail());
return user;
}
}Database Storage Result
After insertion, the phone, email, and idCard columns contain ciphertext, while non‑sensitive columns such as id and username remain in plaintext.
Production‑Grade Security Recommendations
Key Management
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.util.Base64;
/**
* Loads AES key from configuration (Base64) instead of hard‑coding.
*/
@Configuration
public class EncryptionConfig {
@Value("${encryption.key}")
private String encryptionKeyBase64;
@Bean
public SecretKey secretKey() {
byte[] keyBytes = Base64.getDecoder().decode(encryptionKeyBase64);
return new SecretKeySpec(keyBytes, "AES");
}
} encryption:
enabled: true
key: MTIzNDU2Nzg5MDEyMzQ1Ng== # Base64‑encoded 16‑byte keySensitive Data Masking for Logs
import org.apache.commons.lang3.StringUtils;
public class SensitiveDataMaskUtil {
public static String maskPhone(String phone) {
if (StringUtils.isBlank(phone) || phone.length() != 11) return phone;
return phone.substring(0, 3) + "****" + phone.substring(7);
}
public static String maskEmail(String email) {
if (StringUtils.isBlank(email)) return email;
int at = email.indexOf('@');
if (at <= 2) return "***" + email.substring(at);
return email.substring(0, 2) + "***" + email.substring(at);
}
public static String maskIdCard(String idCard) {
if (StringUtils.isBlank(idCard) || idCard.length() != 18) return idCard;
return idCard.substring(0, 6) + "********" + idCard.substring(14);
}
}Solution Summary
Simple Integration: Add @Encrypted to fields; no business‑code changes.
Low Maintenance: Encryption logic is centralized in the interceptor and utility class.
Strong Security: Uses standard AES‑GCM with proper key management.
Transparent to Developers: Business layer reads and writes plaintext as usual.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
