1. 项目概述为什么Java开发者必须掌握加密算法最近在整理团队的技术资产发现一个挺有意思的现象很多做了两三年的Java开发对Spring Boot、微服务这些框架玩得挺溜但一涉及到数据安全比如用户密码怎么存、接口传输怎么防篡改第一反应还是去网上找个MD5工具类。问起AES和RSA的区别或者为什么现在不推荐直接用MD5往往就有点含糊了。这其实是个挺普遍的问题框架用多了一些底层但至关重要的基础反而被忽略了。加密算法就是这样一个“底层且至关重要”的基础。它不是什么高深莫测的黑科技而是我们每天写代码时保护用户数据、确保系统安全的基石。无论是用户登录时的密码校验还是支付时敏感信息的传输甚至是配置文件里数据库连接密码的隐藏都离不开它。标题里的“常用加密算法汇总”目的就是把Java生态里这些最常用、最该会的加密知识给你掰开揉碎了讲清楚。这不是一份冷冰冰的API文档罗列而是结合我这些年踩过的坑、最佳实践总结出来的一份“生存指南”。你会发现搞懂了这些你不仅能写出更安全的代码在面试中被问到“加密与安全”这类八股文时也能言之有物知道背后的所以然。这篇文章适合所有阶段的Java开发者。如果你是新手可以把它当作一份入门到精通的路线图如果你是有经验的开发者可以重点看“注意事项”和“常见问题”部分查漏补缺看看自己的用法是否有安全隐患。我们的目标很简单让你在需要用到加密时能快速、准确地选出合适的算法并用正确的方式实现它避免那些常见的“坑”。2. 加密算法核心分类与选型逻辑在动手写代码之前我们必须先理清一个根本问题面对不同的场景我该用哪种加密算法乱用算法比不用更危险比如用哈希算法去加密需要解密的数据或者用对称加密去解决数字签名问题都会导致系统设计出现致命缺陷。加密算法主要分为三大类哈希算法、对称加密算法和非对称加密算法。它们各有各的“职责”和“脾气”。2.1 哈希算法单向的“指纹”提取器哈希算法的核心特点是单向性和固定输出。你把任意长度的数据比如一个文件、一段密码丢给它它会计算出一个固定长度的、看似随机的字符串哈希值。这个过程是不可逆的你无法从哈希值反推出原始数据。同时理想情况下不同的数据输入会产生截然不同的哈希值抗碰撞。在Java中最常见的哈希算法就是MD5和SHA系列如SHA-1, SHA-256。它们通常用来校验数据完整性下载一个文件后计算其哈希值与官方提供的哈希值对比一致则说明文件未被篡改。存储用户密码这是哈希算法最经典的应用。服务器不存储用户明文密码只存储其哈希值。用户登录时服务器对输入的密码进行相同的哈希计算然后与存储的哈希值比对。但这里有个关键进化早期直接使用MD5或SHA-1哈希密码的方式已被淘汰因为彩虹表攻击可以快速破解简单哈希。现在必须使用加盐Salt和慢哈希算法如PBKDF2, bcrypt, scrypt。注意MD5和SHA-1已被证实存在严重的安全弱点可以人为制造碰撞即两个不同的数据产生相同的哈希值。因此绝对不要将它们用于任何安全敏感的场景如数字签名或密码存储。对于校验文件完整性这类对抗性不强的场景SHA-256是更安全的选择。2.2 对称加密算法同一把钥匙的锁与开锁对称加密顾名思义加密和解密使用同一把密钥。就像你用同一把钥匙锁门和开门。它的优点是速度快适合加密大量数据。Java中内建的常用对称加密算法是AES高级加密标准。它已经取代了老旧的DES和3DES成为国际标准。AES又根据密钥长度分为AES-128、AES-192和AES-256密钥越长安全性越高但计算开销也略大。对于绝大多数应用AES-128已经足够安全。对称加密的核心挑战在于密钥管理。如何安全地把密钥分发给需要通信的双方如果密钥在传输中被截获整个加密体系就崩塌了。因此它常用于加密存储在本地的数据如加密的配置文件或者用于加密通信中的“会话密钥”。2.3 非对称加密算法公钥锁私钥开非对称加密使用一对密钥公钥和私钥。公钥公开给所有人私钥自己严格保密。用公钥加密的数据只有对应的私钥才能解密用私钥签名的数据任何人都可以用公钥验证签名是否来自私钥持有者。Java中典型的代表是RSA算法。它的速度比对称加密慢很多所以通常不直接用于加密大量数据。它的主要用途是密钥交换解决对称加密的密钥分发难题。通信方A用B的公钥加密一个随机生成的对称密钥然后发给BB用自己的私钥解密得到对称密钥。后续通信就用这个对称密钥进行高速的AES加密。数字签名A用私钥对一段数据的哈希值进行加密即签名然后将数据和签名一起发出。B用A的公钥解密签名得到哈希值H1再计算收到数据的哈希值H2如果H1等于H2则证明数据确实来自A且未被篡改。选型逻辑速查表场景推荐算法关键理由存储用户密码PBKDF2WithHmacSHA256 / bcrypt / scrypt慢哈希、加盐抗彩虹表攻击校验文件完整性SHA-256 / SHA-512抗碰撞性强安全性高加密数据库字段或配置文件AES (GCM模式)速度快支持认证加密防篡改HTTPS/API传输层加密TLS协议 (底层通常使用ECDHE密钥交换AES加密)行业标准结合了非对称和对称加密优势代码或文档签名RSA (配合SHA-256)广泛支持用于验证发布者身份3. 核心算法Java实现与最佳实践理论清楚了我们来看代码。Java通过JCAJava密码体系结构和JCEJava密码学扩展提供了丰富的加密支持。我们不用重复造轮子但必须学会正确、安全地使用这些轮子。3.1 密码存储告别MD5拥抱PBKDF2与bcrypt错误示范绝对要避免// 警告这是极不安全的做法 public static String md5Password(String password) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(password.getBytes(StandardCharsets.UTF_8)); return bytesToHex(digest); // 转换为十六进制字符串存储 } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } }这种方式无盐、哈希速度快一个简单的彩虹表就能破解大部分常用密码。正确实践一使用PBKDF2PBKDF2Password-Based Key Derivation Function 2通过多次哈希迭代来增加计算成本从而抵御暴力破解。import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import java.security.SecureRandom; import java.security.spec.KeySpec; import java.util.Base64; public class PasswordUtil { private static final int ITERATION_COUNT 100000; // 迭代次数建议10万次以上 private static final int KEY_LENGTH 256; // 密钥长度 private static final int SALT_LENGTH 16; // 盐值长度16字节128位 // 生成盐值并加密密码 public static String encryptPassword(String password) throws Exception { SecureRandom random new SecureRandom(); byte[] salt new byte[SALT_LENGTH]; random.nextBytes(salt); // 生成随机盐 KeySpec spec new PBEKeySpec(password.toCharArray(), salt, ITERATION_COUNT, KEY_LENGTH); SecretKeyFactory factory SecretKeyFactory.getInstance(PBKDF2WithHmacSHA256); byte[] hash factory.generateSecret(spec).getEncoded(); // 存储格式迭代次数:盐值:哈希值 便于验证时解析 return ITERATION_COUNT : Base64.getEncoder().encodeToString(salt) : Base64.getEncoder().encodeToString(hash); } // 验证密码 public static boolean verifyPassword(String inputPassword, String storedPassword) throws Exception { String[] parts storedPassword.split(:); int iterations Integer.parseInt(parts[0]); byte[] salt Base64.getDecoder().decode(parts[1]); byte[] storedHash Base64.getDecoder().decode(parts[2]); KeySpec spec new PBEKeySpec(inputPassword.toCharArray(), salt, iterations, KEY_LENGTH); SecretKeyFactory factory SecretKeyFactory.getInstance(PBKDF2WithHmacSHA256); byte[] inputHash factory.generateSecret(spec).getEncoded(); // 使用恒定时间比较防止时序攻击 return MessageDigest.isEqual(inputHash, storedHash); } }关键点解析随机盐Salt每个密码都有独一无二的盐彻底杜绝彩虹表攻击。盐不需要保密与哈希值一起存储即可。高迭代次数如10万次显著增加哈希计算时间使暴力破解成本急剧上升。使用SecureRandom生成密码学安全的随机数避免伪随机数生成器如Random带来的可预测性风险。恒定时间比较使用MessageDigest.isEqual()而不是Arrays.equals()防止通过比较耗时差异来推测密码正确位的时序攻击。正确实践二使用bcrypt更推荐对于新项目我更推荐使用bcrypt。它内部自动处理了加盐并且迭代次数工作因子是可配置的随着硬件性能提升可以增加工作因子来保持安全性。Spring Security等框架都内置了bcrypt支持。// 通常借助库如BCryptPasswordEncoder (来自Spring Security) import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; public class BcryptDemo { public static void main(String[] args) { BCryptPasswordEncoder encoder new BCryptPasswordEncoder(12); // 工作因子默认10越高越安全也越慢 String rawPassword mySecretPassword; String encodedPassword encoder.encode(rawPassword); // 输出类似$2a$12$SomeRandomSaltAndHash... System.out.println(加密后 encodedPassword); boolean matches encoder.matches(rawPassword, encodedPassword); System.out.println(验证结果 matches); } }bcrypt的哈希值字符串自身就包含了算法版本、工作因子、盐和哈希结果管理起来非常方便。3.2 对称加密AES的GCM模式实战AES有多种工作模式如ECB, CBC, GCM。ECB模式是极不安全的因为它会导致相同的明文块加密成相同的密文块泄露数据模式。CBC模式需要手动处理填充和初始化向量IV且需要单独的消息认证码MAC来保证完整性容易用错。目前最佳选择是GCM模式Galois/Counter Mode。它属于“认证加密”模式同时提供保密性加密和完整性防篡改且API相对友好。import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; import java.security.SecureRandom; import java.util.Base64; public class AesGcmUtil { private static final int AES_KEY_SIZE 128; // 也可以是 192 或 256 private static final int GCM_TAG_LENGTH 128; // 认证标签长度单位比特 private static final int GCM_IV_LENGTH 12; // 推荐IV长度12字节96位 // 生成密钥 public static SecretKey generateKey() throws Exception { KeyGenerator keyGen KeyGenerator.getInstance(AES); keyGen.init(AES_KEY_SIZE); return keyGen.generateKey(); } // 加密 public static String encrypt(String plaintext, SecretKey key) throws Exception { byte[] iv new byte[GCM_IV_LENGTH]; SecureRandom random new SecureRandom(); random.nextBytes(iv); // 每次加密必须使用不同的IV Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); GCMParameterSpec spec new GCMParameterSpec(GCM_TAG_LENGTH, iv); cipher.init(Cipher.ENCRYPT_MODE, key, spec); byte[] ciphertext cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); // 将IV和密文拼接在一起存储/传输 byte[] encryptedData new byte[iv.length ciphertext.length]; System.arraycopy(iv, 0, encryptedData, 0, iv.length); System.arraycopy(ciphertext, 0, encryptedData, iv.length, ciphertext.length); return Base64.getEncoder().encodeToString(encryptedData); } // 解密 public static String decrypt(String base64EncryptedData, SecretKey key) throws Exception { byte[] encryptedData Base64.getDecoder().decode(base64EncryptedData); byte[] iv new byte[GCM_IV_LENGTH]; System.arraycopy(encryptedData, 0, iv, 0, iv.length); Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); GCMParameterSpec spec new GCMParameterSpec(GCM_TAG_LENGTH, iv); cipher.init(Cipher.DECRYPT_MODE, key, spec); byte[] ciphertext new byte[encryptedData.length - GCM_IV_LENGTH]; System.arraycopy(encryptedData, GCM_IV_LENGTH, ciphertext, 0, ciphertext.length); byte[] plaintext cipher.doFinal(ciphertext); return new String(plaintext, StandardCharsets.UTF_8); } }实操心得IV必须唯一且随机对于同一个密钥每次加密都必须使用一个新的、密码学安全的随机IV。重复使用IV会严重破坏GCM模式的安全性。IV不需要保密通常和密文一起存储。密钥管理是关键AES密钥必须安全存储。可以考虑使用硬件安全模块HSM、云服务商的密钥管理服务KMS或者至少使用环境变量或配置中心加密存储而不是硬编码在代码里。异常处理cipher.doFinal()在解密失败如认证标签校验不通过时会抛出AEADBadTagException。这其实是一个安全特性告诉你数据可能被篡改了一定要捕获并妥善处理不要简单地忽略。3.3 非对称加密RSA的密钥交换与签名RSA加密解密示例常用于加密小数据或密钥import javax.crypto.Cipher; import java.security.*; import java.util.Base64; public class RsaUtil { // 生成密钥对 public static KeyPair generateKeyPair() throws NoSuchAlgorithmException { KeyPairGenerator keyGen KeyPairGenerator.getInstance(RSA); keyGen.initialize(2048); // 密钥长度目前推荐至少2048位 return keyGen.generateKeyPair(); } // 公钥加密 public static String encryptWithPublicKey(String plaintext, PublicKey publicKey) throws Exception { Cipher cipher Cipher.getInstance(RSA/ECB/OAEPWithSHA-256AndMGF1Padding); // 使用OAEP填充模式更安全 cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] ciphertext cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(ciphertext); } // 私钥解密 public static String decryptWithPrivateKey(String base64Ciphertext, PrivateKey privateKey) throws Exception { Cipher cipher Cipher.getInstance(RSA/ECB/OAEPWithSHA-256AndMGF1Padding); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] ciphertext Base64.getDecoder().decode(base64Ciphertext); byte[] plaintext cipher.doFinal(ciphertext); return new String(plaintext, StandardCharsets.UTF_8); } }重要提醒RSA算法有明文长度限制。对于2048位的密钥使用OAEP填充时能加密的明文最大长度约为 256字节 - 42字节填充开销≈ 214字节。所以它不能直接用于加密大文件或长文本通常只用于加密一个随机的AES会话密钥。RSA数字签名示例import java.security.*; public class RsaSignatureUtil { // 私钥签名 public static byte[] sign(String message, PrivateKey privateKey) throws Exception { Signature signer Signature.getInstance(SHA256withRSA); signer.initSign(privateKey); signer.update(message.getBytes(StandardCharsets.UTF_8)); return signer.sign(); } // 公钥验签 public static boolean verify(String message, byte[] signature, PublicKey publicKey) throws Exception { Signature verifier Signature.getInstance(SHA256withRSA); verifier.initVerify(publicKey); verifier.update(message.getBytes(StandardCharsets.UTF_8)); return verifier.verify(signature); } }签名过程是先对消息计算哈希这里用SHA-256然后用私钥加密这个哈希值。验证过程是用公钥解密签名得到哈希值H1再计算消息的哈希值H2对比H1和H2。4. 国密算法SM的Java集成与应用在一些对信息安全有特定要求的领域如金融、政务可能会要求使用国家密码管理局认定的国产商用密码算法国密算法。其中SM4对称加密和SM2非对称加密基于椭圆曲线是核心。Java标准库并未内置这些算法需要引入第三方库如Bouncy CastleBCProvider。4.1 引入Bouncy Castle依赖以Maven为例dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk15on/artifactId version1.70/version !-- 使用最新稳定版 -- /dependency4.2 SM4 ECB模式加密示例仅作演示生产慎用ECBimport org.bouncycastle.jce.provider.BouncyCastleProvider; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.security.Security; import java.util.Base64; public class Sm4Util { static { // 在程序启动时添加Bouncy Castle Provider Security.addProvider(new BouncyCastleProvider()); } public static SecretKey generateSm4Key() throws Exception { KeyGenerator kg KeyGenerator.getInstance(SM4, BC); // 指定算法和Provider kg.init(128); // SM4密钥固定为128位 return kg.generateKey(); } public static String encryptEcb(String plaintext, SecretKey key) throws Exception { Cipher cipher Cipher.getInstance(SM4/ECB/PKCS5Padding, BC); cipher.init(Cipher.ENCRYPT_MODE, key); byte[] ciphertext cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(ciphertext); } public static String decryptEcb(String base64Ciphertext, SecretKey key) throws Exception { Cipher cipher Cipher.getInstance(SM4/ECB/PKCS5Padding, BC); cipher.init(Cipher.DECRYPT_MODE, key); byte[] ciphertext Base64.getDecoder().decode(base64Ciphertext); byte[] plaintext cipher.doFinal(ciphertext); return new String(plaintext, StandardCharsets.UTF_8); } }再次强调ECB模式不安全仅用于演示算法调用。生产环境应使用SM4的CBC或GCM模式如果BC库支持并妥善管理IV。SM2的集成更为复杂涉及椭圆曲线密钥对生成、签名验签等需要参考Bouncy Castle的详细文档进行实现。5. 实战中的常见“坑”与排查技巧即使选对了算法写对了代码在实际部署和运行中你依然可能会遇到各种奇怪的问题。下面是我总结的几个高频“坑点”和解决方法。5.1 “InvalidKeyException: Illegal key size” 或 “NoSuchProviderException”问题描述在使用AES-256或某些高强度算法时可能会抛出密钥长度非法的异常。根本原因Java默认的“受限策略文件”限制了加密强度。历史上出于美国出口管制法律的要求JDK默认限制了加密密钥的长度。解决方案推荐检查你的JDK版本对于JDK 8u151/8u152及以上版本以及所有JDK 9及以上版本已经默认解除了这个限制。你可以通过运行以下代码来检查System.out.println(Max AES Key Length: Cipher.getMaxAllowedKeyLength(AES));如果输出是2147483647说明限制已解除。旧版本JDK手动替换策略文件去Oracle官网下载对应你JDK版本的“Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files”。将下载的jar包local_policy.jar和US_export_policy.jar复制到${JAVA_HOME}/jre/lib/security/目录下覆盖原文件。注意生产环境务必使用已解除限制的JDK版本或自行替换策略文件并确保所有部署节点服务器、容器的JDK环境一致。5.2 “BadPaddingException: Given final block not properly padded”问题描述在解密时特别是使用RSA或AES的CBC模式时经常遇到此异常。排查思路密钥不匹配这是最常见的原因。确保加密和解密使用的是完全相同的密钥。对于对称加密检查密钥是否被意外修改或编码如Base64后没有正确解码。对于非对称加密确认用的是正确的公钥/私钥对。算法/模式/填充不匹配加密时指定的完整算法字符串如AES/CBC/PKCS5Padding必须与解密时一字不差。不同Provider的写法可能略有差异确保一致。IV问题CBC/GCM模式解密时使用的IV必须和加密时使用的IV完全相同。检查你的IV存储和还原逻辑。数据被篡改或损坏在传输或存储过程中密文可能被截断、修改或编码出错如Base64解码错误。确保密文完整、正确地传递。调试技巧在开发阶段可以将密钥、IV、加密前的明文、加密后的密文Hex或Base64格式全部打印出来。在加密和解密两端对比这些值能快速定位问题所在。5.3 性能瓶颈与优化加密解密是CPU密集型操作不当使用会影响系统性能。密钥生成KeyGenerator.getInstance(...).generateKey()和KeyPairGenerator.getInstance(...).generateKeyPair()是非常耗时的操作。绝对不要在每次加密/解密时都生成新密钥。密钥应该作为配置或秘密信息在应用启动时生成一次或从安全存储中加载然后缓存起来复用。RSA加密大对象牢记RSA不能加密超过其密钥长度限制的数据。对于大文件标准做法是 a. 生成一个随机的AES会话密钥。 b. 用AES会话密钥加密大文件速度快。 c. 用RSA公钥加密这个AES会话密钥数据量小。 d. 将RSA加密后的会话密钥和AES加密后的文件一起存储或发送。使用线程安全的Cipher对象Cipher对象不是线程安全的。不要在多线程间共享同一个Cipher实例。通常的实践是为每个线程或每次操作创建新的Cipher实例或者使用ThreadLocal进行缓存。由于Cipher的初始化init方法也有一定开销在高并发场景下使用ThreadLocal缓存初始化好的Cipher对象是一个常见的优化手段。5.4 密钥的安全存储与生命周期管理这是加密系统中最容易被忽视也最致命的一环。代码再安全密钥泄露了一切归零。禁止硬编码永远不要把密钥直接写在源代码里尤其是提交到版本控制系统如Git。环境变量与配置中心将密钥放在环境变量中或使用加密的配置文件并通过配置中心如Spring Cloud Config with Encryption在应用启动时注入。确保生产服务器的环境变量访问权限严格控制。使用专业的密钥管理服务对于企业级应用应考虑使用HSM或云KMS如AWS KMS, Azure Key Vault, 阿里云KMS。这些服务提供密钥的硬件级保护、自动轮转、访问审计等功能。密钥轮转为密钥设置生命周期定期轮转更换密钥。即使当前密钥泄露轮转后攻击者也无法解密历史数据如果使用了每个数据独立的IV或盐。设计系统时需要考虑密钥版本管理确保新旧密钥在过渡期内都能使用。6. 进阶话题TLS/SSL与应用层加密的边界很多开发者会问“我们的服务已经用了HTTPSTLS/SSL为什么还要在应用代码里做加密” 这是一个非常好的问题涉及到安全边界的划分。TLS/SSL传输层安全保障的是数据在网络传输过程中的安全即“管道安全”。它解决了窃听、篡改和冒充问题。数据到达目标服务器后会被解密。如果服务器被入侵或者数据需要落盘存入数据库、缓存、日志那么这些静态数据就处于明文状态。应用层加密即本文讨论的加密保障的是数据本身的安全而不管它处于传输中还是静止中。即使传输管道是安全的或者数据已经存储在数据库里只要没有解密密钥数据就是一堆乱码。最佳实践内外兼修对外提供的API、网站必须使用HTTPSTLS 1.2。这是底线。按需加密对于特别敏感的数据如身份证号、银行卡号、医疗记录即使在内网传输或存储也应进行应用层加密。这就是“端到端加密”或“字段级加密”的思想。密钥隔离TLS证书的私钥和应用数据的加密密钥应该分开管理降低单点沦陷的风险。我个人在实际项目中的做法是对于核心的用户PII个人身份信息数据在持久化到数据库之前一定会用应用层的AES-GCM再进行一次加密。TLS管“路上”的安全我自己的加密管“家里”的安全。这样就算数据库备份文件被拖库或者有内鬼直接访问了数据库看到的也是加密后的密文心里会踏实很多。最后再分享一个小技巧在团队中推行加密规范时不要只给出一份文档。最好能提供一个经过安全审计的、开箱即用的工具类库就像本文中的代码示例那样并附上清晰的单元测试和场景示例。降低开发者的使用门槛才能让安全实践真正落地。毕竟最安全的算法如果因为用起来太麻烦而被绕过那也等于零。
Java加密算法实战指南:从哈希、AES到RSA与国密SM4
1. 项目概述为什么Java开发者必须掌握加密算法最近在整理团队的技术资产发现一个挺有意思的现象很多做了两三年的Java开发对Spring Boot、微服务这些框架玩得挺溜但一涉及到数据安全比如用户密码怎么存、接口传输怎么防篡改第一反应还是去网上找个MD5工具类。问起AES和RSA的区别或者为什么现在不推荐直接用MD5往往就有点含糊了。这其实是个挺普遍的问题框架用多了一些底层但至关重要的基础反而被忽略了。加密算法就是这样一个“底层且至关重要”的基础。它不是什么高深莫测的黑科技而是我们每天写代码时保护用户数据、确保系统安全的基石。无论是用户登录时的密码校验还是支付时敏感信息的传输甚至是配置文件里数据库连接密码的隐藏都离不开它。标题里的“常用加密算法汇总”目的就是把Java生态里这些最常用、最该会的加密知识给你掰开揉碎了讲清楚。这不是一份冷冰冰的API文档罗列而是结合我这些年踩过的坑、最佳实践总结出来的一份“生存指南”。你会发现搞懂了这些你不仅能写出更安全的代码在面试中被问到“加密与安全”这类八股文时也能言之有物知道背后的所以然。这篇文章适合所有阶段的Java开发者。如果你是新手可以把它当作一份入门到精通的路线图如果你是有经验的开发者可以重点看“注意事项”和“常见问题”部分查漏补缺看看自己的用法是否有安全隐患。我们的目标很简单让你在需要用到加密时能快速、准确地选出合适的算法并用正确的方式实现它避免那些常见的“坑”。2. 加密算法核心分类与选型逻辑在动手写代码之前我们必须先理清一个根本问题面对不同的场景我该用哪种加密算法乱用算法比不用更危险比如用哈希算法去加密需要解密的数据或者用对称加密去解决数字签名问题都会导致系统设计出现致命缺陷。加密算法主要分为三大类哈希算法、对称加密算法和非对称加密算法。它们各有各的“职责”和“脾气”。2.1 哈希算法单向的“指纹”提取器哈希算法的核心特点是单向性和固定输出。你把任意长度的数据比如一个文件、一段密码丢给它它会计算出一个固定长度的、看似随机的字符串哈希值。这个过程是不可逆的你无法从哈希值反推出原始数据。同时理想情况下不同的数据输入会产生截然不同的哈希值抗碰撞。在Java中最常见的哈希算法就是MD5和SHA系列如SHA-1, SHA-256。它们通常用来校验数据完整性下载一个文件后计算其哈希值与官方提供的哈希值对比一致则说明文件未被篡改。存储用户密码这是哈希算法最经典的应用。服务器不存储用户明文密码只存储其哈希值。用户登录时服务器对输入的密码进行相同的哈希计算然后与存储的哈希值比对。但这里有个关键进化早期直接使用MD5或SHA-1哈希密码的方式已被淘汰因为彩虹表攻击可以快速破解简单哈希。现在必须使用加盐Salt和慢哈希算法如PBKDF2, bcrypt, scrypt。注意MD5和SHA-1已被证实存在严重的安全弱点可以人为制造碰撞即两个不同的数据产生相同的哈希值。因此绝对不要将它们用于任何安全敏感的场景如数字签名或密码存储。对于校验文件完整性这类对抗性不强的场景SHA-256是更安全的选择。2.2 对称加密算法同一把钥匙的锁与开锁对称加密顾名思义加密和解密使用同一把密钥。就像你用同一把钥匙锁门和开门。它的优点是速度快适合加密大量数据。Java中内建的常用对称加密算法是AES高级加密标准。它已经取代了老旧的DES和3DES成为国际标准。AES又根据密钥长度分为AES-128、AES-192和AES-256密钥越长安全性越高但计算开销也略大。对于绝大多数应用AES-128已经足够安全。对称加密的核心挑战在于密钥管理。如何安全地把密钥分发给需要通信的双方如果密钥在传输中被截获整个加密体系就崩塌了。因此它常用于加密存储在本地的数据如加密的配置文件或者用于加密通信中的“会话密钥”。2.3 非对称加密算法公钥锁私钥开非对称加密使用一对密钥公钥和私钥。公钥公开给所有人私钥自己严格保密。用公钥加密的数据只有对应的私钥才能解密用私钥签名的数据任何人都可以用公钥验证签名是否来自私钥持有者。Java中典型的代表是RSA算法。它的速度比对称加密慢很多所以通常不直接用于加密大量数据。它的主要用途是密钥交换解决对称加密的密钥分发难题。通信方A用B的公钥加密一个随机生成的对称密钥然后发给BB用自己的私钥解密得到对称密钥。后续通信就用这个对称密钥进行高速的AES加密。数字签名A用私钥对一段数据的哈希值进行加密即签名然后将数据和签名一起发出。B用A的公钥解密签名得到哈希值H1再计算收到数据的哈希值H2如果H1等于H2则证明数据确实来自A且未被篡改。选型逻辑速查表场景推荐算法关键理由存储用户密码PBKDF2WithHmacSHA256 / bcrypt / scrypt慢哈希、加盐抗彩虹表攻击校验文件完整性SHA-256 / SHA-512抗碰撞性强安全性高加密数据库字段或配置文件AES (GCM模式)速度快支持认证加密防篡改HTTPS/API传输层加密TLS协议 (底层通常使用ECDHE密钥交换AES加密)行业标准结合了非对称和对称加密优势代码或文档签名RSA (配合SHA-256)广泛支持用于验证发布者身份3. 核心算法Java实现与最佳实践理论清楚了我们来看代码。Java通过JCAJava密码体系结构和JCEJava密码学扩展提供了丰富的加密支持。我们不用重复造轮子但必须学会正确、安全地使用这些轮子。3.1 密码存储告别MD5拥抱PBKDF2与bcrypt错误示范绝对要避免// 警告这是极不安全的做法 public static String md5Password(String password) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(password.getBytes(StandardCharsets.UTF_8)); return bytesToHex(digest); // 转换为十六进制字符串存储 } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } }这种方式无盐、哈希速度快一个简单的彩虹表就能破解大部分常用密码。正确实践一使用PBKDF2PBKDF2Password-Based Key Derivation Function 2通过多次哈希迭代来增加计算成本从而抵御暴力破解。import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import java.security.SecureRandom; import java.security.spec.KeySpec; import java.util.Base64; public class PasswordUtil { private static final int ITERATION_COUNT 100000; // 迭代次数建议10万次以上 private static final int KEY_LENGTH 256; // 密钥长度 private static final int SALT_LENGTH 16; // 盐值长度16字节128位 // 生成盐值并加密密码 public static String encryptPassword(String password) throws Exception { SecureRandom random new SecureRandom(); byte[] salt new byte[SALT_LENGTH]; random.nextBytes(salt); // 生成随机盐 KeySpec spec new PBEKeySpec(password.toCharArray(), salt, ITERATION_COUNT, KEY_LENGTH); SecretKeyFactory factory SecretKeyFactory.getInstance(PBKDF2WithHmacSHA256); byte[] hash factory.generateSecret(spec).getEncoded(); // 存储格式迭代次数:盐值:哈希值 便于验证时解析 return ITERATION_COUNT : Base64.getEncoder().encodeToString(salt) : Base64.getEncoder().encodeToString(hash); } // 验证密码 public static boolean verifyPassword(String inputPassword, String storedPassword) throws Exception { String[] parts storedPassword.split(:); int iterations Integer.parseInt(parts[0]); byte[] salt Base64.getDecoder().decode(parts[1]); byte[] storedHash Base64.getDecoder().decode(parts[2]); KeySpec spec new PBEKeySpec(inputPassword.toCharArray(), salt, iterations, KEY_LENGTH); SecretKeyFactory factory SecretKeyFactory.getInstance(PBKDF2WithHmacSHA256); byte[] inputHash factory.generateSecret(spec).getEncoded(); // 使用恒定时间比较防止时序攻击 return MessageDigest.isEqual(inputHash, storedHash); } }关键点解析随机盐Salt每个密码都有独一无二的盐彻底杜绝彩虹表攻击。盐不需要保密与哈希值一起存储即可。高迭代次数如10万次显著增加哈希计算时间使暴力破解成本急剧上升。使用SecureRandom生成密码学安全的随机数避免伪随机数生成器如Random带来的可预测性风险。恒定时间比较使用MessageDigest.isEqual()而不是Arrays.equals()防止通过比较耗时差异来推测密码正确位的时序攻击。正确实践二使用bcrypt更推荐对于新项目我更推荐使用bcrypt。它内部自动处理了加盐并且迭代次数工作因子是可配置的随着硬件性能提升可以增加工作因子来保持安全性。Spring Security等框架都内置了bcrypt支持。// 通常借助库如BCryptPasswordEncoder (来自Spring Security) import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; public class BcryptDemo { public static void main(String[] args) { BCryptPasswordEncoder encoder new BCryptPasswordEncoder(12); // 工作因子默认10越高越安全也越慢 String rawPassword mySecretPassword; String encodedPassword encoder.encode(rawPassword); // 输出类似$2a$12$SomeRandomSaltAndHash... System.out.println(加密后 encodedPassword); boolean matches encoder.matches(rawPassword, encodedPassword); System.out.println(验证结果 matches); } }bcrypt的哈希值字符串自身就包含了算法版本、工作因子、盐和哈希结果管理起来非常方便。3.2 对称加密AES的GCM模式实战AES有多种工作模式如ECB, CBC, GCM。ECB模式是极不安全的因为它会导致相同的明文块加密成相同的密文块泄露数据模式。CBC模式需要手动处理填充和初始化向量IV且需要单独的消息认证码MAC来保证完整性容易用错。目前最佳选择是GCM模式Galois/Counter Mode。它属于“认证加密”模式同时提供保密性加密和完整性防篡改且API相对友好。import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; import java.security.SecureRandom; import java.util.Base64; public class AesGcmUtil { private static final int AES_KEY_SIZE 128; // 也可以是 192 或 256 private static final int GCM_TAG_LENGTH 128; // 认证标签长度单位比特 private static final int GCM_IV_LENGTH 12; // 推荐IV长度12字节96位 // 生成密钥 public static SecretKey generateKey() throws Exception { KeyGenerator keyGen KeyGenerator.getInstance(AES); keyGen.init(AES_KEY_SIZE); return keyGen.generateKey(); } // 加密 public static String encrypt(String plaintext, SecretKey key) throws Exception { byte[] iv new byte[GCM_IV_LENGTH]; SecureRandom random new SecureRandom(); random.nextBytes(iv); // 每次加密必须使用不同的IV Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); GCMParameterSpec spec new GCMParameterSpec(GCM_TAG_LENGTH, iv); cipher.init(Cipher.ENCRYPT_MODE, key, spec); byte[] ciphertext cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); // 将IV和密文拼接在一起存储/传输 byte[] encryptedData new byte[iv.length ciphertext.length]; System.arraycopy(iv, 0, encryptedData, 0, iv.length); System.arraycopy(ciphertext, 0, encryptedData, iv.length, ciphertext.length); return Base64.getEncoder().encodeToString(encryptedData); } // 解密 public static String decrypt(String base64EncryptedData, SecretKey key) throws Exception { byte[] encryptedData Base64.getDecoder().decode(base64EncryptedData); byte[] iv new byte[GCM_IV_LENGTH]; System.arraycopy(encryptedData, 0, iv, 0, iv.length); Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); GCMParameterSpec spec new GCMParameterSpec(GCM_TAG_LENGTH, iv); cipher.init(Cipher.DECRYPT_MODE, key, spec); byte[] ciphertext new byte[encryptedData.length - GCM_IV_LENGTH]; System.arraycopy(encryptedData, GCM_IV_LENGTH, ciphertext, 0, ciphertext.length); byte[] plaintext cipher.doFinal(ciphertext); return new String(plaintext, StandardCharsets.UTF_8); } }实操心得IV必须唯一且随机对于同一个密钥每次加密都必须使用一个新的、密码学安全的随机IV。重复使用IV会严重破坏GCM模式的安全性。IV不需要保密通常和密文一起存储。密钥管理是关键AES密钥必须安全存储。可以考虑使用硬件安全模块HSM、云服务商的密钥管理服务KMS或者至少使用环境变量或配置中心加密存储而不是硬编码在代码里。异常处理cipher.doFinal()在解密失败如认证标签校验不通过时会抛出AEADBadTagException。这其实是一个安全特性告诉你数据可能被篡改了一定要捕获并妥善处理不要简单地忽略。3.3 非对称加密RSA的密钥交换与签名RSA加密解密示例常用于加密小数据或密钥import javax.crypto.Cipher; import java.security.*; import java.util.Base64; public class RsaUtil { // 生成密钥对 public static KeyPair generateKeyPair() throws NoSuchAlgorithmException { KeyPairGenerator keyGen KeyPairGenerator.getInstance(RSA); keyGen.initialize(2048); // 密钥长度目前推荐至少2048位 return keyGen.generateKeyPair(); } // 公钥加密 public static String encryptWithPublicKey(String plaintext, PublicKey publicKey) throws Exception { Cipher cipher Cipher.getInstance(RSA/ECB/OAEPWithSHA-256AndMGF1Padding); // 使用OAEP填充模式更安全 cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] ciphertext cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(ciphertext); } // 私钥解密 public static String decryptWithPrivateKey(String base64Ciphertext, PrivateKey privateKey) throws Exception { Cipher cipher Cipher.getInstance(RSA/ECB/OAEPWithSHA-256AndMGF1Padding); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] ciphertext Base64.getDecoder().decode(base64Ciphertext); byte[] plaintext cipher.doFinal(ciphertext); return new String(plaintext, StandardCharsets.UTF_8); } }重要提醒RSA算法有明文长度限制。对于2048位的密钥使用OAEP填充时能加密的明文最大长度约为 256字节 - 42字节填充开销≈ 214字节。所以它不能直接用于加密大文件或长文本通常只用于加密一个随机的AES会话密钥。RSA数字签名示例import java.security.*; public class RsaSignatureUtil { // 私钥签名 public static byte[] sign(String message, PrivateKey privateKey) throws Exception { Signature signer Signature.getInstance(SHA256withRSA); signer.initSign(privateKey); signer.update(message.getBytes(StandardCharsets.UTF_8)); return signer.sign(); } // 公钥验签 public static boolean verify(String message, byte[] signature, PublicKey publicKey) throws Exception { Signature verifier Signature.getInstance(SHA256withRSA); verifier.initVerify(publicKey); verifier.update(message.getBytes(StandardCharsets.UTF_8)); return verifier.verify(signature); } }签名过程是先对消息计算哈希这里用SHA-256然后用私钥加密这个哈希值。验证过程是用公钥解密签名得到哈希值H1再计算消息的哈希值H2对比H1和H2。4. 国密算法SM的Java集成与应用在一些对信息安全有特定要求的领域如金融、政务可能会要求使用国家密码管理局认定的国产商用密码算法国密算法。其中SM4对称加密和SM2非对称加密基于椭圆曲线是核心。Java标准库并未内置这些算法需要引入第三方库如Bouncy CastleBCProvider。4.1 引入Bouncy Castle依赖以Maven为例dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk15on/artifactId version1.70/version !-- 使用最新稳定版 -- /dependency4.2 SM4 ECB模式加密示例仅作演示生产慎用ECBimport org.bouncycastle.jce.provider.BouncyCastleProvider; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.security.Security; import java.util.Base64; public class Sm4Util { static { // 在程序启动时添加Bouncy Castle Provider Security.addProvider(new BouncyCastleProvider()); } public static SecretKey generateSm4Key() throws Exception { KeyGenerator kg KeyGenerator.getInstance(SM4, BC); // 指定算法和Provider kg.init(128); // SM4密钥固定为128位 return kg.generateKey(); } public static String encryptEcb(String plaintext, SecretKey key) throws Exception { Cipher cipher Cipher.getInstance(SM4/ECB/PKCS5Padding, BC); cipher.init(Cipher.ENCRYPT_MODE, key); byte[] ciphertext cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(ciphertext); } public static String decryptEcb(String base64Ciphertext, SecretKey key) throws Exception { Cipher cipher Cipher.getInstance(SM4/ECB/PKCS5Padding, BC); cipher.init(Cipher.DECRYPT_MODE, key); byte[] ciphertext Base64.getDecoder().decode(base64Ciphertext); byte[] plaintext cipher.doFinal(ciphertext); return new String(plaintext, StandardCharsets.UTF_8); } }再次强调ECB模式不安全仅用于演示算法调用。生产环境应使用SM4的CBC或GCM模式如果BC库支持并妥善管理IV。SM2的集成更为复杂涉及椭圆曲线密钥对生成、签名验签等需要参考Bouncy Castle的详细文档进行实现。5. 实战中的常见“坑”与排查技巧即使选对了算法写对了代码在实际部署和运行中你依然可能会遇到各种奇怪的问题。下面是我总结的几个高频“坑点”和解决方法。5.1 “InvalidKeyException: Illegal key size” 或 “NoSuchProviderException”问题描述在使用AES-256或某些高强度算法时可能会抛出密钥长度非法的异常。根本原因Java默认的“受限策略文件”限制了加密强度。历史上出于美国出口管制法律的要求JDK默认限制了加密密钥的长度。解决方案推荐检查你的JDK版本对于JDK 8u151/8u152及以上版本以及所有JDK 9及以上版本已经默认解除了这个限制。你可以通过运行以下代码来检查System.out.println(Max AES Key Length: Cipher.getMaxAllowedKeyLength(AES));如果输出是2147483647说明限制已解除。旧版本JDK手动替换策略文件去Oracle官网下载对应你JDK版本的“Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files”。将下载的jar包local_policy.jar和US_export_policy.jar复制到${JAVA_HOME}/jre/lib/security/目录下覆盖原文件。注意生产环境务必使用已解除限制的JDK版本或自行替换策略文件并确保所有部署节点服务器、容器的JDK环境一致。5.2 “BadPaddingException: Given final block not properly padded”问题描述在解密时特别是使用RSA或AES的CBC模式时经常遇到此异常。排查思路密钥不匹配这是最常见的原因。确保加密和解密使用的是完全相同的密钥。对于对称加密检查密钥是否被意外修改或编码如Base64后没有正确解码。对于非对称加密确认用的是正确的公钥/私钥对。算法/模式/填充不匹配加密时指定的完整算法字符串如AES/CBC/PKCS5Padding必须与解密时一字不差。不同Provider的写法可能略有差异确保一致。IV问题CBC/GCM模式解密时使用的IV必须和加密时使用的IV完全相同。检查你的IV存储和还原逻辑。数据被篡改或损坏在传输或存储过程中密文可能被截断、修改或编码出错如Base64解码错误。确保密文完整、正确地传递。调试技巧在开发阶段可以将密钥、IV、加密前的明文、加密后的密文Hex或Base64格式全部打印出来。在加密和解密两端对比这些值能快速定位问题所在。5.3 性能瓶颈与优化加密解密是CPU密集型操作不当使用会影响系统性能。密钥生成KeyGenerator.getInstance(...).generateKey()和KeyPairGenerator.getInstance(...).generateKeyPair()是非常耗时的操作。绝对不要在每次加密/解密时都生成新密钥。密钥应该作为配置或秘密信息在应用启动时生成一次或从安全存储中加载然后缓存起来复用。RSA加密大对象牢记RSA不能加密超过其密钥长度限制的数据。对于大文件标准做法是 a. 生成一个随机的AES会话密钥。 b. 用AES会话密钥加密大文件速度快。 c. 用RSA公钥加密这个AES会话密钥数据量小。 d. 将RSA加密后的会话密钥和AES加密后的文件一起存储或发送。使用线程安全的Cipher对象Cipher对象不是线程安全的。不要在多线程间共享同一个Cipher实例。通常的实践是为每个线程或每次操作创建新的Cipher实例或者使用ThreadLocal进行缓存。由于Cipher的初始化init方法也有一定开销在高并发场景下使用ThreadLocal缓存初始化好的Cipher对象是一个常见的优化手段。5.4 密钥的安全存储与生命周期管理这是加密系统中最容易被忽视也最致命的一环。代码再安全密钥泄露了一切归零。禁止硬编码永远不要把密钥直接写在源代码里尤其是提交到版本控制系统如Git。环境变量与配置中心将密钥放在环境变量中或使用加密的配置文件并通过配置中心如Spring Cloud Config with Encryption在应用启动时注入。确保生产服务器的环境变量访问权限严格控制。使用专业的密钥管理服务对于企业级应用应考虑使用HSM或云KMS如AWS KMS, Azure Key Vault, 阿里云KMS。这些服务提供密钥的硬件级保护、自动轮转、访问审计等功能。密钥轮转为密钥设置生命周期定期轮转更换密钥。即使当前密钥泄露轮转后攻击者也无法解密历史数据如果使用了每个数据独立的IV或盐。设计系统时需要考虑密钥版本管理确保新旧密钥在过渡期内都能使用。6. 进阶话题TLS/SSL与应用层加密的边界很多开发者会问“我们的服务已经用了HTTPSTLS/SSL为什么还要在应用代码里做加密” 这是一个非常好的问题涉及到安全边界的划分。TLS/SSL传输层安全保障的是数据在网络传输过程中的安全即“管道安全”。它解决了窃听、篡改和冒充问题。数据到达目标服务器后会被解密。如果服务器被入侵或者数据需要落盘存入数据库、缓存、日志那么这些静态数据就处于明文状态。应用层加密即本文讨论的加密保障的是数据本身的安全而不管它处于传输中还是静止中。即使传输管道是安全的或者数据已经存储在数据库里只要没有解密密钥数据就是一堆乱码。最佳实践内外兼修对外提供的API、网站必须使用HTTPSTLS 1.2。这是底线。按需加密对于特别敏感的数据如身份证号、银行卡号、医疗记录即使在内网传输或存储也应进行应用层加密。这就是“端到端加密”或“字段级加密”的思想。密钥隔离TLS证书的私钥和应用数据的加密密钥应该分开管理降低单点沦陷的风险。我个人在实际项目中的做法是对于核心的用户PII个人身份信息数据在持久化到数据库之前一定会用应用层的AES-GCM再进行一次加密。TLS管“路上”的安全我自己的加密管“家里”的安全。这样就算数据库备份文件被拖库或者有内鬼直接访问了数据库看到的也是加密后的密文心里会踏实很多。最后再分享一个小技巧在团队中推行加密规范时不要只给出一份文档。最好能提供一个经过安全审计的、开箱即用的工具类库就像本文中的代码示例那样并附上清晰的单元测试和场景示例。降低开发者的使用门槛才能让安全实践真正落地。毕竟最安全的算法如果因为用起来太麻烦而被绕过那也等于零。