Spring Boot自定义字段加密解密注解实现方案咨询
Hey there! 作为Spring自定义注解的新手,你想实现的这个类似Couchbase @EncryptedField的需求完全可行,而且确实有几种靠谱的方案可以选——Spring AOP是其中一种,不过还有更贴合数据持久化场景的JPA EntityListener方案,我给你详细拆解每种方案的实现思路,你可以根据自己的项目情况来挑:
方案一:Spring AOP 实现字段加密解密
核心思路
利用AOP拦截数据库操作的关键方法(比如Repository的保存/查询方法),在方法执行前对标注了@EncryptedField的字段加密,执行完成后对返回的实体字段解密。
步骤1:定义自定义注解@EncryptedField
先写一个简单的注解,用来标记需要加密的字段:
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface EncryptedField { }
步骤2:封装加密解密工具类
把加密解密的逻辑抽成工具类,建议用AES算法,注意密钥要从配置文件读取,绝对不能硬编码:
@Component public class EncryptionUtils { @Value("${encryption.secret-key}") // 从application.yml/properties读取密钥 private String secretKey; private static final String ALGORITHM = "AES"; public String encrypt(String plainText) throws Exception { if (plainText == null) return null; Cipher cipher = Cipher.getInstance(ALGORITHM); SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(), ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, keySpec); byte[] encryptedBytes = cipher.doFinal(plainText.getBytes()); return Base64.getEncoder().encodeToString(encryptedBytes); } public String decrypt(String encryptedText) throws Exception { if (encryptedText == null) return null; Cipher cipher = Cipher.getInstance(ALGORITHM); SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(), ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, keySpec); byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedText)); return new String(decryptedBytes); } }
步骤3:编写AOP切面拦截方法
拦截Repository的保存和查询方法,处理单个实体或集合类型的参数/返回值:
@Aspect @Component public class EncryptionAspect { @Autowired private EncryptionUtils encryptionUtils; // 拦截保存/更新方法 @Before("execution(* org.springframework.data.repository.Repository+.save*(..))") public void encryptBeforeSave(JoinPoint joinPoint) throws Exception { processEntities(joinPoint.getArgs(), this::encryptFields); } // 拦截查询方法,解密返回结果 @AfterReturning(pointcut = "execution(* org.springframework.data.repository.Repository+.find*(..))", returning = "result") public void decryptAfterFind(Object result) throws Exception { processSingleEntity(result, this::decryptFields); } // 批量处理实体(支持单个或集合) private void processEntities(Object[] args, Consumer<Object> processor) throws Exception { for (Object arg : args) { if (arg instanceof Collection) { ((Collection<?>) arg).forEach(processor); } else { processor.accept(arg); } } } private void processSingleEntity(Object result, Consumer<Object> processor) throws Exception { if (result instanceof Collection) { ((Collection<?>) result).forEach(processor); } else { processor.accept(result); } } // 反射加密标注了@EncryptedField的字段 private void encryptFields(Object entity) throws Exception { Field[] fields = entity.getClass().getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(EncryptedField.class) && field.getType().equals(String.class)) { field.setAccessible(true); String plainText = (String) field.get(entity); field.set(entity, encryptionUtils.encrypt(plainText)); } } } // 反射解密标注了@EncryptedField的字段 private void decryptFields(Object entity) throws Exception { Field[] fields = entity.getClass().getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(EncryptedField.class) && field.getType().equals(String.class)) { field.setAccessible(true); String encryptedText = (String) field.get(entity); field.set(entity, encryptionUtils.decrypt(encryptedText)); } } } }
注意点
- 切入点表达式要根据你的实际项目调整,如果是自定义Service方法,要改成Service层的方法路径
- 要处理分页查询的返回结果(比如
Page类型),可以在processSingleEntity里增加判断 - 加密解密的异常要做统一处理,避免影响正常业务
方案二:JPA EntityListener(更推荐,贴合持久化生命周期)
核心思路
利用JPA的实体生命周期回调注解(@PrePersist、@PreUpdate、@PostLoad),在实体保存/更新前自动加密字段,从数据库加载后自动解密字段,逻辑更贴合数据持久化的流程。
步骤1:定义@EncryptedField注解
和方案一的注解完全一致,这里不再重复。
步骤2:加密解密工具类
和方案一的工具类完全一致。
步骤3:编写EntityListener监听实体生命周期
@Component public class EncryptionEntityListener { @Autowired private EncryptionUtils encryptionUtils; // 保存/更新前加密字段 @PrePersist @PreUpdate public void encryptBeforeSave(Object entity) throws Exception { encryptFields(entity); } // 加载实体后解密字段 @PostLoad public void decryptAfterLoad(Object entity) throws Exception { decryptFields(entity); } private void encryptFields(Object entity) throws Exception { Field[] fields = entity.getClass().getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(EncryptedField.class) && field.getType().equals(String.class)) { field.setAccessible(true); String plainText = (String) field.get(entity); if (plainText != null) { field.set(entity, encryptionUtils.encrypt(plainText)); } } } } private void decryptFields(Object entity) throws Exception { Field[] fields = entity.getClass().getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(EncryptedField.class) && field.getType().equals(String.class)) { field.setAccessible(true); String encryptedText = (String) field.get(entity); if (encryptedText != null) { field.set(entity, encryptionUtils.decrypt(encryptedText)); } } } } }
步骤4:在实体类上绑定Listener
@Entity @EntityListeners(EncryptionEntityListener.class) // 绑定监听类 public class User { @Id private Long id; private String username; @EncryptedField // 标记需要加密的字段 private String phone; @EncryptedField private String email; // getter、setter省略 }
为什么推荐这个方案?
- 完全贴合JPA的实体生命周期,逻辑清晰,不需要额外的AOP拦截配置
- 只对绑定了Listener的实体生效,范围可控
- 天然支持单个实体的保存/加载,不需要额外处理集合类型(JPA会自动遍历集合中的实体)
方案对比与选择
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Spring AOP | 可以拦截任意层的方法(比如Service层),不局限于JPA | 切入点配置复杂,容易遗漏方法;需要手动处理分页、集合等特殊返回值 | 项目未使用JPA,或者需要在Service层统一处理加密逻辑 |
| JPA EntityListener | 贴合持久化生命周期,逻辑简洁,无需额外拦截 | 仅适用于JPA实体;如果用MyBatis等其他持久化框架则不适用 | 使用Spring Data JPA的项目,推荐优先选择 |
如果你的项目用的是MyBatis,可以考虑自定义TypeHandler结合@EncryptedField注解,思路和上面类似:在设置SQL参数时加密字段,从结果集读取时解密字段。
内容的提问来源于stack exchange,提问作者vijayashankard




