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

如何解决Spring Boot中解密时的IllegalBlockSizeException异常?

Spring Boot JPA AttributeConverter 加密解密异常排查

问题场景

在Spring Boot应用中使用AttributeConverter实现实体字段的持久化加密、查询解密,通过@Convert(converter = DataEncryptionConverter.class)标记目标字段。手动调用转换器的加密/解密方法、手动调用仓库存取值均正常,但执行部分JPA仓库方法(如User findByPrimaryEmail(String primaryEmail))时抛出IllegalBlockSizeException。

转换器代码实现

@Converter
@NoArgsConstructor
public class DataEncryptionConverter implements AttributeConverter<Object, String> {

    //    @Value("${encryption.key}")
    private String encryptionKey = "secret-test-key1";
    private final byte[] iv = new byte[16];
    private final String encryptionCipher = "AES/CBC/PKCS5Padding";

    private Key key;
    private Cipher cipher;

    private Key getKey() {
        if (key == null) {
            key = new SecretKeySpec(encryptionKey.getBytes(), "AES");
        }
        return key;
    }

    private Cipher getCipher() throws GeneralSecurityException {
        if (cipher == null) {
            cipher = Cipher.getInstance(encryptionCipher);
        }
        return cipher;
    }

    // initilize cipher into the memory -- decides whether we want to encrypt or decrypt data with the cipher
    private void initCipher(int encryptMode) throws GeneralSecurityException {
        IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
        getCipher().init(encryptMode, getKey(), ivParameterSpec);
    }

    @SneakyThrows
    @Override
    public String convertToDatabaseColumn(Object attribute) {
        if (attribute == null) {
            return null;
        }
        initCipher(Cipher.ENCRYPT_MODE);
        byte[] bytes = SerializationUtils.serialize(attribute);
        return Base64.getEncoder().encodeToString(getCipher().doFinal(bytes));
    }

    @SneakyThrows
    @Override
    public Object convertToEntityAttribute(String dbData) {
        if (dbData == null) {
            return null;
        }
        initCipher(Cipher.DECRYPT_MODE);
        byte[] bytes = getCipher().doFinal(Base64.getDecoder().decode(dbData));
        return SerializationUtils.deserialize(bytes);
    }
}

抛出的异常

Apr 12, 2023 6:49:07 PM org.apache.catalina.core.StandardWrapperValve invoke
SEVERE: Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
java.lang.reflect.UndeclaredThrowableException
    at com.sun.proxy.$Proxy201.getSingleResult(Unknown Source)
    at org.springframework.data.jpa.repository.query.JpaQueryExecution$SingleEntityExecution.doExecute(JpaQueryExecution.java:196)
    at org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:88)
    at org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:155)
    at org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:143)
    

Caused by: javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher
    at java.base/com.sun.crypto.provider.CipherCore.prepareInputBuffer(CipherCore.java:1005)
    at java.base/com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:848)
    at java.base/com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446)
    at java.base/javax.crypto.Cipher.doFinal(Cipher.java:2202)
    at com.tse.core_application.configuration.DataEncryptionConverter.convertToDatabaseColumn(DataEncryptionConverter.java:70)
    at com.tse.core_application.configuration.DataEncryptionConverter.convertToDatabaseColumn(DataEncryptionConverter.java:22)

问题分析与解决方案

核心问题

异常栈显示在convertToDatabaseColumn方法中触发了解密相关的异常,本质是JPA执行查询时,把传入的明文查询参数当成密文,调用加密方法处理——@Convert注解会同时作用于实体字段的存取值和查询参数,导致明文参数被二次加密,最终查询逻辑混乱并触发异常。

另外当前转换器存在两个关键问题:

  1. Cipher实例线程不安全,作为成员变量复用会导致多线程状态混乱
  2. 硬编码全0的IV,不符合CBC模式加密的安全规范,且存在安全风险

修复步骤

1. 解决查询参数转换异常

如果要基于加密字段做查询,不能依赖@Convert自动处理参数,需手动对查询参数加密后再传入仓库:

// 查询前手动加密参数
String encryptedEmail = dataEncryptionConverter.convertToDatabaseColumn(primaryEmail);
User user = userRepository.findByPrimaryEmail(encryptedEmail);

若想避免手动加密,可自定义JPA查询并在语句中处理参数加密,但这种方式依赖数据库函数,通用性较差。

2. 修复线程安全问题

将Cipher改为局部变量,每次加密/解密时创建新实例,避免多线程冲突:

private Cipher getCipher(int encryptMode) throws GeneralSecurityException {
    Cipher cipher = Cipher.getInstance(encryptionCipher);
    IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
    cipher.init(encryptMode, getKey(), ivParameterSpec);
    return cipher;
}

同时修改转换器的核心方法:

@SneakyThrows
@Override
public String convertToDatabaseColumn(Object attribute) {
    if (attribute == null) {
        return null;
    }
    Cipher cipher = getCipher(Cipher.ENCRYPT_MODE);
    byte[] bytes = SerializationUtils.serialize(attribute);
    return Base64.getEncoder().encodeToString(cipher.doFinal(bytes));
}

@SneakyThrows
@Override
public Object convertToEntityAttribute(String dbData) {
    if (dbData == null) {
        return null;
    }
    Cipher cipher = getCipher(Cipher.DECRYPT_MODE);
    byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(dbData));
    return SerializationUtils.deserialize(bytes);
}

3. 优化加密安全性

  • 生成随机IV并与密文一起存储:CBC模式要求IV随机唯一,不能固定为0。修改加密解密逻辑,将IV和密文拼接后编码:
@SneakyThrows
@Override
public String convertToDatabaseColumn(Object attribute) {
    if (attribute == null) {
        return null;
    }
    // 生成随机IV
    SecureRandom secureRandom = new SecureRandom();
    byte[] iv = new byte[16];
    secureRandom.nextBytes(iv);
    
    Cipher cipher = Cipher.getInstance(encryptionCipher);
    cipher.init(Cipher.ENCRYPT_MODE, getKey(), new IvParameterSpec(iv));
    
    byte[] bytes = SerializationUtils.serialize(attribute);
    byte[] encryptedBytes = cipher.doFinal(bytes);
    
    // 拼接IV和密文(IV在前)
    byte[] result = new byte[iv.length + encryptedBytes.length];
    System.arraycopy(iv, 0, result, 0, iv.length);
    System.arraycopy(encryptedBytes, 0, result, iv.length, encryptedBytes.length);
    
    return Base64.getEncoder().encodeToString(result);
}

@SneakyThrows
@Override
public Object convertToEntityAttribute(String dbData) {
    if (dbData == null) {
        return null;
    }
    byte[] data = Base64.getDecoder().decode(dbData);
    
    // 拆分IV和密文
    byte[] iv = new byte[16];
    byte[] encryptedBytes = new byte[data.length - iv.length];
    System.arraycopy(data, 0, iv, 0, iv.length);
    System.arraycopy(data, iv.length, encryptedBytes, 0, encryptedBytes.length);
    
    Cipher cipher = Cipher.getInstance(encryptionCipher);
    cipher.init(Cipher.DECRYPT_MODE, getKey(), new IvParameterSpec(iv));
    
    byte[] bytes = cipher.doFinal(encryptedBytes);
    return SerializationUtils.deserialize(bytes);
}
  • 使用符合AES规范的密钥:AES要求密钥长度为16/24/32字节(对应AES-128/192/256),当前密钥secret-test-key1仅13字节,需替换为符合长度的密钥,或通过PBKDF2等密钥派生函数生成标准长度密钥。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 09:08:09