Java使用JCE做AES加解密时密文乱码、结果每次不同如何解决
AES加解密密文存储问题修复方案
问题根因
你遇到的两个问题本质是对AES加密输出特性、字符编码规则理解偏差导致的,具体说明:
- 密文出现怪异乱码:AES加密输出的是无编码规则的随机二进制字节流,不属于任何合法字符集(UTF-8、GBK等)的可打印字符序列。直接调用
new String(encryptedBytes)是强行用系统默认字符集转译随机二进制,必然出现不可识别符号,甚至会因为字符集不兼容丢失部分字节,后续再把这个乱码字符串转回字节数组时会损坏密文,导致解密失败。 - 每次运行生成不同密文:你当前代码每次启动都会调用
KeyGenerator.generateKey()随机生成全新的AES密钥,密钥不同密文自然不同;另外如果使用带随机初始化向量(IV)的安全加密模式(如CBC、GCM),即使密钥、明文完全相同,每次加密生成的密文也会不同,这是正常的安全设计,只要解密时拿到正确的密钥、对应IV就可以正常解密,不影响使用。
密文存储选型
密文存储二选一即可,禁止直接把二进制密文强转String存储:
- 二进制存储:数据库对应字段使用
BLOB/VARBINARY/BINARY类型,直接存储加密得到的原始byte[]数组,没有额外编码开销,读写效率最高。 - 文本存储:如果业务场景必须用
VARCHAR/TEXT类文本字段存储,需要先把二进制密文通过Base64或Hex编码转为全可打印的常规字符串,再入库;读取时先把字符串解码回原始byte[]数组,再执行解密操作。
代码修复要点
你当前的实现还有几个兼容性、安全性问题需要修正:
- 初始化Cipher时必须显式指定完整的「算法/工作模式/填充方式」,不能只写
AES,否则不同JDK版本/厂商会使用不同默认值,加解密结果不兼容。推荐使用安全性更高的AES/CBC/PKCS5Padding或AES/GCM/NoPadding,不要用默认的ECB模式。 - 密钥不能每次运行随机生成,需要在首次生成后通过安全介质持久化存储(比如配置中心、密钥管理服务),后续加解密都使用同一把密钥,否则历史密文永远无法解密。
- 使用CBC/GCM模式时,每次加密需要生成随机IV,IV不需要保密,直接拼在密文前面/分字段和密文一起存储即可,解密时先从密文中拆分出IV,再初始化解密器。
- 字符串和字节数组互转时必须显式指定字符集(推荐UTF-8),避免不同系统默认编码不一致导致的乱码问题。
修正后参考代码
import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import java.security.SecureRandom; import java.util.Base64; public class AesDemo { // 实际使用时从安全配置中读取持久化的密钥,不要每次运行重新生成 private static SecretKey getPersistentKey() throws Exception { // 首次生成后可以把密钥编码后存到安全配置中,这里为示例简化 KeyGenerator gen = KeyGenerator.getInstance("AES"); gen.init(128); // AES推荐使用128/256位密钥 return gen.generateKey(); } // 加密方法,返回Base64编码的密文,可直接存入VARCHAR类型字段 public static String encrypt(String plainText, SecretKey key) throws Exception { Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); // 生成随机IV byte[] iv = new byte[cipher.getBlockSize()]; new SecureRandom().nextBytes(iv); IvParameterSpec ivSpec = new IvParameterSpec(iv); cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec); byte[] encryptedBytes = cipher.doFinal(plainText.getBytes("UTF-8")); // 把IV和密文拼接后转Base64返回,无需单独存储IV byte[] combined = new byte[iv.length + encryptedBytes.length]; System.arraycopy(iv, 0, combined, 0, iv.length); System.arraycopy(encryptedBytes, 0, combined, iv.length, encryptedBytes.length); return Base64.getEncoder().encodeToString(combined); } // 解密方法,传入Base64编码的密文 public static String decrypt(String cipherTextBase64, SecretKey key) throws Exception { byte[] combined = Base64.getDecoder().decode(cipherTextBase64); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); // 拆分出IV和真实密文 byte[] iv = new byte[cipher.getBlockSize()]; byte[] encryptedBytes = new byte[combined.length - iv.length]; System.arraycopy(combined, 0, iv, 0, iv.length); System.arraycopy(combined, iv.length, encryptedBytes, 0, encryptedBytes.length); IvParameterSpec ivSpec = new IvParameterSpec(iv); cipher.init(Cipher.DECRYPT_MODE, key, ivSpec); byte[] plainBytes = cipher.doFinal(encryptedBytes); return new String(plainBytes, "UTF-8"); } public static void main(String[] args) throws Exception { SecretKey key = getPersistentKey(); String text1 = "welcome back"; String text2 = "hello guys"; String enc1 = encrypt(text1, key); String enc2 = encrypt(text2, key); // 输出的密文为常规Base64字符,无乱码 System.out.println(enc1); System.out.println(enc2); System.out.println(decrypt(enc1, key)); System.out.println(decrypt(enc2, key)); } }
补充说明:如果你的业务场景是存储用户登录密码,不要使用AES这类可逆加密算法,应当使用BCrypt、Argon2等带盐的慢哈希算法,避免密钥泄露后所有用户密码被还原。
内容的提问来源于stack exchange,提问作者bem
相关产品推荐
相关产品推荐

