【发布时间】:2014-10-10 17:46:13
【问题描述】:
我正在尝试在插入/更新之前加密域实体上的一些字段,并在选择显示在 UI 中时对其进行解密。
我正在使用带有 Hibernate 和 EntityListener 的 Spring Data JPA 存储库,它在 @PostLoad 生命周期事件期间解密并在 @PrePersist 和 @PreUpdate 期间加密。我遇到的问题是,一旦将记录从数据库加载到 PersistenceContext 中,侦听器就会解密数据,这使 EntityManager 认为实体已被更改,从而触发更新,因此再次触发 @PreUpdate 加密。关于如何处理这个问题的任何建议?
- Spring 4.0.4.RELEASE
- Spring Data JPA 1.5.2.RELEASE
- 休眠 4.2.14.Final
是否有从 JPA 存储库返回分离实体的简单方法?
实体类
@Entity
@Table(name="cases")
@EntityListeners(EncryptionListener.class)
public class MyCase implements Serializable, EncryptionEntity {
private static final Logger logger = LoggerFactory.getLogger(MyCase.class);
private static final long serialVersionUID = 1L;
private String caseNumber;
private byte[] secretProperty;
private byte[] iv;
@Id
@Column(name="case_number")
public String getCaseNumber() {
return caseNumber;
}
public void setCaseNumber(String caseNumber) {
this.caseNumber = caseNumber;
}
@Column(name="secret_property")
public byte[] getSecretProperty() {
return secretProperty;
}
public void setSecretProperty(byte[] secretProperty) {
this.secretProperty = secretProperty;
}
@Column
public byte[] getIv() {
return iv;
}
public void setIv(byte[] iv) {
this.iv = iv;
}
@Override
@Transient
public byte[] getInitializationVector() {
return this.iv;
}
@Override
public void setInitializationVector(byte[] iv) {
this.setIv(iv);
}
}
加密实体接口
public interface EncryptionEntity {
public byte[] getInitializationVector();
public void setInitializationVector(byte[] iv);
}
Spring Data JPA 存储库
public interface MyCaseRepository extends JpaRepository<MyCase, String> {
}
MyCaseService 接口
public interface MyCaseService {
public MyCase findOne(String caseNumber);
public MyCase save(MyCase case);
}
MyCaseService 实施
public class MyCaseServiceImpl implements MyCaseService {
private static final Logger logger = LoggerFactory.getLogger(MyCaseServiceImpl.class);
@Autowired
private MyCaseRepository repos;
@Override
public MyCase findOne(String caseNumber) {
return repos.findOne(caseNumber);
}
@Transactional(readOnly=false)
public MyCase save(MyCase case) {
return repos.save(case);
}
}
加密 JPA 侦听器类
@Component
public class EncryptionListener {
private static final Logger logger = LoggerFactory.getLogger(EncryptionListener.class);
private static EncryptionUtils encryptionUtils;
private static SecureRandom secureRandom;
private static Map<Class<? extends EncryptionEntity>,
List<EncryptionEntityProperty>> propertiesToEncrypt;
@Autowired
public void setCrypto(EncryptionUtils encryptionUtils){
EncryptionListener.encryptionUtils = encryptionUtils;
}
@Autowired
public void setSecureRandom(SecureRandom secureRandom){
EncryptionListener.secureRandom = secureRandom;
}
public EncryptionListener(){
if (propertiesToEncrypt == null){
propertiesToEncrypt = new HashMap<Class<? extends EncryptionEntity>, List<EncryptionEntityProperty>>();
//MY CASE
List<EncryptionEntityProperty> propertyList = new ArrayList<EncryptionEntityProperty>();
propertyList.add(new EncryptionEntityProperty(MyCase.class, "secretProperty", byte[].class));
propertiesToEncrypt.put(MyCase.class, propertyList);
}
}
@PrePersist
public void prePersistEncryption(EncryptionEntity entity){
logger.debug("PRE-PERSIST");
encryptFields(entity);
}
@PreUpdate
public void preUpdateEncryption(EncryptionEntity entity){
logger.debug("PRE-UPDATE");
encryptFields(entity);
}
public void encryptFields(EncryptionEntity entity){
byte[] iv = new byte[16];
secureRandom.nextBytes(iv);
encryptionUtils.setIv(iv);
entity.setInitializationVector(iv);
logger.debug("Encrypting " + entity);
Class<? extends EncryptionEntity> entityClass = entity.getClass();
List<EncryptionEntityProperty> properties = propertiesToEncrypt.get(entityClass);
for (EncryptionEntityProperty property : properties){
logger.debug("Encrypting '{}' field of {}", property.getName(), entityClass.getSimpleName());
if (property.isEncryptedWithIv() == false){
logger.debug("Encrypting '{}' without IV.", property.getName());
}
try {
byte[] bytesToEncrypt = (byte[]) property.getGetter().invoke(entity, (Object[]) null);
if (bytesToEncrypt == null || bytesToEncrypt.length == 0){
continue;
}
byte[] encrypted = encryptionUtils.encrypt(bytesToEncrypt, property.isEncryptedWithIv());
property.getSetter().invoke(entity, new Object[]{encrypted});
} catch (Exception e){
logger.error("Error while encrypting '{}' property of {}: " + e.getMessage(), property.getName(), entityClass.toString());
e.printStackTrace();
}
}
}
@PostLoad
public void decryptFields(EncryptionEntity entity){
logger.debug("POST-LOAD");
logger.debug("Decrypting " + entity);
Class<? extends EncryptionEntity> entityClass = entity.getClass();
byte[] iv = entity.getInitializationVector();
List<EncryptionEntityProperty> properties = propertiesToEncrypt.get(entityClass);
for (EncryptionEntityProperty property : properties){
try {
byte[] value = (byte[]) property.getGetter().invoke(entity, (Object[]) null);
if (value == null || value.length == 0){
logger.debug("Ignoring blank field {} of {}", property.getName(), entityClass.getSimpleName());
continue;
}
logger.debug("Decrypting '{}' field of {}", property.getName(), entityClass.getSimpleName());
if (property.isEncryptedWithIv() == false){
logger.debug("Decrypting '{}' without IV.", property.getName());
}
byte[] decrypted = encryptionUtils.decrypt(value, iv, property.isEncryptedWithIv());
property.getSetter().invoke(entity, new Object[]{decrypted});
} catch (Exception e){
logger.error("Error while decrypting '{}' property of {}", property.getName(), entityClass.toString());
e.printStackTrace();
}
}
}
}
【问题讨论】:
-
在调用
findOne(String caseNumber)方法时是否调用了@PostLoad? -
@wypieprz,是的,@PostLoad 方法是由 findOne 方法触发的。而@PostUpdate在secretProperty被解密后立即触发。
-
在持久化上下文之外执行
MyCaseServiceImpl.findOne方法怎么样?然后实体应作为分离的实体返回。 -
Spring 创建一个使用 EntityManager 的 MyCaseRepository 代理。不知道如何避免持久性上下文。也许如果我没有用
@Transactional注释 findOne 方法?我已经转向另一种方法,使用我的 EncryptionListener 作为注入组件来加密/解密。但它肯定不如我希望的透明加密/解密那么优雅。 -
我猜
@Transactional(propagation = Propagation.NOT_SUPPORTED)会强制分离。
标签: java spring hibernate jpa encryption