You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Java中集中实现多实体类的数据加密与解密?

嘿,这个问题太有代表性了——重复在每个Service里写加解密逻辑不仅冗余,还容易因为漏写导致数据泄露或者解密失败。我给你整理了几个不同层级的解决方案,你可以根据项目的实际情况挑最适合的:


方案1:JPA属性转换器(AttributeConverter)——最推荐的优雅方案

这是Spring Data JPA官方推荐的字段级加解密方式,完全在JPA层面自动处理,Service层和DTO层完全不需要关心加解密逻辑,代码侵入性极低。

步骤1:编写通用加密转换器

先写一个针对String类型的加密转换器(如果有其他类型需要加密,可以再扩展):

@Component
public class StringEncryptionConverter implements AttributeConverter<String, String> {

    private final EncryptionService encryptionService;

    // 构造注入Spring加密服务Bean,避免单例依赖问题
    public StringEncryptionConverter(EncryptionService encryptionService) {
        this.encryptionService = encryptionService;
    }

    @Override
    public String convertToDatabaseColumn(String plainText) {
        // 写入数据库前自动加密
        if (plainText == null) return null;
        return encryptionService.doEncrypt(plainText);
    }

    @Override
    public String convertToEntityAttribute(String encryptedText) {
        // 从数据库读取时自动解密
        if (encryptedText == null) return null;
        return encryptionService.doDecrypt(encryptedText);
    }
}

步骤2:给实体字段添加注解

只需要在需要加密的字段上加上@Convert注解,其他代码完全不需要改:

@Entity
@Getter
@Setter
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 标记这个字段需要用加密转换器处理
    @Convert(converter = StringEncryptionConverter.class)
    private String name;

    private String contact;
}

步骤3:Service层简化到极致

现在Service里完全不用管加解密,直接用明文操作就行:

@Service
public class CustomerService {
    private final CustomerRepository customerRepository;

    // 构造注入(比@Autowired更推荐)
    public CustomerService(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    public void save(String name){
        Customer customer = new Customer();
        customer.setName(name); // 直接传明文,JPA自动加密后存库
        customerRepository.save(customer);
    }

    public CustomerDTO getCustomer(Long customerId) {
        Customer customer = customerRepository.findById(customerId)
                .orElseThrow(() -> new RuntimeException("Customer not found"));
        CustomerDTO dto = new CustomerDTO();
        dto.setName(customer.getName()); // 直接拿明文,JPA已经自动解密
        return dto;
    }
}

注意事项:

如果需要根据加密字段查询(比如findByName),需要先加密查询参数再传入,或者用Specification动态生成加密后的查询条件:

// 在Repository里添加自定义查询方法
@Repository
public interface CustomerRepository extends JpaRepository<Customer, Long>{
    Optional<Customer> findByName(String encryptedName);
}

// Service里调用时先加密参数
public Optional<Customer> findByName(String plainName) {
    String encryptedName = encryptionService.doEncrypt(plainName);
    return customerRepository.findByName(encryptedName);
}

方案2:通用抽象Service类——适合需要灵活控制的场景

如果你需要对加解密逻辑有更细粒度的控制(比如某些字段只在特定场景加密),可以把通用逻辑抽成抽象Service,让所有实体Service继承它。

步骤1:编写抽象BaseService

public abstract class BaseEncryptedService<T, ID, DTO> {
    protected final JpaRepository<T, ID> repository;
    protected final EncryptionService encryptionService;

    public BaseEncryptedService(JpaRepository<T, ID> repository, EncryptionService encryptionService) {
        this.repository = repository;
        this.encryptionService = encryptionService;
    }

    // 通用保存方法,子类只需要实现字段加密逻辑
    public void save(DTO dto) {
        T entity = convertDtoToEntity(dto);
        encryptEntityFields(entity);
        repository.save(entity);
    }

    // 通用查询方法,子类只需要实现字段解密逻辑
    public DTO get(ID id) {
        T entity = repository.findById(id)
                .orElseThrow(() -> new RuntimeException("Entity not found"));
        decryptEntityFields(entity);
        return convertEntityToDto(entity);
    }

    // 子类需要实现的抽象方法:DTO转实体
    protected abstract T convertDtoToEntity(DTO dto);

    // 子类需要实现的抽象方法:实体转DTO
    protected abstract DTO convertEntityToDto(T entity);

    // 子类需要实现的抽象方法:加密实体字段
    protected abstract void encryptEntityFields(T entity);

    // 子类需要实现的抽象方法:解密实体字段
    protected abstract void decryptEntityFields(T entity);
}

步骤2:子类Service继承并实现逻辑

@Service
public class CustomerService extends BaseEncryptedService<Customer, Long, CustomerDTO> {

    public CustomerService(CustomerRepository repository, EncryptionService encryptionService) {
        super(repository, encryptionService);
    }

    @Override
    protected Customer convertDtoToEntity(CustomerDTO dto) {
        // 可以用ModelMapper、MapStruct等工具简化转换
        Customer customer = new Customer();
        customer.setName(dto.getName());
        return customer;
    }

    @Override
    protected CustomerDTO convertEntityToDto(Customer entity) {
        CustomerDTO dto = new CustomerDTO();
        dto.setName(entity.getName());
        return dto;
    }

    @Override
    protected void encryptEntityFields(Customer entity) {
        entity.setName(encryptionService.doEncrypt(entity.getName()));
        // 其他需要加密的字段在这里添加
    }

    @Override
    protected void decryptEntityFields(Customer entity) {
        entity.setName(encryptionService.doDecrypt(entity.getName()));
    }
}

方案3:重写实体的Get/Set方法——你提到的思路,可行但有坑

这个方案是直接在实体的Get/Set方法里处理加解密,但需要注意JPA的访问类型问题,不然可能导致加解密失效。

修改实体类

@Entity
@Access(AccessType.PROPERTY) // 必须指定JPA通过Get/Set方法访问属性,否则会直接操作字段
public class Customer {
    private Long id;
    private String encryptedName; // 数据库里存储加密后的内容
    private String contact;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    // 对外暴露的明文name属性,内部用encryptedName存储加密值
    public String getName() {
        return encryptionService.doDecrypt(this.encryptedName);
    }

    public void setName(String plainName) {
        this.encryptedName = encryptionService.doEncrypt(plainName);
    }

    // 其他字段正常处理
    public String getContact() {
        return contact;
    }

    public void setContact(String contact) {
        this.contact = contact;
    }

    // 注意:实体里不能直接@Autowired注入Bean,需要通过Spring上下文获取或者用单例
    private EncryptionService encryptionService = SpringContextHolder.getBean(EncryptionService.class);
}

注意事项:

需要处理Spring Bean注入问题(实体无法直接@Autowired);JPA访问类型必须设为PROPERTY,否则会绕过Get/Set方法直接操作字段;查询加密字段时同样需要先加密参数。


总结

如果没有特殊的定制需求,方案1(AttributeConverter)是最优选择,代码最简洁,侵入性最低,完全符合Spring Data JPA的设计理念。如果需要灵活控制加解密逻辑,可以选择方案2(抽象Service)。方案3虽然可行,但存在一些坑,不推荐作为首选。

内容的提问来源于stack exchange,提问作者Teong

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.27 18:54:06