Spring Boot自定义Attribute Converter异常:重复调用与解密失效
在Spring Boot+Spring JPA场景下,对自定义对象字段做加密存储时,自定义的AttributeConverter未按预期工作,存在两个问题:
- 调用
repository.save()保存数据时,convertToDatabaseColumn方法被重复调用4次,且会触发convertToEntityAttribute方法; - 调用
repository.findByReferenceIdAndName()查询数据时,convertToEntityAttribute方法未被调用,无法获取解密后的值。
相关代码如下:
public class PIDetails { @Id @GeneratedValue(strategy = IDENTITY) @Column(name = "id") private Integer id; @Column(name = "name") private String name; @Column(name = "referenceId") private String referenceId; @Column(name = "pdo") @Convert(converter = AttributeEncryptor.class) private PersonalDetails pdo; public PIDetails(String lender, String referenceId) { this.lender = lender; this.referenceId = referenceId; } public void setpdo(@NotNull BoardingDetails boardingDetails) { this.bfo = boardingDetails; } } public class PersonalDetails { private FinancialDetails.FinancialDetailsData financialDetails; } @NoArgsConstructor @AllArgsConstructor @SuperBuilder @Getter public class FinancialDetails extends OnboardingDetails { @NotNull(message = "financial data can't be null") private FinancialDetailsData data; @NoArgsConstructor @AllArgsConstructor @Builder @Getter @Setter public static class FinancialDetailsData { @JsonProperty("externalReferenceId") private String externalReferenceId; @JsonProperty("panNumber") private String panNumber; @JsonProperty("kycNo") private String kycNo; @JsonProperty("dob") private long dob; } } @Converter public class AttributeEncryptor implements AttributeConverter<PersonalDetails, String> { private static final String AES = "AES"; private static final String SECRET = "secret-key-12345"; private final Key key; private final Cipher cipher; private Gson gson = new Gson(); public AttributeEncryptor() throws Exception { key = new SecretKeySpec(SECRET.getBytes(), AES); cipher = Cipher.getInstance(AES); } @Override public String convertToDatabaseColumn(PersonalDetails personalDetails) { try { if(!isEncryptedString(personalDetails.getFinancialDetails().getPanNumber())){ String encryptedPan = encrypt(personalDetails.getFinancialDetails().getPanNumber()); personalDetails.getFinancialDetails().setPanNumber(encryptedPan); } String personalDetailsJson= gson.toJson(personalDetails); return personalDetailsJson; } catch (Exception e) { throw new IllegalStateException(e); } } @Override public PersonalDetails convertToEntityAttribute(String dbData) { try { PersonalDetails personalDetails = gson.fromJson(dbData, PersonalDetails.class); String panNumber = decrypt(personalDetails.getFinancialDetails().getPanNumber()); personalDetails.getFinancialDetails().setPanNumber(panNumber); return personalDetails; } catch (Exception e) { throw new IllegalStateException(e); } } public String encrypt(String attribute){ try { cipher.init(Cipher.ENCRYPT_MODE, key); return Base64.getEncoder().encodeToString(cipher.doFinal(attribute.getBytes())); } catch (IllegalBlockSizeException | BadPaddingException | InvalidKeyException e) { throw new IllegalStateException(e); } } public String decrypt(String attribute){ try { cipher.init(Cipher.DECRYPT_MODE, key); return new String(cipher.doFinal(Base64.getDecoder().decode(attribute))); } catch (InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) { throw new IllegalStateException(e); } } public boolean isEncryptedString(String attribute) { try { decrypt(attribute); return true; } catch(Exception e) { return false; } } }
代码中存在的问题
1. 修改原始实体触发JPA重复转换
在convertToDatabaseColumn方法中,直接修改了传入的personalDetails对象的panNumber字段:
personalDetails.getFinancialDetails().setPanNumber(encryptedPan);
JPA会跟踪实体对象的状态变化,实体被修改后会重新触发字段转换逻辑,导致convertToDatabaseColumn被多次调用,甚至触发convertToEntityAttribute方法(实体状态变更后JPA可能重新校验加载)。
2. Cipher实例非线程安全,存在并发风险
Cipher类实例不是线程安全的,当前代码将其定义为全局成员变量,多线程环境下同时调用加密/解密方法会导致异常或数据错乱。
3. 实体类字段映射错误,赋值失败
PIDetails构造函数中使用this.lender,但类中未定义lender字段;setpdo方法参数为BoardingDetails,却给this.bfo赋值,而类中实际字段是pdo,导致pdo字段无法被正确赋值,后续转换易出现空指针异常。
4. 序列化/反序列化字段映射不匹配
PersonalDetails类的字段是financialDetails(类型FinancialDetails.FinancialDetailsData),但FinancialDetails类的对应数据字段是data(类型FinancialDetailsData)。Gson序列化时,PersonalDetails的financialDetails会被序列化为JSON的"financialDetails"字段,与实际存储的结构不匹配,导致反序列化时personalDetails.getFinancialDetails()为null,进而在convertToEntityAttribute中抛出异常,甚至导致JPA无法触发转换逻辑。
5. 查询未触发解密的潜在原因
除上述序列化问题外,还可能是:
- JPA使用二级缓存,缓存了未解密的原始数据,未触发Converter;
- 自定义查询方法返回DTO而非实体对象,Converter仅对实体字段生效;
pdo字段被标记为懒加载,但查询时未显式加载该字段。
内容的提问来源于stack exchange,提问作者Shiva

