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.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Automate Sensitive Data Encryption in MyBatis with Annotations and Interceptors

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: true

Usage 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 key

Sensitive 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.

JavaMyBatisInterceptorAnnotationEncryption
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.