Java Cipher调用doFinal时抛出IllegalBlockSizeException问题求助
嘿,我来帮你搞定这个加密解密的头疼问题!你遇到的IllegalBlockSizeException提示“Input length must be multiple of 16 when decrypting with padded cipher”,这在对称加密(比如AES)里是非常常见的问题,咱们一步步拆解原因,然后给出可行的解决方案。
为什么会出现这个异常?
你用的应该是块加密算法(比如AES),这类算法的默认模式通常是带填充的(比如PKCS5Padding)。简单来说:
- 加密时,算法会自动把原始数据填充到16字节的整数倍(AES的块大小是16字节)
- 解密时,要求输入的密文必须严格是16字节的整数倍,否则就会抛出这个异常
常见的触发原因有这几个:
- 密文在传输/转换过程中被截断、篡改,或者用了错误的编码(比如直接把字节数组转成普通字符串,导致字节丢失)
- 你的单例类里把
Cipher作为成员变量复用,而Cipher是有状态的,重复使用会导致状态混乱 - 加密和解密时用的算法模式/填充规则不一致
- 密钥长度不符合算法要求(不过这个通常会抛出另一种异常,先放在后面排查)
针对性解决方案
1. 修复单例类的Cipher使用方式
绝对不要把Cipher作为单例类的成员变量! 因为Cipher不是线程安全的,而且每次调用doFinal()后状态会变化,复用很容易导致解密时的块大小错误。正确的做法是每次加密/解密时重新初始化Cipher。
修改你的SecurityHandler示例(我帮你补全了完整的加密解密逻辑,包括CBC模式的IV处理):
import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.Base64; public class SecurityHandler { private static SecurityHandler instance; private final String securityKey; // 私有构造方法,初始化密钥 private SecurityHandler() { // 注意:密钥的UTF-8编码长度必须是16/24/32字节(对应AES 128/192/256位) securityKey = "adnanyc83z43h1jbncxgsdfgs..."; } // 单例获取方法 public static synchronized SecurityHandler getInstance() { if (instance == null) { instance = new SecurityHandler(); } return instance; } // 加密方法:返回包含IV的字节数组(解密需要用同一个IV) public byte[] encrypt(String plainText) throws Exception { SecretKeySpec keySpec = new SecretKeySpec(securityKey.getBytes(StandardCharsets.UTF_8), "AES"); // 明确指定算法模式和填充,不要依赖JVM默认值 Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); // CBC模式需要随机IV,这里生成一个和块大小一致的IV byte[] iv = new byte[cipher.getBlockSize()]; new SecureRandom().nextBytes(iv); IvParameterSpec ivSpec = new IvParameterSpec(iv); cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); // 把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 result; } // 解密方法:传入包含IV的加密数据 public String decrypt(byte[] encryptedData) throws Exception { SecretKeySpec keySpec = new SecretKeySpec(securityKey.getBytes(StandardCharsets.UTF_8), "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); // 先从加密数据中提取IV byte[] iv = new byte[cipher.getBlockSize()]; System.arraycopy(encryptedData, 0, iv, 0, iv.length); IvParameterSpec ivSpec = new IvParameterSpec(iv); // 提取真正的密文部分 byte[] encryptedBytes = new byte[encryptedData.length - iv.length]; System.arraycopy(encryptedData, iv.length, encryptedBytes, 0, encryptedBytes.length); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); byte[] decryptedBytes = cipher.doFinal(encryptedBytes); return new String(decryptedBytes, StandardCharsets.UTF_8); } // 可选:如果需要把密文转成字符串传输,用Base64编码(避免字节丢失) public String encryptToBase64(String plainText) throws Exception { byte[] encryptedData = encrypt(plainText); return Base64.getEncoder().encodeToString(encryptedData); } // 可选:从Base64字符串解密 public String decryptFromBase64(String encryptedBase64) throws Exception { byte[] encryptedData = Base64.getDecoder().decode(encryptedBase64); return decrypt(encryptedData); } }
2. 避免密文的错误编码
如果需要把密文转换成字符串(比如通过HTTP接口传输),绝对不要直接用new String(encryptedBytes)!因为加密后的字节数组可能包含非UTF-8兼容的字节,直接转字符串会导致数据丢失或损坏,进而解密时出现块大小错误。
正确的做法是用Base64编码把字节数组转成字符串,就像上面示例里的encryptToBase64和decryptFromBase64方法那样。
3. 确保加密和解密的模式/填充完全一致
一定要明确指定Cipher的完整算法字符串,比如AES/CBC/PKCS5Padding,不要只写AES。因为不同JVM的默认模式可能不一样(比如有些默认是ECB,有些是CBC),一旦加密和解密的模式/填充不匹配,就会抛出块大小错误或者解密失败。
4. 检查密钥长度
AES要求密钥长度必须是16、24或32字节(对应128、192、256位)。你要确认你的securityKey字符串用UTF-8编码后的字节长度符合这个要求,如果不符合,初始化SecretKeySpec时会有问题(虽然报错可能不是当前这个,但也是潜在隐患)。
额外提醒
Cipher不是线程安全的:所以绝对不能把它作为单例成员变量,多个线程同时调用会导致状态混乱,出现各种奇怪的异常。- CBC模式必须使用IV:IV应该是随机生成的,而且解密时必须用和加密时相同的IV,所以要把IV和密文一起保存或传输。
- 不要硬编码密钥:示例里是为了演示,生产环境绝对不能硬编码密钥,要从安全的配置中心或密钥管理服务获取。
内容的提问来源于stack exchange,提问作者Kjosu

