Quarkus中结合Hibernate @ColumnTransformer实现数据加密:如何从application.properties读取密钥?
我之前也碰到过一模一样的困境——既要用@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

