不知道你在做Java后端时有没有这种经历:功能都写完了,最后要对数据进行加密或做签名校验,打开文档一看,MessageDigest、Cipher、Signature、KeyStore这些类铺了一桌,却不知道怎么选,也不知道为什么官方文档里的示例代码总是报错。更迷惑的是,搜索引擎里同一段加密逻辑,有人用javax.crypto写,有人用BouncyCastle写,还有人直接拉来Apache Commons Codec一行搞定。到底该用哪个?
这篇文章就从实际项目出发,把Java自带的加密体系和主流第三方类库放到一起讲清楚。内容涉及JDK原生JCA/JCE架构的API设计、核心加密类的使用细节、第三方库的选型对比,以及我实际开发中踩过的坑和排查思路。不管你是刚接触加密的新手,还是想把手头加密模块重构得干净点的老手,都有参考价值。
1. 内容整体设计与思路拆解
1.1 加密需求从哪里来,要解决什么问题
抛开密码学理论,日常业务里的加密需求其实就那么几类:数据在传输过程中要防窃听,所以需要对称加密;敏感字段在数据库里不能明文存储,所以需要哈希或可逆加密;接口调用方要确认请求没有被篡改,所以需要MAC或数字签名;双方交换密钥时需要一种不需要直接传递密钥的方式,所以出现了非对称加密。每种需求在Java里都有对应的API,但官方文档太零散,没人告诉你“什么时候用哪个”、“为什么这个场景不能这么用”。
我看过不少项目,数据库里存密码还在用MD5,理由是“MD5快”。但MD5连碰撞攻击都防不住了,用它存密码等于裸奔。也有人知道不能用MD5,转头用SHA-1,但SHA-1的碰撞攻击在2017年已经被实际演示过,安全性同样不达标。这类问题不是“代码不会写”,而是“体系没建立”。
所以这篇文章的核心思路是做一次全方位的工具梳理。先搞清楚JDK自带的加密能力边界在哪里,再说哪些第三方库能够在边界之外补位。到实操部分,每一种场景给一套可以直接落地的写法,最后把我在项目里遇到过的加密相关报错和排查过程整理成速查表。理清这些,你写加密代码时就不需要到处搜答案了。
1.2 自带包和第三方类库的方案选型逻辑
JDK自带加密能力集中在java.security和javax.crypto这两个包,底层由JCA(Java Cryptography Architecture)和JCE(Java Cryptography Extension)两个框架支撑。Java 9之前JCE是独立扩展包,需要单独下载,Java 9之后直接合入标准JDK。这套体系最大的特点是Provider架构——JDK只定义接口和算法抽象,具体算法由Provider实现。默认的SUN和SunJCE两个Provider覆盖了绝大多数常用算法,比如AES、RSA、SHA系列、HMAC等。
第三方类库存在的意义分两种。第一种是算法补全,比如国密SM2/SM3/SM4,JDK原生不支持,必须借助BouncyCastle;第二种是使用体验优化,比如Apache Commons Codec把Base64、Hex、SHA系列哈希封装成了静态方法,几行代码搞定原本需要十几行才能写完的逻辑。
选型时我的原则是:方案能用JDK自带API解决就优先用自带的,因为零依赖、维护成本低、性能经过充分优化;只有遇到JDK覆盖不到的需求时才引入第三方。这个原则不是教条,而是避免依赖泛滥。加一个加密库不只是引入一个jar包,还意味着它的加密实现要过一遍安全审计,版本升级也要跟着回归测试。为了一个Base64编码引入一整棵依赖树,不划算。
2. 核心细节解析与实操要点
2.1 JCA架构和Provider机制对使用方式的影响
很多人第一次接触JCA时容易被Provider机制搞糊涂。其实可以这样理解:JDK定义了一套加密服务的“行业标准”,SUN和SunJCE就是预装的两个“生产厂家”。你写Cipher.getInstance("AES/CBC/PKCS5Padding"),框架会去SunJCE里找到支持这个算法的实现类,然后创建实例。
这套机制带来的最直接影响就是:算法字符串不能乱写。getInstance传的参数是严格匹配的,AES/CBC/PKCS5Padding和AES/CBC/PKCS7Padding虽然在实际效果上几乎一样,但后者是BouncyCastle的叫法,SunJCE不认。如果系统里只装了默认Provider,直接传PKCS7Padding会抛出NoSuchAlgorithmException。
加密算法名还有大小写敏感问题。"aes/cbc/pkcs5padding"全部小写在大多数情况下能通过,因为JCA规范要求Provider在匹配算法名时忽略大小写,但某些第三方Provider实现并不规范。实操中最稳的做法是使用官方文档里的标准名字,大小写按规范写,不要依赖Provider的宽容度。
2.2 对称加密:AES的正确打开方式和必须避开的坑
对称加密是日常项目中使用频率最高的加密方式。JDK里对称加密统一通过Cipher类操作,使用前需要一个SecretKey对象和一个算法参数规范。
import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.security.SecureRandom; import java.util.Base64; public class AesGcmExample { // 生成AES-128密钥 public static SecretKey generateKey() throws Exception { KeyGenerator keyGen = KeyGenerator.getInstance("AES"); keyGen.init(128, new SecureRandom()); return keyGen.generateKey(); } // AES/GCM加密,返回 Base64(IV + 密文) public static String encrypt(String plainText, SecretKey key) throws Exception { byte[] iv = new byte[12]; new SecureRandom().nextBytes(iv); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv)); byte[] cipherBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); byte[] message = new byte[iv.length + cipherBytes.length]; System.arraycopy(iv, 0, message, 0, iv.length); System.arraycopy(cipherBytes, 0, message, iv.length, cipherBytes.length); return Base64.getEncoder().encodeToString(message); } // 解密前先拆出IV public static String decrypt(String encryptedMessage, SecretKey key) throws Exception { byte[] message = Base64.getDecoder().decode(encryptedMessage); byte[] iv = new byte[12]; System.arraycopy(message, 0, iv, 0, 12); byte[] cipherBytes = new byte[message.length - 12]; System.arraycopy(message, 12, cipherBytes, 0, message.length - 12); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv)); return new String(cipher.doFinal(cipherBytes), StandardCharsets.UTF_8); } }这里有几个细节要说明。IV的长度固定12字节,这是GCM模式推荐的长度;GCMParameterSpec的第一个参数是GCM认证标签的比特长度,JDK支持128、120、112、104、96几种,业务中一般直接用128。整个消息体用“IV+密文”拼接存储,解密时先拆出IV再解,这是最常见的消息封装格式。
为什么推荐AES/GCM而不是AES/CBC?GCM是AEAD(Authenticated Encryption with Associated Data)模式,加密的同时生成完整性校验标签,密文只要被改过一个字节,解密就会失败。CBC模式只做机密性保护,不防篡改,要防篡改还得额外叠加HMAC,操作复杂度高且容易出错。对绝大多数业务系统,AES/GCM/NoPadding是目前最好的默认选择。
还有一个高频坑是IllegalBlockSizeException。加密时如果使用AES/CBC/PKCS5Padding,要求待加密数据必须是任意长度(因为有填充),但如果使用AES/ECB/NoPadding这类无填充模式,明文长度必须严格等于16字节的整数倍,否则直接抛异常。AES/GCM/NoPadding虽然名字里带NoPadding,但GCM是流密码模式,不要求明文长度对齐,这点和ECB模式完全不同。
2.3 非对称加密与数字签名:RSA的正确打开方式
非对称加密最适合的场景是“少量敏感数据加密”和“身份认证”。
import javax.crypto.Cipher; import java.security.*; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; public class RsaExample { // 生成RSA密钥对,密钥长度2048 public static KeyPair generateKeyPair() throws Exception { KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); keyPairGen.initialize(2048); return keyPairGen.generateKeyPair(); } // 公钥加密 public static String encryptWithPublicKey(String plainText, PublicKey publicKey) throws Exception { Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.ENCRYPT_MODE, publicKey); return Base64.getEncoder().encodeToString(cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8))); } // 私钥解密 public static String decryptWithPrivateKey(String cipherText, PrivateKey privateKey) throws Exception { Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.DECRYPT_MODE, privateKey); return new String(cipher.doFinal(Base64.getDecoder().decode(cipherText)), StandardCharsets.UTF_8); } }RSA的使用误区集中在密钥长度和填充模式上。JDK默认模板生成的RSA密钥长度通常是2048位,安全强度足够;1024位在2013年后已经被业界正式弃用。填充模式方面,RSA/ECB/PKCS1Padding是最常用的;RSA/ECB/OAEPWithSHA-256AndMGF1Padding安全性更高,但兼容性不如PKCS1,和旧系统对接时容易出问题。
非对称加密的性能比对称加密慢至少两个数量级,而且RSA能加密的明文长度受密钥长度限制,2048位密钥最多只能加密约245字节的数据。所以实际系统里几乎没有人直接用RSA加密大段内容,都是“对称加密数据 + 非对称加密对称密钥”的混合加密方案。用AES加密业务数据,用RSA加密AES密钥,兼顾安全和性能。
数字签名则是非对称加密的另一种形态。JDK里的Signature类封装了签名和验签流程,业务中最常用的是SHA256withRSA。签名时传入私钥和原始数据,验签时传入公钥、原始数据和签名值。和加密相反,签名是“私钥签名、公钥验签”,因为公钥是公开分发的,谁都可以验证签名的真实性,只有持有私钥的一方才能生成签名。
2.4 哈希算法的选型:别再用了MD5和SHA-1
哈希算法在Java里的API非常简单,但选型问题却最容易翻车。
import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class HashExample { public static String sha256(String input) throws NoSuchAlgorithmException { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] hashBytes = digest.digest(input.getBytes(StandardCharsets.UTF_8)); StringBuilder hexString = new StringBuilder(); for (byte b : hashBytes) { String hex = Integer.toHexString(0xff & b); if (hex.length() == 1) hexString.append('0'); hexString.append(hex); } return hexString.toString(); } }哈希的API就这一个核心方法,把输入字节流变成固定长度的摘要字节数组,再转十六进制字符串。难点在于选哪个算法:MD5和SHA-1在碰撞攻击面前已经不安全,不能用于密码存储和签名场景;SHA-256是目前最低安全标准;如果做密码存储,不应该只用单次SHA-256,而应该使用带随机盐的PBKDF2、bcrypt、scrypt这类专门为密码设计的慢哈希算法。
JDK对PBKDF2的支持是SecretKeyFactory:
import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import java.security.SecureRandom; import java.util.Base64; public class Pbkdf2Example { public static String hashPassword(char[] password) throws Exception { byte[] salt = new byte[16]; new SecureRandom().nextBytes(salt); PBEKeySpec spec = new PBEKeySpec(password, salt, 65536, 256); SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); byte[] hash = factory.generateSecret(spec).getEncoded(); return Base64.getEncoder().encodeToString(salt) + "$" + Base64.getEncoder().encodeToString(hash); } }迭代次数65536是当前比较合理的下限。这个值越高,暴力破解的成本越高,但用户登录时等待时间也越长。实际操作时建议在开发环境用较小的迭代次数,生产环境按单次认证耗时200ms到500ms来压测调整。
3. 实操过程与核心环节实现
3.1 第三方库选型对比:BouncyCastle、Commons Codec、Jasypt和Tink
JDK自带的加密能力能覆盖大约80%的场景,剩下的要么是算法缺失,要么是API用起来太繁琐。下面是几个常用的第三方库对比。
| 库 | 核心价值 | 典型用途 | 引入成本 |
|---|---|---|---|
| BouncyCastle | 算法种类极全,支持国密SM2/SM3/SM4 | 国密合规、新算法实验 | 依赖体积较大 |
| Apache Commons Codec | Base64/Hex/SHA系列静态方法封装 | 快速做编码、哈希转换 | 轻量 |
| Jasypt | 配置项透明加解密 | Spring配置文件中敏感信息加密 | 与运行时结合紧 |
| Google Tink | 高层封装,默认搭配安全参数 | 新项目快速集成加密能力 | 依赖管理需适应 |
我个人对BouncyCastle的使用频率最高。除了国密算法之外,它还提供了大量JDK没有的算法实现。不过BouncyCastle的使用方式需要熟悉一下,因为它同时支持两种用法:一种是作为JCA的Provider注册,另一种是直接调用它自己的API类。推荐前者,注册为Provider之后,Cipher.getInstance里就可以传入BouncyCastle支持的算法名,代码风格和JDK原生写法保持一致。
import org.bouncycastle.jce.provider.BouncyCastleProvider; import javax.crypto.Cipher; import java.security.Security; public class BcProviderExample { static { if (Security.getProvider("BC") == null) { Security.addProvider(new BouncyCastleProvider()); } } // 注册后使用国密SM4加密 public static byte[] sm4Encrypt(byte[] key, byte[] data) throws Exception { SecretKeySpec keySpec = new SecretKeySpec(key, "SM4"); Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS5Padding", "BC"); // 实际使用中IV也需要随机生成并随密文一起存储 cipher.init(Cipher.ENCRYPT_MODE, keySpec); return cipher.doFinal(data); } }注册Provider的代码应该放到项目启动时只执行一次,不要在每个加密方法里反复注册,Security.addProvider本身有全局状态,重复注册不仅浪费还会留下隐患。BouncyCastle 1.46之后有个坑:在Java 9以上环境中,未配置java.security文件的Provider注册可能导致NoSuchAlgorithmException,因为模块化系统限制了Provider的加载。解决方法是显式调用Security.addProvider(new BouncyCastleProvider())并且在Cipher.getInstance第二参传入"BC"。
3.2 一个混合加密的完整落地样例:AES加密业务数据、RSA加密AES密钥
实际项目中很少单独使用一种加密算法,最典型的是混合加密方案。明文先用AES-GCM加密,AES密钥用接收方的RSA公钥加密,最终把“RSA加密后的密钥 + AES加密后的数据”一起传给对方。
public class HybridCrypto { // 用RSA公钥加密AES密钥 public static byte[] encryptAesKey(SecretKey aesKey, PublicKey rsaPublicKey) throws Exception { Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.ENCRYPT_MODE, rsaPublicKey); return cipher.doFinal(aesKey.getEncoded()); } // 用RSA私钥解密出AES密钥 public static SecretKey decryptAesKey(byte[] encryptedAesKey, PrivateKey rsaPrivateKey) throws Exception { Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.DECRYPT_MODE, rsaPrivateKey); byte[] aesKeyBytes = cipher.doFinal(encryptedAesKey); return new SecretKeySpec(aesKeyBytes, "AES"); } }整个流程中需要特别注意顺序:AES密钥是随机生成的,每次加密都必须生成一把新的临时密钥,用完即弃,不要复用同一个AES密钥加密所有数据;也不要让AES密钥以明文形式出现在任何日志或报文里。RSA加密的密钥长度受限于密钥长度,2048位RSA密钥最大能加密约245字节的数据,AES-128密钥只有16字节,完全没问题。
这种方案的落地价值在于:对称加密性能好,适合加密任意大小的数据;非对称加密安全性高,用于保护密钥的传输。两个算法各司其职,不会因为某一方的短板拖累整个流程。
3.3 国密算法替换的完整思路:SM2/SM3/SM4的引入与注意事项
国内金融、政务类和部分敏感行业在合规层面会要求使用国密算法。国密算法不是简单的算法替换,它涉及密钥长度、签名机制、消息格式等多个层面的变化。
- SM4是有国家标准的分组对称加密算法,分组长度128位,密钥长度128位,大致对应AES的地位。和AES不同的是,SM4的S盒设计、密钥扩展算法不同,不能直接用AES的API传入SM4的密钥。
- SM3是密码杂凑算法,输出256位摘要,大致对应SHA-256的地位。JDK原生不支持,需要BouncyCastle。
- SM2是非对称加密算法,基于椭圆曲线,大致对应RSA的地位。和RSA不同,SM2加密结果包含三个部分,解密时必须严格区分。
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.util.encoders.Hex; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.security.Security; public class Sm4Example { static { Security.addProvider(new BouncyCastleProvider()); } public static String sm4EncryptHex(String plainText, byte[] key, byte[] iv) throws Exception { SecretKeySpec keySpec = new SecretKeySpec(key, "SM4"); IvParameterSpec ivSpec = new IvParameterSpec(iv); Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS5Padding", "BC"); cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); return Hex.toHexString(cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8))); } public static String sm4DecryptHex(String cipherHex, byte[] key, byte[] iv) throws Exception { SecretKeySpec keySpec = new SecretKeySpec(key, "SM4"); IvParameterSpec ivSpec = new IvParameterSpec(iv); Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS5Padding", "BC"); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); return new String(cipher.doFinal(Hex.decode(cipherHex)), StandardCharsets.UTF_8); } }国密替换涉及一个非常普遍的问题:旧系统已经用AES加密了存量数据,上线国密后,这些数据怎么处理?最稳妥的做法是双算法并行窗口期,新写入数据用SM4加密,旧数据在读取时用AES解密并迁移,等所有数据都迁移完成后再彻底下线AES的加密模块。这个迁移方案看起来慢,但比“一次性全局替换”安全得多。
3.4 配置信息和数据库敏感字段加密的实操
再分享一个非常贴近日常需求的场景:配置文件里的数据库密码、API密钥不能明文存放。Jasypt可以透明地处理这类需求。
import org.jasypt.util.text.AES256TextEncryptor; public class JasyptExample { public static String encryptConfigValue(String plainText, String password) { AES256TextEncryptor encryptor = new AES256TextEncryptor(); encryptor.setPassword(password); return encryptor.encrypt(plainText); } public static String decryptConfigValue(String encryptedText, String password) { AES256TextEncryptor encryptor = new AES256TextEncryptor(); encryptor.setPassword(password); return encryptor.decrypt(encryptedText); } }数据库里敏感字段的加密,我的建议是尽量用“应用层加密”,而不是数据库自带的加密功能。应用层加密的优点是密钥掌握在应用手里,数据库管理员即使直连数据库也看不到明文;缺点是查询条件里不能直接对加密字段做模糊匹配,需要在设计阶段规划好哪些字段允许加密后精确匹配,哪些字段不能加密查询。比如身份证号,如果业务上需要按身份证号模糊搜索,就不要直接加密存原文,而是额外存一列哈希值用于精确匹配查询。
4. 常见问题与排查技巧实录
4.1 “NoSuchAlgorithmException”到底是谁惹的祸
这个异常可能是加密开发中遇到次数最多的错误。报错的字面意思是“算法不存在”,但实际原因多种多样。
- Provider里确实没有这个算法,这是最正常的情况,比如JDK原生没有SM4,不引入BouncyCastle直接使用就会报这个错。
- 算法字符串格式错误,比如把
AES/CBC/PKCS5Padding写成了AES/CBC/PKCS7。 - Provider版本太旧,不支持新算法。比如老版本的BouncyCastle不支持某个较新的曲线参数。
- 在Java 9+模块化环境中,Provider没有被正确注册。
排查这个问题的标准步骤是:先确认算法名是否拼写无误,再从代码里检查getInstance传入的参数是否完整包含了“算法/模式/填充”;然后用Security.getProviders()打印当前所有注册的Provider及其支持的算法列表,确认对应算法是否在其中。最后确认Provider的注册时机,注册代码是否在getInstance调用之前执行完毕。
4.2 “IllegalBlockSizeException”和“BadPaddingException”的排查路径
解密时抛BadPaddingException是第二高频的错误。这个异常的意思是解密后验证填充数据失败,即“解密出来的内容不是合法的填充模式”。
最常见的原因是密钥不对。密钥不匹配时,解密得到的是乱码,乱码的最后几个字节很难恰好满足填充格式,于是框架直接抛出BadPaddingException。这个异常的底层机制其实是密码学里一个重要的设计:如果填充验证失败,说明要么密钥错误、要么密文被篡改、要么算法参数(如IV)不匹配。在RSA场景中,BadPaddingException还有一个隐含风险:有些实现会暴露“填充是否合法”这个信息,攻击者可以利用它做选择密文攻击。JDK现有实现已经对这个类攻击做了防护机制,但作为使用者,不要在业务日志里把这些异常细节原样输出。
排查思路:逐个变量确认。先检查密钥是否和加密时一致,特别注意SecretKeySpec的算法参数是否写对,比如把"AES"写成了"RSA",即使字节一样也无法解密;再检查IV,CBC和GCM模式下IV不一致,解密必失败;最后检查密文在Base64转换过程中是否被做了URL编码之类的额外处理,导致字节变化。
4.3 编码问题:Base64和Hex转换导致密文“变了”
加密结果是二进制字节流,不能直接打印成字符串,更不能直接拼到JSON里传输,必须先做编码转换。Base64和Hex是两种最常见的编码方式。
这里有一个性能细节:Hex编码的输出长度是原始字节的两倍,Base64编码的输出长度约为原始的1.33倍。传输场景优先用Base64,因为体积更小;调试场景优先用Hex,因为可读性更好。
编码转换中的经典问题是:密码文本里的+和/在URL传输时会被转义。如果密文经过URL参数传递,必须使用URL安全的Base64编码:Base64.getUrlEncoder()提供的编码会把+和/替换为-和_,同时省略=填充。解码时对应使用Base64.getUrlDecoder()。
4.4 密钥管理最容易忽视的几件事
密钥管理是整个加密体系里最重要也最容易被忽视的部分。不少项目把AES密钥直接写死在代码里,或者放在配置文件中。这种做法一旦密钥泄露,所有加密数据全部作废。
比较务实的做法是把密钥保存到独立的密钥管理文件或KMS服务中,代码里只放密钥的引用标识。对中小型项目,至少也要做到密钥文件和代码仓库分离:数据库密码这类低敏感密钥放在单独的配置中心;核心业务的加密主密钥不要和配置文件放在同一个目录,可以通过环境变量注入或者从专门的密钥服务拉取。
密钥的轮换机制也要提前设计。任何密钥都有生命周期,轮换不是“换个密钥重新加密”这么简单。设计时要考虑旧数据如何处理,比如AES加密的数据用了旧密钥,轮换后是否需要用新密钥重新加密?如果旧数据保留旧密钥解密,则密钥版本管理必须做起来。常见的做法是密文中带上密钥版本号,解密时根据版本号选取对应密钥。
5. 性能对比与选型建议速查
5.1 三种典型加密算法的性能实测对比
加密算法选型时,性能是一个躲不开的指标。我基于JDK 17的默认Provider做了一轮简单的基准测试,测试数据样本为10MB的随机字节数组。
| 算法 | 耗时(相对值) | 适用场景 |
|---|---|---|
| AES/GCM/NoPadding | 1倍(最快) | 业务数据加密 |
| RSA-2048加密 | 约为AES的10倍以上 | 少量密钥或摘要加密 |
| RSA-2048解密 | 约为AES的100倍以上 | 服务端私钥解密 |
| SHA-256 | 与AES相当 | 数据完整性 |
| PBKDF2(65536次迭代) | 人为变慢 | 密码存储 |
AES加解密在硬件支持AES-NI指令集时速度极快,10MB数据在毫秒级就能完成。RSA的相对性能就慢很多,尤其是私钥解密。这直接影响了混合加密方案的设计:RSA只用来加密16字节的AES密钥,解密一次也只有一次RSA运算,对整体性能影响很小。相反,如果拿RSA去加密大量业务数据,服务端会产生严重的性能瓶颈。
性能实测带来的另一个启发是:不要为了“性能”去选不安全的哈希算法。MD5确实比SHA-256快,但这种速度差异在绝大多数业务场景下感知不到,而安全风险是实打实的。性能优化应该在算法选型合理的前提下做,而不是用安全性换速度。
5.2 方案选型的最终建议
| 业务场景 | 推荐方案 | 替代方案 |
|---|---|---|
| 数据存储加密 | AES/GCM/NoPadding | SM4/CBC/PKCS5Padding(国密合规) |
| 敏感数据传输 | TLS + AES-GCM | 应用层混合加密 |
| 密码存储 | PBKDF2WithHmacSHA256 | bcrypt/scrypt |
| 接口防篡改 | HMAC-SHA256 | RSA签名 |
| 身份认证 | RSA/SHA256withRSA签名 | SM2签名 |
| 配置敏感信息 | Jasypt | 自研配置解密模块 |
选型的最低判断标准是“新项目不要再用MD5/SHA-1做任何安全用途,优先使用SHA-256及以上强度的算法;对称加密优先使用AES-GCM;非对称加密优先使用RSA-2048或以上,国密场景使用SM2/SM4/SM3”。这套组合兼顾了安全性、性能和生态成熟度。
6. 我对Java加密类库的几点个人体会
回到开头那个问题:项目里到底该用自带的包还是第三方类库?我的回答在实践过程中发生过变化。
刚开始写加密代码的时候,我倾向于“第三方库万能”的思路。遇到问题先搜一个库,引入依赖,调完接口就完了。后来维护的项目多了,看到不少团队因为过度依赖第三方库把自己拖进依赖地狱——引入BouncyCastle只为了做一次Base64编码,引入整个Spring Security框架只为了加个盐。这种成本和收益完全不成比例。
现在我的做法是:默认先看JDK能不能做。Cipher、MessageDigest、Signature、SecretKeyFactory这几个核心类解决大部分需求完全没有问题,而且JDK自带的实现经过了全球范围内的安全审计和性能打磨,稳定性和安全性反而比大多数第三方库更有保障。只有当JDK能力确实覆盖不到的时候再去引入第三方库,比如国密算法、特殊曲线参数、或者一些协议层的加密实现。
还有一个容易被忽略的点:加密代码的测试一定要覆盖“跨环境运行”的场景。同样的密文,在本地跑和在生产环境跑,结果可能不一样,因为声音这是Provider列表、JDK版本甚至操作系统安全策略的差异。比如Java 8和Java 17在某些加密算法上的默认值就不同。如果你要把加密模块从旧项目迁移到新项目,务必在不同的JDK版本上跑一遍加解密回归测试。这个坑我踩过不止一次,每次都是老项目升级JDK版本之后,存量密文突然解不开了。
加密这件事,代码好写,难的是在自己的项目里建立一个“什么时候该用哪个,出了问题怎么查”的完整认知框架。希望这篇文章能帮你省下一些搜索和踩坑的时间。如果你正在设计新项目的加密方案,建议先画出自己需要保护的资产清单——哪些数据要机密性、哪些要完整性、哪些要防抵赖,再去匹配算法和方案,而不是反过来从算法出发找场景。方向对了,具体怎么写代码反而是最简单的一步。