1. 为什么需要AES加密?
在日常开发中,我们经常需要处理敏感数据的存储和传输问题。比如用户的密码、身份证号、银行卡信息等,如果直接以明文形式存储或传输,一旦被黑客截获就会造成严重的安全问题。这时候就需要使用加密技术来保护这些数据。
你可能听说过MD5这种哈希算法,但它只能做单向加密,无法解密。而很多业务场景下我们需要能够还原原始数据,比如加密存储的用户手机号在需要显示时还得能解密出来。这时候对称加密就派上用场了。
对称加密最大的特点就是加密和解密使用同一个密钥,就像我们日常生活中用的钥匙和锁的关系。只有用同一把钥匙才能锁上和打开同一把锁。AES(Advanced Encryption Standard)就是目前最主流的对称加密算法,它已经取代了早期的DES和3DES算法,成为事实上的行业标准。
2. AES加密的核心概念
2.1 分组密码体制
AES采用分组密码的工作方式,它会先把明文数据切分成固定大小的块(block),每个块128位(16字节)。如果最后一块不足16字节,就需要进行填充(Padding)。然后对每个块分别进行加密,最后把所有加密后的块拼接起来形成最终的密文。
这种分组处理的方式有几个关键优势:
- 可以并行处理多个数据块,提高加密效率
- 固定大小的块便于硬件优化实现
- 安全性更高,单个块的破解不会影响其他块
2.2 填充模式(Padding)
由于明文数据长度不一定是16字节的整数倍,所以需要对最后一个块进行填充。AES支持几种常见的填充模式:
- PKCS5/PKCS7:缺少几个字节就填充几个字节的填充值。比如缺5个字节就填充5个0x05
- NoPadding:不填充,要求明文长度必须是16字节的倍数
实际开发中最常用的是PKCS7填充模式。这里有个细节需要注意:如果最后一个块正好是16字节,且内容全是0x10(16),解密时就无法区分这是有效数据还是填充数据。因此PKCS7在这种情况下会自动再追加一个完整的填充块。
2.3 加密模式
AES支持多种加密模式,不同的模式安全性差异很大:
- ECB模式(电子密码本):最简单的模式,相同的明文块会加密成相同的密文块,安全性较差
- CBC模式(密码块链接):引入初始化向量IV,使相同明文加密出不同密文,安全性高
- GCM模式:除了加密还提供认证功能,性能优异
ECB模式因为安全性问题基本不再使用,CBC是最常用的模式,而GCM适合对性能要求高的场景。
2.4 密钥和初始化向量
- 密钥:AES支持128位、192位和256位三种密钥长度。密钥越长安全性越高,但加密速度会变慢。一般128位就足够安全了。
- 初始化向量IV:CBC模式需要的一个随机数,长度也是128位。它的作用是让相同的明文每次加密出不同的密文,提高安全性。
3. Java实现AES加密解密
3.1 解决"Invalid key size"问题
在Java中使用AES-256时,可能会遇到"Illegal key size"错误。这是因为Java默认的加密策略文件限制了密钥长度。解决方法是从Oracle官网下载对应的JCE策略文件:
- 根据JDK版本下载对应的策略文件包
- 解压后得到local_policy.jar和US_export_policy.jar
- 替换JDK和JRE的security目录下的这两个文件
3.2 完整工具类实现
下面是一个完整的AES工具类实现,支持CBC模式和PKCS7填充:
import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.Security; import java.util.Base64; public class AESUtil { static { Security.addProvider(new BouncyCastleProvider()); } /** * AES加密 * @param data 明文 * @param key 密钥(16/24/32字节) * @param iv 初始化向量(16字节) * @return 密文(Base64编码) */ public static String encrypt(String data, String key, String iv) throws Exception { byte[] raw = key.getBytes("UTF-8"); SecretKeySpec keySpec = new SecretKeySpec(raw, "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC"); IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes("UTF-8")); cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte[] encrypted = cipher.doFinal(data.getBytes("UTF-8")); return Base64.getEncoder().encodeToString(encrypted); } /** * AES解密 * @param data 密文(Base64编码) * @param key 密钥(16/24/32字节) * @param iv 初始化向量(16字节) * @return 明文 */ public static String decrypt(String data, String key, String iv) throws Exception { byte[] raw = key.getBytes("UTF-8"); SecretKeySpec keySpec = new SecretKeySpec(raw, "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC"); IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes("UTF-8")); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); byte[] encrypted = Base64.getDecoder().decode(data); byte[] original = cipher.doFinal(encrypted); return new String(original, "UTF-8"); } }3.3 使用示例
public static void main(String[] args) throws Exception { // 16字节的密钥和初始化向量 String key = "1234567890abcdef"; String iv = "abcdef1234567890"; String plainText = "这是一段需要加密的敏感数据"; // 加密 String encrypted = AESUtil.encrypt(plainText, key, iv); System.out.println("加密结果: " + encrypted); // 解密 String decrypted = AESUtil.decrypt(encrypted, key, iv); System.out.println("解密结果: " + decrypted); }4. 实际应用中的注意事项
4.1 密钥管理
密钥的安全性直接决定了加密系统的安全性。在实际项目中:
- 不要硬编码密钥在代码中
- 密钥应该定期更换
- 可以使用密钥管理系统(KMS)来管理密钥
- 对于密码等场景,建议使用PBKDF2等算法从口令派生密钥
4.2 初始化向量IV
- IV不需要保密,但必须随机生成且不可预测
- 同一个密钥下不要重复使用相同的IV
- IV可以随密文一起存储和传输
4.3 性能优化
- 对于大量数据,可以考虑分块处理
- 现代CPU都支持AES-NI指令集,可以显著提升加解密速度
- 根据安全需求选择合适的密钥长度,不必盲目使用256位
4.4 常见问题排查
- 密钥长度不匹配:确保密钥长度是16/24/32字节
- IV长度错误:CBC模式需要16字节的IV
- 填充异常:加密解密要使用相同的填充模式
- 字符编码问题:确保加密解密使用相同的字符编码(推荐UTF-8)
5. AES与其他加密算法的对比
5.1 AES vs DES/3DES
- 密钥长度:DES只有56位,3DES是112/168位,而AES支持128/192/256位
- 安全性:DES已被证明不安全,3DES逐渐被淘汰,AES是目前最安全的选择
- 性能:AES的软件和硬件实现效率都高于3DES
5.2 AES vs RSA
- 加密类型:AES是对称加密,RSA是非对称加密
- 速度:AES比RSA快很多,适合加密大量数据
- 用途:通常配合使用 - RSA加密AES密钥,AES加密实际数据
在实际项目中,我遇到过一个典型问题:使用ECB模式加密的用户数据,当数据中有大量重复内容时,密文也呈现出明显的模式,这给安全分析带来了风险。后来我们全部迁移到CBC模式,配合随机IV生成,彻底解决了这个问题。这也让我深刻理解了加密模式选择的重要性。