You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Quarkus中结合Hibernate @ColumnTransformer实现数据加密:如何从application.properties读取密钥?

解决方案:利用PostgreSQL会话变量+自定义函数实现动态密钥的@ColumnTransformer加解密

我之前也碰到过一模一样的困境——既要用@ColumnTransformer支持LIKE查询,又不想硬编码密钥,还不能用@AttributeConverter。下面这个方案亲测有效,结合PostgreSQL的会话变量、自定义加密函数和Quarkus的拦截器,完美解决所有问题:

1. 先准备PostgreSQL环境

首先确保你的PostgreSQL实例已经启用了pgcrypto扩展(PostgreSQL自带的加密工具),执行以下SQL:

CREATE EXTENSION IF NOT EXISTS pgcrypto;

然后创建两个自定义函数,从会话变量中读取密钥来做加解密,这样就不用把密钥硬编码到SQL里了:

-- 加密函数:接收明文,用会话变量中的密钥加密
CREATE OR REPLACE FUNCTION encrypt_data(data text) RETURNS text AS $$
BEGIN
  RETURN pgp_sym_encrypt(data, current_setting('app.encryption_key'));
END;
$$ LANGUAGE plpgsql;

-- 解密函数:接收密文,用会话变量中的密钥解密
CREATE OR REPLACE FUNCTION decrypt_data(data text) RETURNS text AS $$
BEGIN
  RETURN pgp_sym_decrypt(data::bytea, current_setting('app.encryption_key'));
END;
$$ LANGUAGE plpgsql;

2. 在Quarkus中动态注入密钥到数据库会话

我们需要在每个数据库会话(比如请求开始时)把application.properties里的密钥设置到PostgreSQL的会话变量中,请求结束后再重置。用Quarkus的CDI拦截器就能轻松实现:

import jakarta.annotation.Priority;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.eclipse.microprofile.config.inject.ConfigProperty;

@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class EncryptionKeySessionInterceptor {

    @PersistenceContext
    private EntityManager em;

    // 从application.properties读取密钥
    @ConfigProperty(name = "encryption.key")
    private String encryptionKey;

    @AroundInvoke
    public Object setSessionEncryptionKey(InvocationContext ctx) throws Exception {
        // 设置会话级别的密钥变量
        em.createNativeQuery("SET app.encryption_key = :key")
          .setParameter("key", encryptionKey)
          .executeUpdate();
        try {
            // 执行业务逻辑
            return ctx.proceed();
        } finally {
            // 请求结束后重置变量,避免密钥泄露
            em.createNativeQuery("RESET app.encryption_key").executeUpdate();
        }
    }
}

然后给需要加解密的业务方法(或者全局控制器)加上拦截器注解:

@Interceptors(EncryptionKeySessionInterceptor.class)
public class UserService {
    // 你的业务方法,比如保存/查询用户
}

如果想全局生效,可以用CDI扩展或者Quarkus的@GlobalInterceptor(需要Quarkus 2.1+)。

3. 实体类中配置@ColumnTransformer

现在实体类里的@ColumnTransformer就可以直接调用我们的自定义函数了,不用硬编码密钥:

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import org.hibernate.annotations.ColumnTransformer;

@Entity
public class User {

    @Id
    private Long id;

    @Column(name = "encrypted_email")
    @ColumnTransformer(
        read = "decrypt_data(encrypted_email)", // 读取时解密
        write = "encrypt_data(?)"              // 写入时加密
    )
    private String email;

    // getters and setters
}

4. 支持LIKE查询

现在你可以正常写LIKE查询了,不管是JPQL还是原生SQL都没问题:

// JPQL示例:查询邮箱包含example.com的用户
List<User> users = em.createQuery(
    "SELECT u FROM User u WHERE decrypt_data(u.email) LIKE :pattern", 
    User.class
)
.setParameter("pattern", "%example.com%")
.getResultList();

// 原生SQL示例更直接
List<User> users = em.createNativeQuery(
    "SELECT * FROM user WHERE decrypt_data(encrypted_email) LIKE ?",
    User.class
)
.setParameter(1, "%example.com%")
.getResultList();

额外安全建议

  • 不要把明文密钥直接放在application.properties里,用Quarkus的配置加密功能(比如quarkus.config.secrets集成Vault或者本地加密文件)来保护密钥。
  • 会话变量只会在当前数据库连接会话中生效,请求结束后自动重置,不用担心密钥泄露到其他会话。

为什么之前的反射方案无效?

Hibernate在启动时会一次性解析所有实体的元数据并缓存,之后修改注解的值根本不会被Hibernate识别,所以动态修改注解的思路从根源上就行不通。而上面的方案绕开了@ColumnTransformer需要常量的限制,把密钥的传递放到了数据库会话层面,从根本上解决了问题。

内容的提问来源于stack exchange,提问作者Pasquale Forte

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 21:47:39