Auto‑Encrypt Sensitive Fields in Spring Boot with MyBatis Annotations
This article explains how to eliminate repetitive manual encryption code in Java applications by using a custom @Encrypted annotation together with a MyBatis interceptor, enabling transparent AES‑GCM encryption and decryption of sensitive fields such as phone numbers, emails, and ID numbers.
Background
Sensitive user data (phone, ID, bank card) must not be stored in plain text. Manual encryption in each query leads to code duplication, high maintenance cost, and scattered logic.
Solution Overview
Use an annotation‑driven approach with a MyBatis interceptor to encrypt fields automatically.
Mark fields with @Encrypted.
Interceptor encrypts parameters on update / insert and decrypts result sets on query.
Business code works with plain objects; encryption is transparent.
Performance impact is minimal.
Technical Design
Annotation
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Encrypted {
boolean supportFuzzyQuery() default false;
}Encryption Utility (AES‑GCM)
public class CryptoUtil {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int IV_LENGTH = 12;
public static String encrypt(String plaintext) {
byte[] iv = new byte[IV_LENGTH];
new SecureRandom().nextBytes(iv);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(128, iv));
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);
}
public static String decrypt(String encryptedText) {
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);
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, iv));
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext, StandardCharsets.UTF_8);
}
public static boolean isEncrypted(String value) {
return value != null && value.contains(":");
}
}MyBatis Interceptor
@Intercepts({
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class EncryptionInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
String method = invocation.getMethod().getName();
if ("update".equals(method)) {
Object param = getParameter(invocation);
if (shouldEncrypt(param)) {
encryptFields(param);
}
}
Object result = invocation.proceed();
if ("query".equals(method)) {
decryptResult(result);
}
return result;
}
private void encryptFields(Object obj) { /* reflect @Encrypted fields and encrypt */ }
private void decryptResult(Object result) { /* handle List or single object */ }
private void decryptFields(Object obj) { /* reverse of encryptFields */ }
// helper methods omitted for brevity
}Automatic Configuration
@Configuration
@ConditionalOnProperty(name = "encryption.enabled", havingValue = "true", matchIfMissing = true)
public class EncryptionAutoConfiguration {
@Bean
public ConfigurationCustomizer encryptionConfigurationCustomizer() {
return configuration -> configuration.addInterceptor(new EncryptionInterceptor());
}
}Enable the interceptor with:
encryption:
enabled: trueUsage Example
Entity
public class User {
private Long id;
private String username;
@Encrypted
private String phone;
@Encrypted
private String email;
}Service
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void createUser(User user) {
user.setPhone("13812345678");
user.setEmail("[email protected]");
userMapper.insert(user);
}
public User getUser(Long id) {
User user = userMapper.findById(id);
// phone and email are already decrypted
return user;
}
}Security Considerations
Key Management
Load the AES key from environment variables, a configuration server, or a dedicated KMS instead of hard‑coding.
@Configuration
public class EncryptionConfig {
@Value("${encryption.key}")
private String encryptionKey;
@Bean
public SecretKey secretKey() {
byte[] keyBytes = Base64.getDecoder().decode(encryptionKey);
return new SecretKeySpec(keyBytes, "AES");
}
}Logging Safety
Avoid logging raw encrypted values; provide masking utilities.
public class SensitiveDataLogger {
public String maskPhone(String phone) {
if (phone == null || phone.length() != 11) return phone;
return phone.substring(0,3) + "****" + phone.substring(7);
}
public String maskEmail(String email) {
if (email == null) return email;
int at = email.indexOf('@');
if (at <= 2) return "***" + email.substring(at);
return email.substring(0,2) + "***" + email.substring(at);
}
}Advantages and Limitations
Ease of use: Add @Encrypted to a field.
Clean code: Business logic stays free of encryption code.
Security: Uses standard AES‑GCM.
Maintainability: All encryption logic is centralized in the interceptor.
Not suitable for: Extremely latency‑sensitive workloads, complex encrypted queries, or massive‑scale data sets.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Enthusiast
Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!
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.
