Hibernate @ColumnTransformer注解中使用密钥的实现方案咨询
可行的实现方法
因为@ColumnTransformer的属性要求是编译时常量,没法直接注入配置中的密钥,推荐两种更灵活的实现方式:
方法一:使用Spring Data JPA的AttributeConverter
这是最轻量化的方案,通过实现转换器把实体字段和数据库列的加解密逻辑封装起来,同时可以注入配置中的密钥:
- 实现
AttributeConverter接口,封装加密解密逻辑:
@Component public class StringEncryptConverter implements AttributeConverter<String, byte[]> { @Value("${database.encrypt.secret}") private String secret; @Override public byte[] convertToDatabaseColumn(String attribute) { if (attribute == null) { return null; } try { // 对密钥做SHA256哈希,和原SQL逻辑一致 MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] key = digest.digest(secret.getBytes(StandardCharsets.UTF_8)); Cipher cipher = Cipher.getInstance("AES"); cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES")); return cipher.doFinal(attribute.getBytes(StandardCharsets.UTF_8)); } catch (Exception e) { throw new RuntimeException("加密失败", e); } } @Override public String convertToEntityAttribute(byte[] dbData) { if (dbData == null) { return null; } try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] key = digest.digest(secret.getBytes(StandardCharsets.UTF_8)); Cipher cipher = Cipher.getInstance("AES"); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES")); byte[] decryptedBytes = cipher.doFinal(dbData); // 移除末尾的CHAR(0),匹配原SQL的TRIM逻辑 return new String(decryptedBytes, StandardCharsets.UTF_8).trim().replace("\0", ""); } catch (Exception e) { throw new RuntimeException("解密失败", e); } } }
- 在实体类字段上添加
@Convert注解关联转换器:
@Convert(converter = StringEncryptConverter.class) @Column(columnDefinition = "VARBINARY") private String name;
方法二:自定义Hibernate UserType
如果需要更深度的Hibernate集成(比如支持更复杂的类型或缓存逻辑),可以自定义UserType:
- 实现
UserType接口,封装加解密和类型映射逻辑:
@Component public class EncryptedStringType implements UserType { @Value("${database.encrypt.secret}") private String secret; @Override public int[] sqlTypes() { return new int[]{Types.VARBINARY}; } @Override public Class<String> returnedClass() { return String.class; } @Override public boolean equals(Object x, Object y) { return Objects.equals(x, y); } @Override public int hashCode(Object x) { return x != null ? x.hashCode() : 0; } @Override public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws SQLException { byte[] encryptedBytes = rs.getBytes(names[0]); if (encryptedBytes == null) { return null; } try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] key = digest.digest(secret.getBytes(StandardCharsets.UTF_8)); Cipher cipher = Cipher.getInstance("AES"); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES")); byte[] decryptedBytes = cipher.doFinal(encryptedBytes); return new String(decryptedBytes, StandardCharsets.UTF_8).trim().replace("\0", ""); } catch (Exception e) { throw new HibernateException("解密失败", e); } } @Override public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) throws SQLException { if (value == null) { st.setNull(index, Types.VARBINARY); return; } String strValue = (String) value; try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] key = digest.digest(secret.getBytes(StandardCharsets.UTF_8)); Cipher cipher = Cipher.getInstance("AES"); cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES")); byte[] encryptedBytes = cipher.doFinal(strValue.getBytes(StandardCharsets.UTF_8)); st.setBytes(index, encryptedBytes); } catch (Exception e) { throw new HibernateException("加密失败", e); } } @Override public Object deepCopy(Object value) { return value == null ? null : new String((String) value); } @Override public boolean isMutable() { return false; } @Override public Serializable disassemble(Object value) { return (Serializable) deepCopy(value); } @Override public Object assemble(Serializable cached, Object owner) { return deepCopy(cached); } @Override public Object replace(Object original, Object target, Object owner) { return deepCopy(original); } }
- 在实体类字段上用
@Type注解指定自定义类型:
@Type(type = "com.yourpackage.EncryptedStringType") @Column(columnDefinition = "VARBINARY") private String name;
注意事项
- 两种方案都是在Java端完成加解密,和原数据库端函数逻辑对齐(SHA256哈希密钥、AES加解密、移除末尾空字符)。
- 如果需要在数据库端直接查询加密字段,需先在Java端对查询值加密,再传入SQL;若坚持用数据库端函数,可通过Hibernate拦截器动态拼接SQL,但实现复杂度较高,不推荐。
内容的提问来源于stack exchange,提问作者Joel Quenard Martinez
相关产品推荐
相关产品推荐

