如何让Spring JPA使用自定义ObjectMapper实现字段加密解密?
问题描述
我正在使用Spring Data JPA Repositories结合Hibernate,希望调用JpaRepository方法时触发自定义序列化/反序列化器。需求是:带有@Encrypted注解的实体属性,保存时自动加密存入数据库,查询时自动解密。我已经在客户端-服务端通信中实现了该功能,不想重复添加@JsonSerialize/@JsonDeserialize注解,想复用现有ObjectMapper配置(如下代码),请问如何让Spring JPA强制使用我的ObjectMapper?
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class JsonEncryptionConfig { @Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); ClientServerEncryptionService clientServerEncryptionService = new ClientServerEncryptionService(mapper); DbEncryptionService serverDbEncryptionService = new DbEncryptionService(mapper); SimpleModule cryptoModule = new SimpleModule() { @Override public void setupModule(SetupContext context) { context.addBeanSerializerModifier(new EncryptedJsonSerializer.Modifier(clientServerEncryptionService, serverDbEncryptionService)); context.addBeanDeserializerModifier(new EncryptedJsonDeserializer.Modifier(clientServerEncryptionService, serverDbEncryptionService)); } }; mapper.registerModule(cryptoModule); return mapper; } }
解决方案
要让Spring JPA(底层依赖Hibernate)复用你已配置的ObjectMapper处理实体属性的加密解密,核心是让Hibernate在持久化/加载数据时调用你的ObjectMapper逻辑,以下是两种可行方案:
方案一:自定义Hibernate AttributeConverter
通过实现Hibernate的AttributeConverter接口,在属性转换逻辑中直接注入并复用你的ObjectMapper,自动对带@Encrypted注解的属性进行加密解密:
- 实现转换器,注入Spring容器中的ObjectMapper:
import jakarta.persistence.AttributeConverter; import jakarta.persistence.Converter; import org.springframework.beans.factory.annotation.Autowired; import com.fasterxml.jackson.databind.ObjectMapper; @Converter(autoApply = false) public class EncryptedAttributeConverter implements AttributeConverter<Object, String> { private final ObjectMapper objectMapper; @Autowired public EncryptedAttributeConverter(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Override public String convertToDatabaseColumn(Object attribute) { try { // 触发ObjectMapper中配置的加密序列化逻辑 return objectMapper.writeValueAsString(attribute); } catch (Exception e) { throw new RuntimeException("加密属性失败", e); } } @Override public Object convertToEntityAttribute(String dbData) { try { // 触发ObjectMapper中配置的解密反序列化逻辑 return objectMapper.readValue(dbData, Object.class); } catch (Exception e) { throw new RuntimeException("解密属性失败", e); } } }
- 在实体属性上关联该转换器:
import jakarta.persistence.Column; import jakarta.persistence.Convert; import your.package.EncryptedAttributeConverter; public class YourEntity { @Encrypted @Convert(converter = EncryptedAttributeConverter.class) @Column(columnDefinition = "TEXT") private String sensitiveData; // 其他属性与方法 }
如果想让所有带@Encrypted注解的属性自动应用转换器,可以将@Converter的autoApply设为true,再配合自定义注解的匹配逻辑,不过手动指定转换器更稳妥。
方案二:自定义Hibernate Type
通过扩展Hibernate的AbstractSingleColumnStandardBasicType,将ObjectMapper整合到Hibernate的类型系统中,灵活性更高:
- 自定义Hibernate类型:
import org.hibernate.type.AbstractSingleColumnStandardBasicType; import org.hibernate.type.descriptor.java.StringJavaType; import org.hibernate.type.descriptor.jdbc.VarcharJdbcType; import com.fasterxml.jackson.databind.ObjectMapper; public class EncryptedJsonType extends AbstractSingleColumnStandardBasicType<Object> { public EncryptedJsonType(ObjectMapper objectMapper) { super(VarcharJdbcType.INSTANCE, new EncryptedJsonJavaType(objectMapper)); } @Override public String getName() { return "encrypted_json"; } } // 自定义Java类型描述器,复用ObjectMapper class EncryptedJsonJavaType extends StringJavaType { private final ObjectMapper objectMapper; public EncryptedJsonJavaType(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Override public Object fromString(CharSequence string) { try { return objectMapper.readValue(string.toString(), Object.class); } catch (Exception e) { throw new RuntimeException("解密值失败", e); } } @Override public String toString(Object value) { try { return objectMapper.writeValueAsString(value); } catch (Exception e) { throw new RuntimeException("加密值失败", e); } } }
- 注册自定义类型到Hibernate:
import org.springframework.context.annotation.Configuration; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; @Configuration public class HibernateConfig { private final ObjectMapper objectMapper; public HibernateConfig(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) { LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); em.setDataSource(dataSource); em.setPackagesToScan("your.entity.package"); em.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); Map<String, Object> properties = new HashMap<>(); // 注册自定义类型 properties.put("hibernate.types.encrypted_json", your.package.EncryptedJsonType.class.getName()); em.setJpaPropertyMap(properties); return em; } }
- 在实体属性上指定自定义类型:
import org.hibernate.annotations.Type; import your.package.EncryptedJsonType; public class YourEntity { @Encrypted @Type(type = "encrypted_json") @Column(columnDefinition = "TEXT") private String sensitiveData; // 其他属性与方法 }
关键注意事项
- 注意区分客户端-服务端和数据库场景的加密逻辑:你的ObjectMapper中同时注入了
ClientServerEncryptionService和DbEncryptionService,要确保在数据库转换时使用的是serverDbEncryptionService对应的逻辑,可调整模块逻辑或在转换器中直接调用该服务的方法,避免和客户端通信逻辑冲突。 - 数据库字段建议使用
TEXT或足够长度的VARCHAR,避免加密后的字符串超出存储限制。 - 异常处理要适配业务需求,避免泄露敏感信息。
内容的提问来源于stack exchange,提问作者Tomáš Přitrský
相关产品推荐
相关产品推荐

