ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java加密算法实战:AES、RSA、哈希与HMAC全解析

Java加密算法实战:AES、RSA、哈希与HMAC全解析 加密算法这个知识点在Java开发里的地位一直有点微妙。你说它难平时写业务CRUD基本碰不到你说它不重要一旦遇到登录、支付、第三方接口对接、数据脱敏或者面试官轻描淡写来一句“说说你了解的加密算法”卡壳的人不计其数。这期内容我打算把Java里最常用的几类加密算法从头到尾捋一遍不绕弯子直接说人话。文章会覆盖对称加密AES、非对称加密RSA、哈希摘要MD5、SHA-256、消息认证码HMAC以及它们在真实项目里的组合用法。每一个部分都带完整可运行的Java代码还会把原理、选型原因、常见坑一起讲清楚。无论是正在准备面试的人还是需要在项目里落地数据加密的开发者这篇文章都能直接当工具文用。1. 先搞清楚需求你到底需要哪种“加密”1.1 加密算法不是越复杂越好很多人接触加密的第一反应是把所有数据全都塞给算法“处理一下”这是典型的没有区分业务需求的特点。加密算法本身没有万能药不同的业务场景对应完全不同的算法路线。比如你在做用户登录目标是不让密码以明文形式进数据库那你要的是单向哈希如果是在两个系统之间传报文既要防偷听又要防篡改那通常要组合使用对称加密、非对称加密和摘要算法如果是给接口请求做签名校验你需要的可能是HMAC。我见过不少项目上来就把AES和RSA同时用上结果要保护的字段根本不需要那么高等级的防护。算法用得越重密钥管理的成本就越高调度的性能损耗也越大。搞清楚“要防止谁、防什么、数据在哪泄露”这些前提再去选算法才是正确的做事顺序。1.2 把概念分清楚编码、哈希、加密这是很多新手最迷糊的地方。Base64是加密吗不是。它只是一种编码方式把二进制数据转换成可打印的ASCII字符只是为了方便传输和展示没有任何保密性。MD5和SHA-256是加密吗严格来说它们也不是加密而是哈希摘要是单向的无法从摘要反推出原始数据。真正的加密比如AES和RSA是双向的加密之后可以解密还原这才叫保密通信。这三类东西在项目里的用途完全不同。Base64负责让二进制数据能放进JSON、XML或者URL里哈希负责校验完整性和存储密码加密负责真正保护数据的机密性。把它们区分开代码才会写得清爽跟别人聊设计的时候也不容易鸡同鸭讲。1.3 面试里最常问的基础点有哪些说到面试“Java面试八股文”里关于加密的部分其实翻来覆去就几个考点对称加密和非对称加密的区别、AES和RSA各自的特点、MD5为什么不适合存密码、加密模式和填充的区别、怎么安全地传输密钥。这些题没有太多花哨的内容但能难住不少人。面试官真正想听的不是你会背“AES是对称加密、RSA是非对称加密”这样的定义而是你能不能讲清楚“为什么HTTPS里既用了RSA又用了AES”“为什么AES的ECB模式不安全”“用户密码到底应该怎么存”。这篇文章后面会逐个回答这些问题看完之后不光能面试用项目里照抄也能用。2. 算法分工对称加密、非对称加密和哈希要怎么选2.1 对称加密AES为什么是标准答案对称加密的意思是加密和解密用同一把密钥。通信双方只要持有相同的密钥就能互相加解密。对称加密最大的优点是速度快尤其适合大数据量的场景。Java里常见的对称加密算法有DES、3DES、AES。DES已经可以被轻松破解3DES也因此被淘汰。AES密钥长度分为128位、192位和256位目前公认的安全强度足够是名副其实的主流标准。无论你看哪个开源项目对称加密部分基本就是AES一家独大。选择AES的时候还要注意它的工作模式。AES本身只规定了怎么处理一个数据块而真实数据往往不止一个块大小需要定义怎么串起来这就是模式。ECB模式最简单同样的明文块会生成同样的密文块等于把明文的模式暴露在外面非常危险。CBC模式引入了前一个块参与当前块的加密比ECB强但仍然存在填充方案的问题。现在推荐的是GCM模式它自带认证能力能同时保证机密性和完整性是现代化的默认选择。2.2 非对称加密RSA解决的是密钥分发问题非对称加密用一对密钥公钥和私钥。公钥可以公开给别人私钥自己保存。用公钥加密的数据只能由对应的私钥解密反过来用私钥加密的数据只能由公钥验证。这样最大的好处是双方通信不需要先安全地传递同一把密钥了。RSA是目前最流行的非对称加密算法。它的安全性依赖于大整数因子分解的困难性。Java里的RSA实现很成熟生成密钥对、加解密、签名、验签都能直接用JDK内置库完成。RSA的缺点也很明显密钥长度长至少2048位运算速度远低于AES而且能加密的明文长度受密钥长度限制。所以真实系统里很少直接用RSA加密大量业务数据而是用“RSAAES混合加密”先随机生成一把AES会话密钥用AES加密业务数据再用RSA公钥加密这把AES密钥传给对方。这样既享受了AES的速度又解决了对称密钥的安全分发问题。2.3 哈希算法它其实不是加密哈希算法的特点是无论输入多长输出固定长度且理论上是不可逆的。Java里常见的MessageDigest类支持的算法包括MD5、SHA-1、SHA-256等。很多人习惯把MD5叫作加密这不对更准确的说法是消息摘要。哈希的应用场景是校验数据完整性。比如下载一个大文件官方会公布文件的SHA-256值你下载完算一下哈希一样就说明文件没被改过。再比如接口签名把参数拼接成一个字符串加上盐做哈希双方各自计算比对。还有数据库里存密码的哈希值而不是明文。但要注意MD5和SHA-1目前已经被认为不安全适用于完整性校验这种可碰撞概率要求不高的场合不能用于需要抗碰撞的安全场景。SHA-256则目前仍然安全是哈希算法的主力。哈希没有密钥概念任何人拿到你的算法都能算所以如果要防止数据被篡改就需要用它和密钥组合这就是HMAC的来历。2.4 签名与消息认证HMAC、数字签名在业务里的角色消息认证码HMAC就像“带密钥的哈希”。同样一段数据只有掌握密钥的人才能生成正确的MAC值接收方验证MAC就知道数据是否被篡改、是否来自持有密钥的一方。HMAC常用于接口签名、防止请求重放和参数篡改。数字签名则更像“用私钥做哈希的签名”。RSA私钥对摘要做签名对方用公钥验签就能确认数据确实来自私钥持有方并且没有被改动。支付回调、电子合同、开放平台API签名是数字签名最常见的应用场景。区分这两个概念的关键在于HMAC是对称的双方共用密钥数字签名是非对称的签名和验签用不同密钥。理解了这个区别选择工具的时候就不会混乱。3. Java实现前的四个关键准备3.1 JDK的JCE够用第三方库只是让代码更省事Java自带一套完整的加密API就是JCE体系核心类在javax.crypto和java.security包下。AES、RSA、SHA、HMAC这些算法JDK全都能直接支持不需要引入任何额外依赖。很多初学者一上来就去找第三方封装其实标准库已经完全够了。第三方库像Apache Commons Codec、Hutool、Bouncy Castle更多是把繁琐的Base64、Hex转换封装了或者补一些JDK不原生支持的算法。以我的经验核心算法尽量用JDK标准库辅以Hutool这种工具库减少样板代码是性能和可维护性最平衡的方案。如果项目不允许额外依赖全部用JDK标准库也没问题工具类封装好之后一样好用。3.2 Base64、十六进制转换写工具类前先做好这两个基础加密运算的结果是字节数组直接打印会出现乱码存到数据库也不方便。所以项目里几乎都会把加密结果做一次Base64或者十六进制编码再以字符串形式落地或传输。Base64编码后的字符串更短适合放进JSON十六进制编码后的字符串可读性好适合调试。Java 8之后java.util.Base64是官方推荐的方式不要在代码里再用sun.misc.BASE64Encoder那个类不对外开放且性能差。十六进制转换JDK没有提供现成的工具类我习惯自己写一个不到十行的小方法或者直接用Hutool的HexUtil。这两个基础能力做好后面所有加密工具类都会顺很多。3.3 算法模式、填充、IV和密钥长度错了直接报错这是Java加密最容易踩坑的地方。以AES为例实例化Cipher的时候要指定完整的算法字符串比如“AES/GCM/NoPadding”这表示算法是AES模式是GCM填充方式是NoPadding。如果只写“AES”系统会按Provider的默认值处理不同环境可能出现兼容性问题所以强烈建议显式写全。IV初始化向量是许多模式必需的参数。CBC和GCM都需要IV每次加密都应该随机生成新的IV并把IV连同密文一起传给对方。GCM模式还要求IV不能重复使用否则安全性会急剧下降推荐长度是12字节。RSA这边密钥长度至少要2048位否则会遇到“SecurityException: Unsupported keysize or algorithm parameters”这类错误。JDK 8的某些版本默认策略限制256位AES需要处理一下jre/lib/security/java.security里的crypto.policy配置JDK 9之后默认支持。3.4 密钥怎么保存比算法本身更重要算法再安全密钥泄露出去了等于白干。密钥管理是加密系统里最容易被忽视的一环。常规做法是不要把密钥直接写在代码里也不要在配置中心里明文存放。Java项目一般会在环境变量或者配置中心里引用密钥配合KMS这类密钥管理系统做真正的托管。在本地开发时我一般用Jasypt或配置中心的加密配置功能把密文放进配置文件运行期解密后使用。线上环境则倾向于使用云厂商的KMS由KMS保管根密钥应用只持有临时解密后的密钥或者加密后的副本。密钥还要定期轮换轮换时要注意老数据能解密所以解密的密钥集合还需要保留历史版本。没有设计好这一层算法选得再好也会被审计和运维问题拖垮。4. AES对称加解密实例一套代码覆盖全部场景4.1 用AES/GCM/NoPadding而不是ECB很多老项目里还能看到“AES/ECB/PKCS5Padding”这个写法在面试里会被点名批评。ECB模式同样的明文会产生同样的密文16字节一组的规律性会直接暴露明文信息比如图片经过ECB加密还能看出轮廓。GCM模式则引入了随机IV和认证标签同样明文加密出来的结果完全不同且解密时能校验数据有没有被篡改。所以这一节给的实例直接使用AES/GCM/NoPadding。GCM模式自带完整性校验不需要再额外接一个“先加密再算哈希”的流程这是现代Java加密推荐的标准姿势。4.2 完整工具类代码下面的代码可以直接复制到项目里用也可以根据业务改造成自己的工具类。它包含了密钥生成、加密、解密三个核心方法并且把IV和加密后的密文一并返回调用方只需要关心字符串。import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.Arrays; import java.util.Base64; public class AesGcmUtil { private static final int TAG_LENGTH_BITS 128; private static final int IV_LENGTH_BYTES 12; /** 生成AES密钥长度可选择128、192、256 */ public static SecretKey generateKey(int keySize) throws Exception { KeyGenerator keyGen KeyGenerator.getInstance(AES); keyGen.init(keySize, new SecureRandom()); return keyGen.generateKey(); } /** 加密返回 Base64(iv ciphertext) */ public static String encrypt(String plaintext, SecretKey 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, key, new GCMParameterSpec(TAG_LENGTH_BITS, iv)); byte[] ciphertext cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); byte[] result new byte[iv.length ciphertext.length]; System.arraycopy(iv, 0, result, 0, iv.length); System.arraycopy(ciphertext, 0, result, iv.length, ciphertext.length); return Base64.getEncoder().encodeToString(result); } /** 解密从 Base64 字符串中提取 iv 和密文 */ public static String decrypt(String base64Data, SecretKey key) throws Exception { byte[] decoded Base64.getDecoder().decode(base64Data); byte[] iv Arrays.copyOfRange(decoded, 0, IV_LENGTH_BYTES); byte[] ciphertext Arrays.copyOfRange(decoded, IV_LENGTH_BYTES, decoded.length); Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(TAG_LENGTH_BITS, iv)); byte[] plainBytes cipher.doFinal(ciphertext); return new String(plainBytes, StandardCharsets.UTF_8); } }4.3 关键方法逐行拆解generateKey方法里KeyGenerator要初始化成128位这是安全性和性能平衡比较好的选择。如果你所在项目安全要求高可以用256位。不过在JDK 8早期版本里256位AES受限需要修改crypto.policy配置JDK 9以后没有这个问题。encrypt方法的逻辑是先随机生成12字节的IV然后初始化Cipher为加密模式注意GCMParameterSpec的第一个参数是认证标签长度128位是GCM的推荐值。下一步是doFinal真正执行加密得到密文字节数组。为了让接收方解密密文必须把IV和密文一起传。我这里采用“iv长度固定为12字节所以直接把iv拼在密文前面”的方式解密时按长度截取即可。这样做最省事也符合主流做法。decrypt方法是对称的逆过程。先把Base64字符串解码然后切割出前12字节的IV和剩余部分的密文再初始化Cipher为解密模式。这里为什么能直接切割因为IV长度在设计时固定为12字节生成和解析约定一致。如果以后改了IV长度解密逻辑也要同步修改所以这个常量要写清楚注释。4.4 实际业务中如何替换掉老的DES/3DES如果是老项目迁移从DES/3DES换到AES最省事的替换方案是把Cipher.getInstance的参数从“DES/ECB/PKCS5Padding”改成“AES/GCM/NoPadding”然后把密钥从64位DES密钥升级为128位AES密钥。不过要注意老数据的密文是用旧算法生成的必须保留旧的解密逻辑做兼容新数据走新算法这样能平滑过渡。另一个建议是在加密结果的字符串开头加一个版本前缀比如“v1:xxx”。这样将来换算法、换密钥轮换都能根据版本号识别不至于线上数据解不开。这个细节看起来简单真到了发版到一半发现历史数据读不了的时候是真的会急出一身汗。5. RSA非对称加密实例密钥对、加解密与分段处理5.1 生成RSA密钥对并保存为字符串RSA的密钥对可以生成在内存里临时使用但真实项目里通常需要把公钥和私钥保存下来部署到不同的服务上。JDK标准库生成的KeyPair对象可以直接取字节数组然后用Base64编码成字符串保存。import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; import java.security.PublicKey; import java.util.Base64; public class RsaKeyUtil { public static String[] generateKeyPairString() throws Exception { KeyPairGenerator generator KeyPairGenerator.getInstance(RSA); generator.initialize(2048); KeyPair keyPair generator.generateKeyPair(); String publicKeyString Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()); String privateKeyString Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()); return new String[]{publicKeyString, privateKeyString}; } public static PublicKey loadPublicKey(String publicKeyString) throws Exception { byte[] keyBytes Base64.getDecoder().decode(publicKeyString); java.security.spec.X509EncodedKeySpec spec new java.security.spec.X509EncodedKeySpec(keyBytes); java.security.KeyFactory factory java.security.KeyFactory.getInstance(RSA); return factory.generatePublic(spec); } public static PrivateKey loadPrivateKey(String privateKeyString) throws Exception { byte[] keyBytes Base64.getDecoder().decode(privateKeyString); java.security.spec.PKCS8EncodedKeySpec spec new java.security.spec.PKCS8EncodedKeySpec(keyBytes); java.security.KeyFactory factory java.security.KeyFactory.getInstance(RSA); return factory.generatePrivate(spec); } }这里有个容易出错的地方公钥和私钥的字节编码格式不一样。公钥用的是X.509格式私钥用的是PKCS#8格式所以还原的时候要分别用X509EncodedKeySpec和PKCS8EncodedKeySpec不能混用。网上很多报“java.security.spec.InvalidKeySpecException”的问题大多就是把这两个Spec用反了。5.2 加解密代码与“RSA/ECB/PKCS1Padding”RSA加解密代码和AES类似也是通过Cipher来做。不过这里的模式写法需要特殊说明“RSA/ECB/PKCS1Padding”里的“ECB”只是沿用了JCE命名规范里“工作模式”的位置RSA并没有像AES那样有真正的分组模式概念这个字符串其实表达的是“用RSA加PKCS#1 v1.5填充”。import javax.crypto.Cipher; import java.nio.charset.StandardCharsets; import java.security.PrivateKey; import java.security.PublicKey; import java.util.Base64; public class RsaCipherUtil { public static String encrypt(String plaintext, PublicKey publicKey) throws Exception { Cipher cipher Cipher.getInstance(RSA/ECB/PKCS1Padding); cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] bytes cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(bytes); } public static String decrypt(String ciphertext, PrivateKey privateKey) throws Exception { Cipher cipher Cipher.getInstance(RSA/ECB/PKCS1Padding); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] bytes cipher.doFinal(Base64.getDecoder().decode(ciphertext)); return new String(bytes, StandardCharsets.UTF_8); } }RSA填充模式有PKCS1Padding和OAEPPadding之分。PKCS#1 v1.5填充虽然简单但存在一些理论上的攻击面只是实践中多数兼容性场景仍在用。新项目如果对安全要求高推荐用OAEP填充也就是把算法字符串写成“RSA/ECB/OAEPWithSHA-256AndMGF1Padding”。但前提是通信双方都能兼容这个算法标识否则联调的时候又要扯皮。我一般是在内部系统之间用OAEP和第三方对接时看对方要求再切换回PKCS1Padding。5.3 245字节限制和分段加密RSA一个硬性限制是能加密的明文长度有限。以2048位密钥为例加密操作时需要留出至少11字节给PKCS#1 v1.5填充所以最大明文长度是2048/8 - 11 245字节。如果明文超过245字节Cipher的doFinal会直接抛出IllegalBlockSizeException。解决方案是分段加密。把明文按245字节切块每块独立加密解密时再按256字节一段解密拼接。不过分段加密会让密文长度膨胀而且没有语义安全所以实际业务里很少直接用RSA加密大段数据。更通用的做法是RSA只加密AES密钥业务数据交给AES处理这样就把长度限制绕过去了。这个话题下一小节展开。5.4 RSAAES混合加密微服务和接口对接场景的常规操作混合加密的流程很清晰。发起方随机生成一把AES明文密钥或者叫会话密钥用它对业务报文进行加密然后用接收方的RSA公钥加密这把AES密钥最后把“RSA密文 AES密文”一起发送。接收方先用自己的RSA私钥解出AES密钥再用AES密钥解密业务数据。为什么要绕一圈因为明文传输AES密钥不行全程用RSA加密业务数据又太慢。混合加密正好利用AES的高性能和RSA的密钥分发优势HTTPS的TLS握手本质也是这个思路。我在实际项目里做开放接口时通常会把报文设计成类似下面的结构{ encryptedKey: RSA加密后的AES密钥, payload: AES加密后的业务数据, sign: 对payload做的HMAC或者签名 }AES密钥每次请求都随机生成用完即弃这样即使某一次密钥泄露也只能解开这一条报文不会波及其他数据。这种方案实现复杂度不高安全收益却很明确适合多数微服务之间的敏感数据交换场景。6. 哈希与消息摘要MD5、SHA-256和HMAC实战6.1 MD5能用但要清楚它的边界MD5的Java代码很多人都会写MessageDigest.getInstance(“MD5”)然后digest一下。但它现在已经不适合用在安全场景。2004年就有研究者找到了MD5的碰撞方法构造两个不同内容但MD5值一样的数据已经可行。所以凡是涉及防伪、防篡改、身份验证的需求都不该继续用MD5。但它并没有完全消失。一些内部系统校验文件完整性或者做资源去重速度优势还在。我自己在写代码时会注意一点哪怕项目里之前到处都是MD5新功能的代码也不会继承这个习惯转而用SHA-256。如果历史数据必须兼容再加一层新老逻辑判断而不是继续把MD5往外蔓延。6.2 SHA-256的极简实现SHA-256属于SHA-2家族是目前哈希算法里的主流选择。Java实现非常简洁JDK原生就支持几行代码搞定。import java.nio.charset.StandardCharsets; import java.security.MessageDigest; public class ShaUtil { public static String sha256(String data) throws Exception { MessageDigest digest MessageDigest.getInstance(SHA-256); byte[] hashBytes digest.digest(data.getBytes(StandardCharsets.UTF_8)); return bytesToHex(hashBytes); } private static String bytesToHex(byte[] bytes) { StringBuilder sb new StringBuilder(); for (byte b : bytes) { String hex Integer.toHexString(0xff b); if (hex.length() 1) { sb.append(0); } sb.append(hex); } return sb.toString(); } }MessageDigest本身是线程不安全的多线程环境下每次调用都用一个新实例比较稳妥。上面的实现每次方法内创建实例没有共享状态所以并发下也没问题。虽然SHA-256不可逆但它不适合直接存密码。原因很简单同样的密码算出的哈希一致攻击者可以预先算好大量常见密码的哈希值来比对这就是彩虹表攻击。密码库被拖库后用户密码几乎等于裸奔。所以哈希摘要用于完整性校验可以用来存密码必须加盐并使用专门设计慢哈希算法。6.3 HMAC-SHA256接口签名实战HMAC是带密钥的哈希它在计算摘要时混入了一个只有双方知道的密钥。这样即使算法公开、数据明文可见没有密钥的人也无法伪造合法的MAC值。下面这个工具类是接口签名场景里很常见的写法。调用方把请求参数按一定规则排序拼接加上时间戳和随机数再用密钥做HMAC-SHA256把签名放到Header里。服务端用同样的规则计算一遍比对是否一致。import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.util.Base64; public class HmacUtil { public static String hmacSha256(String data, String secret) throws Exception { Mac mac Mac.getInstance(HmacSHA256); SecretKeySpec secretKeySpec new SecretKeySpec( secret.getBytes(StandardCharsets.UTF_8), HmacSHA256); mac.init(secretKeySpec); byte[] raw mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(raw); } }HMAC做接口签名时有一个经验之谈参数拼接顺序要固定。最稳妥的方法是先把所有参数按照key的字典序排列再用“keyvaluekeyvalue”的格式拼成一个字符串。否则联调时两边排列顺序稍微不一样签名就对不上排查起来非常痛苦。时间戳和随机数也要参与签名这样能杜绝重放攻击。服务端可以对超过比如5分钟的时间戳直接拒绝随机数存到缓存里防重复使用。6.4 密码存储用什么BCrypt和PBKDF2用户密码的正确存储方式不是“加密”而是“加盐慢哈希”。慢的意思是让每次哈希计算消耗足够多的时间这样攻击者暴力破解的成本变得非常高。两种主流方案一个是PBKDF2一个是BCrypt。JDK原生支持PBKDF2不需要额外依赖而BCrypt需要引入jBCrypt或者Spring Security的crypto模块。我做密码存储时往往会选BCrypt因为API简单、盐自动生成、且自带一层基于Blowfish算法的慢哈希设计安全性经过长年验证。// 伪代码示意实际使用请引入 org.mindrot.jbcrypt.BCrypt // 注册时String hashed BCrypt.hashpw(password, BCrypt.gensalt(12)); // 登录时boolean ok BCrypt.checkpw(password, hashed);BCrypt有个“代价因子”的概念默认10可以在gensalt里指定。代价因子越大计算越慢一般开发环境用10生产环境可以调到12左右。这背后的权衡是用户登录的体验不能太慢但也不能快得让攻击者可以批量尝试。选12通常能在普通服务器上达到单次哈希80到120毫秒的水平这个量级对正常用户无感对暴力破解就是巨大的成本。如果项目禁用额外依赖PBKDF2WithHmacSHA256也是很好的选择。关键要点是必须随机生成16字节以上的盐哈希迭代次数不低于一万次推荐六万到十万之间。这些参数没有一个绝对标准基本原则是“在目标硬件上单次计算尽量慢到可接受的上限”。7. 常见问题与排查技巧实录7.1 IllegalBlockSizeException多半是长度超限这个异常在AES和RSA场景都遇到过。RSA里最大明文长度是固定的超过245字节就会被doFinal拒绝解决办法就是分段或换混合加密。AES这里也偶尔出现通常是Cipher模式配置错误或者加密和解密使用的算法不一致比如一端用AES/GCM/NoPadding另一端用AES/CBC/PKCS5Padding去解两边块处理方式对不上就会出现长度或填充的异常。排查时先把两边的算法字符串对齐。7.2 中文解密乱码八成是解码方式不一致加密前把字符串转成字节数组和解密后把字节数组转回字符串两端的字符集必须一致。最常见的问题是一端用平台的默认字符集另一端明确指定UTF-8一旦部署环境默认字符集变了解密出来的中文就是乱码。解决办法是在代码里写死StandardCharsets.UTF_8杜绝依赖环境。另一个常见原因是Base64编解码多余了换行导致密文被改了这个下面会展开。7.3 前端JS加密、Java解不开对齐模式和IV做Web开发经常遇到前端把数据加密后再传给后端比如JSEncrypt加密RSA、CryptoJS加密AES。两边解不开的原因通常是这几个密钥格式不对、模式不统一、IV不一致、填充方式不同。特别是CBC模式前端如果随机生成IV后直接把它拼在密文前面后端也要按同样的规则拆分IV。很多前端库默认用OpenSSL的“Salted__”格式后端如果没做兼容处理就会解密失败。我处理这类问题的方法是先让前端把原始加密参数全部输出出来包括算法名、密钥、IV、加密结果后端再逐步模拟比对。前端和后端各自独立看都“没问题”放到一起就出问题绝大多数是两端对某个隐含默认值的理解不一样。对齐“算法全名IV传递规则字符集”这三点基本都能解决。7.4 密钥管理配置中心、环境变量、KMS怎么选密钥放代码里是最差的选择git历史一旦泄露直接全完。稍微正规一点的做法是放在环境变量或者部署时的用户配置文件里。再进阶一点放到配置中心里用加密后的密文存储应用启动时解密读取。更安全的做法是使用KMS比如云厂商的密钥管理服务由KMS生成和保管主密钥应用通过API获取解密后的密钥或者直接调用KMS完成加解密。我个人的建议是小项目用环境变量加配置中心就够了但代码里要留出“从哪个来源读取密钥”的抽象。大项目直接上KMS别考虑自己搭一套密钥管理系统管理和审计成本极高。密钥轮换也要提前想清楚至少要做到一个业务密钥有多个版本新数据用新密钥老数据用老密钥解。7.5 性能对比AES和RSA数量级的差距AES在支持AES-NI指令集的CPU上可以达到几GB每秒的吞吐量RSA 2048位做一次私钥解密大概需要耗时1到10毫秒核心运算比AES慢了几个数量级。所以混合加密方案里RSA只承担“传递密钥”这一个轻量任务大批量数据永远是AES的活。如果应用中频繁地做大量RSA加密和解密比如每秒上千次性能是会肉眼可见拖垮接口的。这种场景下要么考虑改用ECDH等更轻量的密钥协商方式要么减少RSA调用次数用会话密钥复用方案。做性能测试时最好把加解密耗时也纳入压测范围很多服务慢不是SQL问题而是密钥运算这一层被忽略了。写在最后我从第一次写加密代码到现在踩过最大的坑不是算法选型而是“以为会了”。AES和RSA的用法真正用到生产环境会遇到密钥格式、字符集、兼容性、性能、密钥管理等一堆琐碎的问题。这就像会写SQL不等于会做数据库设计知道加密API不等于能把数据安稳地保护起来。最后分享一个我自己一直在用的稳定套路对称加密只考虑AES模式优先GCM非对称加密用RSA但只用来保护会话密钥或做签名密码存储必须用带盐的慢哈希任何加密方案都要把密钥生命周期管理设计好。这套打法不出彩也没有炫技空间但胜在稳妥能满足绝大多数真实业务的安全需求。希望这份代码和经验能帮你少走几天弯路稳稳地把数据保护好。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进