【问题标题】:Field Level Encryption with Spring Data JPA and JPA EntityListener使用 Spring Data JPA 和 JPA EntityListener 进行字段级加密
【发布时间】: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


【解决方案1】:

我知道提出的问题已经很老了。但我遇到了同样的问题并且要克服,您可以使用 PreLoadEventListener。在您的decryptFields 中使用@PostLoad 而不是使用@PreLoad

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-09-13
    • 2014-12-22
    • 1970-01-01
    • 1970-01-01
    • 2017-07-09
    • 2018-05-25
    • 2015-05-25
    • 2017-11-29
    相关资源
    最近更新 更多