使用javax.crypto加密AES令牌通过HTTP头传递时遇填充异常的解决
解决Java 1.7 AES加密令牌传递的BadPaddingException问题
嘿,我一眼就瞅出你这个问题的症结了——你在接收端处理加密数据时,犯了个二进制数据处理的典型错误:把加密后的随机二进制字节转成普通String再转回字节数组。加密后的字节是完全无规则的二进制流,根本不符合UTF-8这类字符编码的规则,当你用new String(Base64.getDecoder().decode(header))的时候,JVM会把那些识别不了的字节直接替换成默认的占位符(就是你看到的字节63,对应字符?),这直接把原始加密数据给改坏了,解密时自然过不了填充校验,报BadPaddingException。
正确的处理流程(完全兼容Java 1.7)
Java 1.7还没内置java.util.Base64,所以咱们用JDK自带的javax.xml.bind.DatatypeConverter来处理Base64编解码(这个类在1.7里肯定能用),全程记住一个原则:只有Base64编码后的字符串是用来传输的文本,加密后的二进制数据绝对不能转成普通String。
发送端:加密生成可传输的令牌
import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import javax.xml.bind.DatatypeConverter; public class TokenSender { public static void main(String[] args) throws Exception { String apiKeyHex = "02E30E6BE24BF1EA"; // 你的十六进制格式密钥 String applicationId = "your-app-id-123"; // 要识别的请求方ID // 重点:把十六进制密钥转成字节数组,而不是直接getBytes() byte[] rawKeyBytes = DatatypeConverter.parseHexBinary(apiKeyHex); // AES要求密钥长度是16/24/32字节,这里把8字节的密钥补成16字节(AES-128) byte[] aesKeyBytes = new byte[16]; System.arraycopy(rawKeyBytes, 0, aesKeyBytes, 0, rawKeyBytes.length); SecretKeySpec aesKey = new SecretKeySpec(aesKeyBytes, "AES"); // 明确指定模式和填充,别依赖JVM默认值,避免跨环境坑 Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, aesKey); // 加密applicationId,指定UTF-8编码避免乱码 byte[] encryptedData = cipher.doFinal(applicationId.getBytes("UTF-8")); // 转成Base64字符串,这才是能安全放到HTTP头里的内容 String token = DatatypeConverter.printBase64Binary(encryptedData); System.out.println("生成的授权令牌:" + token); } }
接收端:解析令牌并解密
import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import javax.xml.bind.DatatypeConverter; public class TokenReceiver { public static void main(String[] args) throws Exception { String apiKeyHex = "02E30E6BE24BF1EA"; String receivedToken = "从HTTP请求头里拿到的Base64字符串"; // 和发送端一样处理密钥 byte[] rawKeyBytes = DatatypeConverter.parseHexBinary(apiKeyHex); byte[] aesKeyBytes = new byte[16]; System.arraycopy(rawKeyBytes, 0, aesKeyBytes, 0, rawKeyBytes.length); SecretKeySpec aesKey = new SecretKeySpec(aesKeyBytes, "AES"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, aesKey); // 直接把Base64字符串转成字节数组,绝对不要转成String! byte[] encryptedData = DatatypeConverter.parseBase64Binary(receivedToken); // 解密后转成原始的applicationId,同样指定UTF-8 byte[] decryptedBytes = cipher.doFinal(encryptedData); String applicationId = new String(decryptedBytes, "UTF-8"); System.out.println("解密得到的请求方ID:" + applicationId); } }
几个必须纠正的关键点
- 别碰二进制转String的操作:加密后的字节是纯二进制,不是文本,只有Base64编码后才是合法的可传输文本,直接在字节数组和Base64字符串之间转换就好,跳过中间的String转字节步骤。
- 密钥要正确转换:你的API密钥是十六进制格式的,不能直接用
getBytes()拿字节,得用parseHexBinary()把十六进制字符串转成真实的字节数组,不然密钥完全不对,解密肯定失败。 - 明确AES的模式和填充:原来的
Cipher.getInstance("AES")会用JVM默认的模式和填充,不同环境可能不一样,显式写AES/ECB/PKCS5Padding能避免兼容性问题。 - AES密钥长度合规:AES只认16、24、32字节的密钥,你的原始密钥是8字节,所以得补成16字节(示例里用0填充),要是追求更安全,可以用PBKDF2这类密钥派生函数生成合规长度的密钥。
额外的安全小提醒
- ECB模式别长期用:ECB模式是确定性的,相同明文会生成相同密文,很容易被破解。要是能改,建议换成CBC或者GCM模式(需要加IV向量,增加随机性),Java 1.7完全支持这些模式。
- 密钥别硬编码:把密钥写在代码里太危险了,最好从环境变量或者加密配置文件里读取。
内容的提问来源于stack exchange,提问作者Andrea Grimandi
相关产品推荐
相关产品推荐

