Java加密体系全解析:从JCA/JCE到AES/RSA与国密算法落地

发布时间:2026/9/28 14:58:39
Java加密体系全解析:从JCA/JCE到AES/RSA与国密算法落地
作为一个在Java圈子里写了不少年代码的老兵我几乎每隔一段时间就会被问到加密相关的问题要么是面试官追着问AES和RSA的区别要么是安全评审要求把业务系统里的MD5全部换掉要么是项目经理甩过来一句“新系统要用国密算法你评估下工作量”。Java里加密相关的API看起来散落在java.security、javax.crypto这些包里不少同学一打开就懵但真把它拆开看逻辑并不复杂。这篇东西我会从JCA/JCE底层结构讲起把对称加密、非对称加密、哈希摘要、HMAC、国密SM系列在Java里的落地方式一条线梳理清楚每段都附上能直接跑起来的代码和我在生产环境里踩过的坑。适合刚接触加密的初级工程师建立完整认知也适合准备Java面试或者考系统分析师的同学当复习提纲有经验的开发也可以直接跳到后面看坑。1. Java加密体系全景先搞懂JCA/JCE再说加解密很多初学者一上来就搜“AES加解密Java代码”然后复制粘贴一个工具类就完事。这种用法短期能跑但一旦遇到“换个算法”“换种填充方式”“部署到低版本JDK报Illegal key size”这种需求立刻抓瞎。所以我的建议是别急着写代码先把Java加密体系的地基摸清楚。1.1 JCA/JCE是什么为什么Java能一套API走遍各种算法JCA全称Java Cryptography ArchitectureJCE全称Java Cryptography Extension。这俩其实是设计层面上的东西JCA偏向于提供架构框架涵盖消息摘要、签名、证书这类能力JCE则是在JCA框架下专门负责加解密、密钥协商、MAC等具体能力的扩展。但日常开发中大家基本不抠边界统一理解成“JDK自带的加密框架”就行。这个架构的一个核心设计叫Provider模式。你可以把Provider理解成“算法实现厂商的插槽”SunJCE、SunRsaSign这些是JDK内置的Provider而BouncyCastle、Conscrypt这类第三方库则是后来插进来的Provider。写代码时用的是统一的Cipher.getInstance(AES/GCM/NoPadding)底层却是Provider各自的大脑——所以不管底层算法细节怎么变Java这层API永远是同一套面孔。打个比方它就像电脑上的USB接口你插U盘、插网卡、插采集卡用的都是同一个Type-A口但你插进去的东西完全不同。Java的加密API就是这个USB口Provider就是那些不同的扩展设备。这一段最大的实际意义在于如果未来某天你需要支持一个冷门算法不需要改业务代码只需要引入一个支持该算法的Provider甚至在构造Cipher时显式指定它Cipher cipher Cipher.getInstance(SM4/ECB/PKCS5Padding, BC);第二个参数“BC”就是BouncyCastle这个Provider的名字。这就是“一套API走天下”的核心价值。1.2 三类算法先分清对称、非对称和摘要Java里能遇到的加密算法说破天也就三大类。类型密钥特点典型算法典型用途速度对称加密加密解密用同一个密钥AES、DES、3DES、SM4大批量数据加密、文件加密快非对称加密公钥加密私钥解密或私钥签名公钥验签RSA、ECC、SM2密钥交换、数字签名、少量数据加密明显更慢摘要算法无密钥不可逆MD5、SHA-1、SHA-256、SM3完整性校验、密码存储、文件指纹非常快对称加密最大的问题就是密钥怎么安全地交给对方你用同一个密钥加密发出去之前得先把密钥渠道打通这在双方不预先认识的前提下很棘手。非对称加密解决了分发问题——公钥随便发私钥自己留任何人都能用公钥给你发密文只有你手里的私钥能解开。RSA的致命弱点是性能比对称加密慢一到两个数量级所以实际上不会拿RSA去顶几十KB的业务数据。于是就有了混合加密先用RSA加密一把随机的AES对称密钥再用这个AES密钥加密真正的业务数据集合了两边的优点HTTPS里的握手过程本质上就在干这件事。摘要算法跟前面两类有本质区别它没有密钥的概念而且流程不可逆只有输入相同才可能输出相同。哈希有个特性叫雪崩效应原文改一个字节摘要就面目全非所以它用来检测“数据有没有被改过”非常合适。1.3 动手之前先处理三件事编码、密钥材料和随机数我在帮同事Review代码时发现好多人不是算法选错而是在最基础的几个环节上翻车。第一是编码。Java的String在内存里是UTF-16密文是字节数组。如果你直接把byte[]扔给new String()大概率得到一堆乱码甚至可能把部分字节搞坏。加密后想要以字符串形式传输正确做法是用Base64编码。反之拿到Base64字符串后要先用Base64.getDecoder().decode()还原成字节数组再解密。第二是密钥材料。不同算法对密钥长度有硬性要求AES密钥必须是16字节、24字节或32字节。如果你用一个String当密钥请务必固定编码方式否则在不同环境里同样的字符串可能生成不同字节序列SecretKeySpec key new SecretKeySpec(keyStr.getBytes(StandardCharsets.UTF_8), AES);这里的SecretKeySpec是用来把“原始密钥字节”包装成JCE能认的SecretKey对象的拿不到现成密钥时可以用KeyGenerator来生成新的随机密钥。注意getInstance传的算法名要和密钥类型匹配AES就用AES乱传会抛InvalidKeyException。第三是随机数。加密场景里所谓的随机数必须是SecureRandom千万不能用Math.random()。早期项目里有些开发者图省事用固定种子构造SecureRandom来“复现结果”这等于直接把IV固定了存在严重的可预测性风险。正确姿势很朴素SecureRandom random new SecureRandom(); byte[] iv new byte[12]; random.nextBytes(iv);SecureRandom内部的熵源来自操作系统收集的环境噪声比如硬件中断、IO时间差等等不是数字公式硬算出来的伪随机序列这才是密码学意义上的安全随机。2. 对称加密实践AES的正确打开方式对称加密里AES是当前绝对的主流。很多教科书还在讲DES和3DES生产环境里也偶尔能翻出老古董代码但我的建议是任何新系统一律用AES起步别再碰DES。DES的56位有效密钥长度放在今天就是笑话暴力破解毫无压力3DES虽然补了密钥长度但块大小还是64位慢且笨重。AES块大小128位密钥支持128/192/256位性能和安全性平衡得很好。2.1 先弄明白工作模式、填充和IV不然密文连自己都解不开AES本身只是个“块加密算法”它每次只能处理16字节的数据块。那超过16字节的数据怎么办这就引入了工作模式Mode和填充方式Padding。工作模式决定了块与块之间怎么关联填充方式决定了最后不足16字节的尾巴怎么补齐。模式特点场景建议ECB每块独立加密相同明文产生相同密文强烈不建议用于多块数据会泄露结构CBC每块与上一块密文异或后再加密需要IV曾经的主流但需要注意填充预言攻击GCM认证加密模式同时保证机密性和完整性当前最推荐的通用选择ECB模式在面试题里出现频率极高它看似简单实际上暴露数据模式。想象一个图片加密的场景如果原图由大量重复色块构成用ECB加密后轮廓可能照样清晰可见。这是教科书级别的反例。CBC模式解决了模式泄露问题每个明文块在加密前先和前一个密文块做异或所以即使两段明文一样密文也不同。但它需要一个初始向量IV而且CBC加PKCS5Padding在某些协议实现里存在Padding Oracle攻击风险所以综合看下来GCM模式是我现在的首选。这里插一个容易混淆的知识点Java里的PKCS5Padding和PKCS7Padding经常被搞混。PKCS5标准只定义了8字节块大小的填充PKCS7才覆盖16字节块但Java官方API里写AES/CBC/PKCS5Padding已经能正常工作原因是Java的JCE实际实现的就是PKCS7逻辑只是沿用了旧名称。面试被问到的话这个细节可以说清楚。IV初始化向量的作用是在CBC和GCM模式下给第一个块一个随机的“起点”。IV不需要保密但绝对不能重复使用尤其GCM模式下IV重复会导致认证标签彻底失效。一般做法是每次加密时生成一个新的IV然后把IV拼接在密文前面一起发给对方。2.2 可直接落地的AES-GCM工具类这是我项目中一直在用的一个AES-GCM工具类核心参数都按最新的安全实践来配置import javax.crypto.Cipher; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.Base64; public class AesGcmUtil { private static final int GCM_TAG_LENGTH_BITS 128; private static final int IV_LENGTH_BYTES 12; public static String encrypt(String plainText, byte[] key) throws Exception { byte[] iv new byte[IV_LENGTH_BYTES]; new SecureRandom().nextBytes(iv); Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, AES), new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv)); byte[] encrypted cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); byte[] result new byte[iv.length encrypted.length]; System.arraycopy(iv, 0, result, 0, iv.length); System.arraycopy(encrypted, 0, result, iv.length, encrypted.length); return Base64.getEncoder().encodeToString(result); } public static String decrypt(String cipherText, byte[] key) throws Exception { byte[] all Base64.getDecoder().decode(cipherText); byte[] iv new byte[IV_LENGTH_BYTES]; System.arraycopy(all, 0, iv, 0, IV_LENGTH_BYTES); byte[] encrypted new byte[all.length - IV_LENGTH_BYTES]; System.arraycopy(all, IV_LENGTH_BYTES, encrypted, 0, encrypted.length); Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, AES), new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv)); byte[] decrypted cipher.doFinal(encrypted); return new String(decrypted, StandardCharsets.UTF_8); } }几个细节重点说一下。第一GCM模式的doFinal返回的字节数组里已经自动包含了密文和认证标签Tag。你在用update方法拿前面块的数据时得到的只是不完整的密文流最后的Tag是在doFinal时才生成并追加进去的。所以收尾工作一定要取doFinal的完整返回值很多人自己封GCM工具类时习惯性把update的结果拼起来结果发现标签总是对不上。第二GCM是认证加密模式解密时如果数据被篡改、密钥不正确、或者IV不对会直接抛javax.crypto.AEADBadTagException。这意味着你不需要额外再做一遍哈希校验GCM本身就把完整性保证做完了。代码里可以做这样的捕获try { return new String(cipher.doFinal(encrypted), StandardCharsets.UTF_8); } catch (AEADBadTagException e) { throw new SecurityException(密文被篡改或密钥不正确, e); }第三IV采用12字节是GCM推荐做法。如果IV是16字节JDK底层会多走一次GHASH计算性能略差且没什么必要12字节是NIST推荐值。就算IV被人看到也没关系它不提供机密性它只保证随机性。2.3 GCM模式下的AAD与推荐参数配置GCM模式的认证能力不只覆盖密文本身还可以附带一些“明文但需要防篡改”的信息这部分数据叫AADAdditional Authenticated Data。常见的用法是密文里存用户ID、时间戳这些非敏感字段加密时把它们作为AAD一起送进去这样如果有人篡改了用户ID或者时间戳解密时同样会报认证失败。Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv)); cipher.updateAAD(userId1024ts2024-01-01.getBytes(StandardCharsets.UTF_8)); byte[] encrypted cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));注意一个顺序问题updateAAD必须在update或者doFinal之前调用在JDK的GCM实现中一旦开始处理明文再追加AAD就会抛异常。最后给你一个我常用的生产参数表参数推荐值说明算法AES/GCM/NoPadding认证加密抗CBC填充预言攻击密钥长度256位配合32字节密钥IV长度12字节GCM标准随机值Tag长度128位16字节认证标签AES密钥如果只有16字节就是AES-12824字节是AES-19232字节是AES-256。Java 8u161之后以及所有更高版本都原生支持AES-256不需要额外下载JCE策略文件。3. 非对称加密RSA的关键并不在加密在签名和密钥交换RSA在大学教材里被反复讲一讲到非对称加密举例永远是RSA。但不能只会背公式Java里落地时有一堆具体问题要处理。3.1 RSA加解密实现从密钥生成到OAEP填充先看密钥生成KeyPairGenerator keyPairGenerator KeyPairGenerator.getInstance(RSA); keyPairGenerator.initialize(2048); KeyPair keyPair keyPairGenerator.generateKeyPair(); PublicKey publicKey keyPair.getPublic(); PrivateKey privateKey keyPair.getPrivate();生产环境密钥长度建议直接2048位起步。1024位早在2013年左右就被安全标准淘汰了3072位和4096位虽然更安全但性能和公钥长度也更大一般金融、政企场景敢用4096普通互联网系统2048足够。加解密时的核心选择是填充方式。老代码里经常看到RSA/ECB/PKCS1Padding其中“ECB”三个字极具迷惑性——RSA是单块加密根本不存在分组模式这个ECB只是JCE沿用的命名习惯千万不要被它误导去翻RSA分组的资料也不要在面试时对着这行代码说RSA用了ECB模式。PKCS1Padding也就是v1.5填充虽然能用但已经被学术界证明存在Bleichenbacher攻击等风险2003年有论文指出它的可证明安全强度不足。现代推荐使用的是OAEP填充Cipher cipher Cipher.getInstance(RSA/ECB/OAEPWithSHA-256AndMGF1Padding); cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] encrypted cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));OAEP全称Optimal Asymmetric Encryption Padding它会在明文前面拼接随机数所以同样的明文每次加密出来的密文都不同这本身也是抗攻击的一部分。这里顺便算一个数值。2048位RSA密钥对应的最大块长度是256字节。采用OAEPWithSHA-256时填充开销是2*hLen2字节SHA-256的哈希长度是32字节所以开销为66字节。于是单次加密的最大明文长度是256-66190字节。超过这个长度doFinal会抛IllegalBlockSizeException提示数据过长。3.2 明文长度限制与“RSAAES”混合方案这个190字节的硬限制让RSA根本不适合加密普通业务数据。在实际企业系统里稍微像样一点的接口报文就是几百字节到几KBRSA全程参与根本不现实。所以标准解法是混合加密随机生成一把AES密钥用这把AES密钥加密业务数据然后把AES密钥用接收方的RSA公钥加密两者一起发给对方。对方先用RSA私钥解开拿到AES密钥再用AES密钥解开业务数据。这套流程在HTTPS握手、JWT某些加密算法、电子合同、开放平台回调里到处都是。好处是兼顾了RSA的安全分发和AES的高效吞吐。如果让我用一句话概括RSA不是用来“加密数据”的是用来“加密钥匙”的。3.3 数字签名保证“不是伪造的也没被改过”RSA另一个更重要的落地场景是数字签名其中涉及的API不是Cipher而是Signature。Signature signature Signature.getInstance(SHA256withRSA); signature.initSign(privateKey); signature.update(data.getBytes(StandardCharsets.UTF_8)); byte[] signed signature.sign();验签方用公钥Signature verifier Signature.getInstance(SHA256withRSA); verifier.initVerify(publicKey); verifier.update(data.getBytes(StandardCharsets.UTF_8)); boolean valid verifier.verify(signed);签名只在持有私钥的人手里能产生任何持有公钥的人都能验证。它做的不是保密而是防篡改和防抵赖。我在对接开放平台时经常遇到这类需求第三方回调数据不能明文裸奔需要双方约定用RSA私钥签名接收方用平台公钥验签一旦验签失败直接拒绝处理。实操中经常遇到的问题是双方用不同的填充参数签名比如一方用SHA256withRSA另一方用MD5withRSA验签直接false。遇到验签失败先对一遍算法名和原文编码这是最省时间的排错路径。4. 消息摘要与HMAC完整性校验的高频场景摘要算法很多人会写但不一定知道选型背后的门道。4.1 为什么还在用MD5的建议赶紧换MD5在上古时代红极一时下载站挂一个MD5值用来校验文件完整性。但从安全角度看MD5早在2004年就被学术界证明可以实现碰撞——两个不同的文件可以拥有完全相同的MD5值。SHA-1的碰撞在2017年也被Google团队真正构造出来了。这意味着什么在“防止文件被篡改”这类安全边界场景里MD5和SHA-1已经不合格。SHA-256是当前的主流选择。密钥长度和算法名对不上SHA-256也不是密钥不叫长度输出就是256比特的摘要输出32字节碰撞难度2^128安全余量足够。Java里用起来非常简单MessageDigest digest MessageDigest.getInstance(SHA-256); byte[] hash digest.digest(data.getBytes(StandardCharsets.UTF_8));需要特别说明的是摘要算法不是用来做加密的工具它是用来做完整性校验的。很多人把它和加密混为一谈被问到“SHA-256能解密吗”就懵了。哈希从设计上就是单向的不存在解密这一说。4.2 密码存储绝不能直接用哈希加盐与PBKDF2这是我在安全评审里反复提醒团队的一件事。很多老系统里存密码就是直接MD5(password)甚至还有MD5(MD5(password))这种自以为高级的方案。除非系统里存的是完全无价值的演示数据否则这种做法都该被立刻整治。直接哈希面临两个问题。第一是彩虹表攻击攻击者预先把几亿个常见密码的哈希值算好拿到你的哈希一查就知道明文。第二是相同密码产生相同摘要用户之间一比较就能发现谁用了同一密码。正确做法是加盐慢哈希。盐是一段随机字节每个用户都有独立的盐和密码拼在一起再哈希这样相同密码的用户也会得到不同摘要。同时引入一个计算代价较高的哈希函数让暴力破解的成本上升。JDK内置的PBKDF2就可以import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import java.security.SecureRandom; import java.util.Base64; public String hashPassword(String password, byte[] salt) throws Exception { int iterations 100_000; int keyLengthBits 256; PBEKeySpec spec new PBEKeySpec(password.toCharArray(), salt, iterations, keyLengthBits); SecretKeyFactory factory SecretKeyFactory.getInstance(PBKDF2WithHmacSHA256); byte[] hash factory.generateSecret(spec).getEncoded(); return iterations : Base64.getEncoder().encodeToString(salt) : Base64.getEncoder().encodeToString(hash); }盐的生成也用SecureRandom16字节足够。迭代次数共10万次左右。这个数字太低容易被暴力破解太高会让服务器CPU压力过大在普通互联网系统里10万是一个比较合适的基准。Spring Security里默认用的BCryptPasswordEncoder思路类似BCrypt内部自带盐而且迭代代价可配置用起来更省心。还有一个小细节容易被忽略校验密码时要用常量时间比较函数不要用字符串的equals。常规equals在遇到第一位不一致时就返回攻击者可以通过响应时间差判断前缀是否相同。Java里可以用MessageDigest.isEqual(byte[], byte[])来避免时序侧信道。4.3 HMAC带密钥的摘要JWT里HS256就是它HMAC全称Keyed-Hash Message Authentication Code从名字就能看出它和普通摘要的区别普通哈希没有密钥任何人都能算HMAC则要求双方共享一个密钥只有知道密钥的人才能生成和验证MAC值。场景感很强的一个例子是JWT。JWT的签名算法HS256官方名字叫HMAC SHA-256和Java API里HmacSHA256是同一个东西。服务端用密钥签名Token任何篡改都会导致验签失败。Java代码import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.util.Base64; public static String hmacSha256(String data, byte[] key) throws Exception { Mac mac Mac.getInstance(HmacSHA256); SecretKeySpec keySpec new SecretKeySpec(key, HmacSHA256); mac.init(keySpec); byte[] result mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(result); }看到new SecretKeySpec(key, HmacSHA256)可能有人疑惑为什么密钥类型名和算法名一样。HMAC的密钥本质上就是一段随机字节数组用SecretKeySpec包装只是为了符合JCE接口规范。HMAC比普通SHA-256多了一层安全性不知道密钥的人无法伪造MAC所以它特别适合用在回调通知的校验、防篡改token、接口参数签名等场景。5. 国产密码算法在Java里的落地最近这几年系统分析师考试里“国产加密算法知识点”频频出现实际项目里也越来越多地看到“请支持国密”的招标要求。其实国密算法本身不是新东西但Java生态里使用它需要做一些额外动作。5.1 SM2/SM3/SM4分别对标什么国密算法通常特指SM2、SM3、SM4三件套它们不是乱编号每款都有明确的定位。算法类型对标国际算法关键参数主要用途SM2非对称RSA/ECC基于椭圆曲线公钥加密长度受限签名、密钥协商、少量数据加密SM3摘要SHA-256输出256位摘要完整性校验、数字签名中的哈希SM4对称分组AES-128分组长度128位密钥长度128位大数据量加密SM4和AES-128的参数结构非常接近都是128位分组、128位密钥只是运算细节不同。SM2对标的是基于椭圆曲线的能力实现签名和密钥交换用得最多因为它比RSA在相同安全强度下所需的密钥更短运算资源消耗也更小。SM3对标SHA-256输出也是32字节。JDK默认不自带这些算法这也是很多第一次接触国密的Java开发者最大的阻碍。你直接Cipher.getInstance(SM4/GCM/NoPadding)会抛NoSuchAlgorithmException必须引入第三方Provider。5.2 用Hutool和Bouncy Castle快速接入SM系列我自己写项目时最常用的方案是用Hutool的crypto模块它内部封装了BouncyCastleAPI非常友好几行代码就能跑起来。先引入依赖dependency groupIdcn.hutool/groupId artifactIdhutool-crypto/artifactId version5.8.25/version /dependencySM4对称加解密示例import cn.hutool.crypto.symmetric.SM4; import java.nio.charset.StandardCharsets; byte[] key 0123456789abcdef.getBytes(StandardCharsets.UTF_8); // 16字节对应128位 SM4 sm4 new SM4(key); String cipherText sm4.encryptHex(需要加密的业务数据); String plainText sm4.decryptStr(cipherText);如果对灵活性要求高比如需要自己控制模式、IV、AAD就绕过Hutool直接用BouncyCastleimport org.bouncycastle.jce.provider.BouncyCastleProvider; import javax.crypto.Cipher; import java.security.Security; Security.addProvider(new BouncyCastleProvider()); Cipher cipher Cipher.getInstance(SM4/CBC/PKCS7Padding, BC);注意这里显式指定了Provider名称为“BC”避免和JDK内置Provider产生冲突。SM2签名在Hutool里也封装得很干净import cn.hutool.crypto.asymmetric.SM2; SM2 sm2 new SM2(privateKeyStr, publicKeyStr); byte[] signed sm2.sign(data.getBytes(StandardCharsets.UTF_8)); boolean valid sm2.verify(data.getBytes(StandardCharsets.UTF_8), signed);SM2和SM3配合使用的数字签名方案能完全替代传统的“RSA SHA-256”组合这也是很多等保合规场景里关注的能力。5.3 如果你是备考系统分析师背这几条就够了从考试角度准备国密算法不需要会写代码重点是建立概念映射。我总结下来就三句话加一张表SM2是非对称算法负责签名和密钥协商对标RSA/ECC。SM3是摘要算法输出256位负责完整性校验对标SHA-256。SM4是对称分组算法分组和密钥长度都是128位负责数据加密对标AES-128。考试里经常出现“以下哪个算法属于对称密码体制”“SM3的摘要长度是多少”“SM2主要用于什么场景”这类题能把这三句话记牢基本就能应对。如果题目再延伸考安全特性就补充一句SM3的压缩函数结构和SHA-256不同抗碰撞能力满足商用密码强度要求SM4采用32轮非线性迭代结构设计时重点考虑了嵌入式环境下的实现效率。6. 常见问题速查与我的排坑记录写到这里分享几个我在实际开发里亲身踩过或帮同事排过的坑。这些问题在官方文档里不一定写得很醒目但遇到时你是真能卡半天。6.1 Illegal key size和无限强度策略问题有段时间特别流行一道排查题代码写着AES-256在本地开发环境跑得好好的部署到测试服务器的JDK 8上就抛InvalidKeyException: Illegal key size。原因很直接老版本的JDK 8默认限制了加密强度只允许128位密钥的对称加密256位的AES被告非法。解决办法有两种一是升级到JDK 8u161以上的版本该版本开始默认启用无限强度加密策略二是如果生产环境JDK无法升级老路子是手动替换$JAVA_HOME/jre/lib/security下面的local_policy.jar和US_export_policy.jar把它们换成JCE无限强度策略文件。这两种方案里我永远推荐第一种因为替换jar的方式在后续JDK小版本升级时容易被覆盖而且下载策略文件也要保证源可信。6.2 解密出来乱码问题这样查乱码问题是加解密代码里最高频的报错来源。出现乱码时按照这个顺序排查第一解出来的byte[]是不是用UTF-8还原成字符串的如果默认用了平台字符集Linux和Windows结果可能不一致第二密文在传输过程中是否经过Base64中间环节有没有人把Base64字符串再做了URL编码导致加号被替换成%2B之类第三密钥字符串在生成SecretKeySpec时getBytes有没有显式指定StandardCharsets.UTF_8。一个比较偷懒但非常有效的验证方式在本地用同一份密钥加密把Base64密文复制到一个干净的新工程里解密如果本地能解、测试环境不能解那基本就是字符集或者配置串被环境处理时弄脏了。6.3 线上“偶发解密失败”排查思路“偶发”这个词在加密场景里几乎总意味着一个东西随机数或初始向量的一致性出了问题。我最常看到的代码长这样// 错误示范 cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(iv)); byte[] encrypted cipher.doFinal(data); // 省略这边把iv和encrypted拼接后传输问题往往不在加密端而在解密端——有些人为了省事把IV写死成一个固定值或者把IV和密文分开存放到不同字段结果某一次上线后字段对应关系变了解密就随机失败。正确的做法就是我前面工具类里展示的那样把IV和密文拼在一起输出解密时先拆出IV再用。对于GCM模式还建议检查解密时的Tag长度是否配置正确。加密时用的是128位Tag解密时配置成96位或80位也会出现偶尔成功、偶尔失败的情况这不是随机数的问题是参数不一致的问题。6.4 密钥管理的几条个人建议密钥安全其实是整个加密体系里门槛最高的一环。算法再安全密钥如果直接硬编码在代码仓库里那一切都是白搭。我一般这样分层处理开发环境密钥放到环境变量或本地配置文件通过Spring的ConfigurationProperties注入不要出现在代码里。测试/预发环境放进配置中心用命名空间隔离。生产环境有条件就上KMS或HSM让密钥只在硬件安全模块内使用业务代码拿到的只是一个引用。哪怕一开始没条件上KMS至少要保证不同环境的密钥不共用并且有轮换机制。密钥一旦泄露不是换个密钥字符串就能解决的所有用旧密钥加密过数据的下游系统都得同步排期处理。这个成本远高于一开始把密钥管理好。折腾加密这些年我最大的一个体会是真正难的不是写加解密代码而是记住“你永远无法在所有细节上都靠自己搞定”算法选型有标准、参数配置有规范、密钥管理有工具我们要做的是把每个环节按成熟方案落地别自己发明变体。如果你正在做的一个项目里涉及到加密需求可以按这篇文章的顺序从JCA/JCE框架往下梳理一遍至少能少踩一半网上常见的坑。等你在真实系统里把RSA和AES的混合流程走通一遍后再回头看这些概念就会发现它们已经形成了自己的知识网络。