You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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

火山引擎 最新活动