Java对称加密实战:从AES/DES原理到安全实现与避坑指南

📅 2026/7/4 10:11:06 👁️ 阅读次数 📝 编程学习
Java对称加密实战:从AES/DES原理到安全实现与避坑指南

1. 项目概述:为什么对称加密是Java开发者的必修课?

在Java开发中,无论是处理用户敏感信息、保护配置文件,还是实现安全的网络通信,加密都是一个绕不开的话题。而对称加密,特别是AES和DES,作为其中最经典、应用最广泛的算法,几乎成了每一位后端开发者工具箱里的标配。你可能在面试中被问到“AES和DES的区别”,也可能在项目中接到一个“把数据库里的手机号加密存储”的需求。但真正动手时,很多人会发现,从知道概念到写出安全、健壮的代码,中间隔着一道鸿沟。网上教程要么只给几行代码片段,语焉不详;要么大谈特谈数学原理,让人望而却步。这篇文章,我就以一个踩过无数坑的过来人身份,带你从零开始,彻底搞懂如何在Java中实现AES和DES对称加密。我们不只讲“怎么用”,更要深挖“为什么这么用”,以及那些官方文档里不会写的“坑”和“技巧”。无论你是正在准备面试的求职者,还是需要解决实际加密需求的开发者,这篇内容都能给你一份可以直接“抄作业”的实战指南。

2. 对称加密核心原理与算法选型

2.1 什么是对称加密?一个简单的类比

想象一下你和朋友约定了一个简单的密码规则:把每个字母往后移三位。比如“HELLO”就变成了“KHOOR”。你们双方都知道这个“移三位”的规则(这就是密钥),所以你能加密,他也能解密。这就是对称加密最朴素的思想:加密和解密使用同一把钥匙

在计算机世界里,这把“钥匙”就是一串二进制数据(密钥),而“移三位”的规则就是加密算法(如AES、DES)。它的最大优点是计算速度快,适合加密大量数据,比如整个文件或数据库字段。但核心挑战也随之而来:密钥如何安全地共享给接收方?如果密钥在传输中被截获,整个加密体系就形同虚设。因此,在实际系统中,对称加密通常不会单独使用,而是和RSA等非对称加密结合,由非对称加密来安全地传递那把对称加密的密钥。

2.2 AES vs DES:为什么AES是当今的绝对主流?

在Java的javax.crypto包中,我们最常打交道的两个对称加密算法就是DES和AES。理解它们的区别,是正确选型的第一步。

DES (Data Encryption Standard):曾经的王者,如今的退役老兵DES诞生于1970年代,密钥长度固定为56位(通常说的64位包含了8位奇偶校验位)。在当年,它的安全性是革命性的。但随着计算机算力的指数级增长,56位的密钥空间(约72千万亿种可能)在现代暴力破解面前已显得力不从心。1999年,一台专门设计的机器可以在22小时内破解DES。因此,单纯的DES在需要安全性的场合已被认为是不安全的。不过,为了兼容老系统,你仍然可能在遗留代码中看到它。

AES (Advanced Encryption Standard):现代的加密基石为了取代DES,美国国家标准与技术研究院(NIST)在2000年选中了Rijndael算法作为AES标准。AES支持128、192和256位三种密钥长度,其设计极大地增强了抵抗各种密码分析攻击的能力。128位的AES,其密钥空间就已达3.4 x 10^38,以目前的技术水平,暴力破解在理论上不可行。它已成为全球范围内金融、政府、互联网行业数据加密的事实标准。在Java中,除非有极其特殊的兼容性要求,无脑选择AES就对了

注意:虽然我们讨论DES,但实际生产环境中,如果遇到使用DES的旧系统,更安全的做法是将其迁移到AES,或者至少使用3DES(对数据执行三次DES加密,密钥长度等效提升至112或168位)作为过渡。Java中也提供了DESede(即3DES)的实现。

2.3 算法模式与填充模式:容易被忽略的关键细节

选定了AES,你的工作只完成了一半。接下来两个关键参数直接决定了加密的安全性和正确性:工作模式填充模式

工作模式 (Cipher Mode):定义如何重复应用算法

  • ECB (Electronic Codebook):最简单的模式,将数据分成块,每块独立加密。致命缺点:相同的明文块会产生相同的密文块,无法隐藏数据模式。一张纯色图片加密后可能还能看出轮廓。绝对不要用于加密有意义的数据
  • CBC (Cipher Block Chaining):最常用的模式之一。每个明文块在加密前,会先与前一个密文块进行异或操作。这需要一个初始化向量(IV)来启动这个过程。IV不需要保密,但必须是随机且不可预测的,通常和密文一起存储或传输。CBC能很好地隐藏数据模式。
  • 其他模式:如GCM(Galois/Counter Mode)不仅能提供保密性,还能提供完整性认证,是当前推荐用于新系统的模式,尤其在网络传输中。

填充模式 (Padding Scheme):处理最后一块不完整的数据AES是块加密算法,一次处理一个固定大小的数据块(如128位)。如果明文长度不是块大小的整数倍,就需要填充。

  • PKCS5Padding / PKCS7Padding:最常用的填充方式。对于AES(块大小16字节),如果最后缺N个字节,就填充N个值为N的字节。解密后会自动去除。在Java中,PKCS5Padding实际上用于8字节块(如DES),但标准库允许将其用于AES,底层会按PKCS7Padding处理。
  • NoPadding:不填充。要求明文长度必须是块大小的整数倍,否则会抛出异常。除非你能严格控制数据长度,否则不建议使用。

一个完整的算法标识符在Java中通常这样表示:AES/CBC/PKCS5Padding。它指明了算法、模式和填充。

3. Java实现AES加密解密全流程解析

理论说再多,不如一行代码。下面我们进入实战环节,我会用一个工具类的形式,展示AES加密解密的完整实现,并穿插讲解每一个参数和步骤背后的考量。

3.1 核心工具类设计与依赖

我们首先构建一个AESUtil工具类。Java标准库javax.crypto已经提供了我们所需的一切,无需额外依赖。

import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Base64; public class AESUtil { // 推荐使用AES-256,但需要注意JCE无限制强度策略文件 private static final String ALGORITHM = "AES"; private static final String TRANSFORMATION_CBC = "AES/CBC/PKCS5Padding"; private static final String TRANSFORMATION_GCM = "AES/GCM/NoPadding"; // GCM模式不需要填充 private static final int AES_KEY_SIZE = 256; // 密钥长度:128, 192, 256 private static final int GCM_TAG_LENGTH = 128; // GCM认证标签长度,单位比特 // Base64编码器/解码器,用于将二进制密文转换为可安全传输的字符串 private static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder(); private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder(); }

关键点解析:

  1. 算法与变换:我们将算法(AES)和模式/填充(AES/CBC/PKCS5Padding)分开定义。Cipher类初始化时需要的是“变换”字符串。
  2. 密钥长度:设置为256位以提供最高安全强度。但请注意,Java默认的JCE策略文件可能限制密钥长度为128位。如果需要使用192或256位,需要为JRE安装“Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files”。这是一个常见的部署坑。
  3. GCM模式:作为一种认证加密模式,它同时提供保密性和完整性。我们单独定义其变换字符串。GCM模式使用NoPadding,因为它本身是一种流加密模式。

3.2 密钥的生成与管理:安全的第一道门

密钥是整个加密体系的根基。密钥的生成、存储和传递必须万分小心。

public class AESUtil { // ... 上文代码 /** * 生成一个随机的AES密钥 * @return 生成的SecretKey */ public static SecretKey generateKey() throws NoSuchAlgorithmException { KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM); // 使用强随机数生成器初始化密钥生成器 keyGen.init(AES_KEY_SIZE, SecureRandom.getInstanceStrong()); return keyGen.generateKey(); } /** * 将密钥转换为Base64编码的字符串,便于存储或传输(需确保存储环境安全!) * @param secretKey 密钥 * @return Base64编码的密钥字符串 */ public static String convertKeyToString(SecretKey secretKey) { return BASE64_ENCODER.encodeToString(secretKey.getEncoded()); } /** * 从Base64编码的字符串还原密钥 * @param keyStr Base64编码的密钥字符串 * @return 还原的SecretKey */ public static SecretKey convertStringToKey(String keyStr) { byte[] decodedKey = BASE64_DECODER.decode(keyStr); // AES-256需要32字节的密钥材料 return new SecretKeySpec(decodedKey, ALGORITHM); } }

实操心得与避坑指南:

  1. 永远使用SecureRandom:初始化KeyGenerator时,务必使用SecureRandom.getInstanceStrong()new SecureRandom()。绝对不要使用普通的Random类,它的随机性是可预测的,会严重削弱密钥安全性。
  2. 密钥存储是老大难问题:将密钥转换成Base64字符串,只是为了演示和临时存储。生产环境中,绝不能将密钥硬编码在代码里或明文存放在配置文件中。常见的做法是:
    • 使用专门的密钥管理服务(KMS),如云服务商提供的产品。
    • 在应用启动时,从受保护的环境变量或加密的配置中心获取密钥。
    • 对于极其敏感的系统,可以使用硬件安全模块(HSM)。
  3. 密钥生命周期管理:密钥需要定期轮换。设计系统时,应考虑支持多版本密钥,以便在轮换期间,旧数据仍能被解密。

3.3 实现CBC模式加密与解密

CBC模式因其平衡的安全性和广泛兼容性,是目前最常见的模式。它的核心是初始化向量IV。

public class AESUtil { // ... 上文代码 /** * 使用CBC模式加密 * @param plainText 明文 * @param secretKey 密钥 * @return 一个包含IV和密文的字符串(格式: IV_base64:cipherText_base64) */ public static String encryptWithCBC(String plainText, SecretKey secretKey) throws Exception { // 1. 获取Cipher实例 Cipher cipher = Cipher.getInstance(TRANSFORMATION_CBC); // 2. 生成一个随机的16字节初始化向量(IV) byte[] iv = new byte[16]; // AES块大小是16字节 SecureRandom secureRandom = new SecureRandom(); secureRandom.nextBytes(iv); IvParameterSpec ivSpec = new IvParameterSpec(iv); // 3. 初始化Cipher为加密模式,传入密钥和IV cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); // 4. 执行加密 byte[] cipherTextBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); // 5. 将IV和密文一起编码返回。IV不需要保密,但必须唯一且随机。 String ivBase64 = BASE64_ENCODER.encodeToString(iv); String cipherTextBase64 = BASE64_ENCODER.encodeToString(cipherTextBytes); return ivBase64 + ":" + cipherTextBase64; } /** * 使用CBC模式解密 * @param encryptedText 加密后的字符串(格式: IV_base64:cipherText_base64) * @param secretKey 密钥 * @return 解密后的明文 */ public static String decryptWithCBC(String encryptedText, SecretKey secretKey) throws Exception { // 1. 拆分出IV和密文 String[] parts = encryptedText.split(":"); if (parts.length != 2) { throw new IllegalArgumentException("Invalid encrypted text format"); } byte[] iv = BASE64_DECODER.decode(parts[0]); byte[] cipherTextBytes = BASE64_DECODER.decode(parts[1]); // 2. 获取Cipher实例并初始化为解密模式 Cipher cipher = Cipher.getInstance(TRANSFORMATION_CBC); IvParameterSpec ivSpec = new IvParameterSpec(iv); cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec); // 3. 执行解密 byte[] plainTextBytes = cipher.doFinal(cipherTextBytes); return new String(plainTextBytes, StandardCharsets.UTF_8); } }

关键细节与注意事项:

  1. IV必须随机且唯一:每次加密都必须使用一个新的、随机的IV。重复使用相同的IV和密钥加密不同数据,会严重削弱CBC模式的安全性。这就是为什么我们要用SecureRandom生成IV。
  2. IV需要和密文一起存储/传输:IV本身不是秘密,可以公开。我们通常将其与密文拼接在一起(如用分隔符:),接收方需要先提取IV才能解密。
  3. 字符编码:在getBytes()new String()时,务必明确指定字符编码(如UTF-8)。忽略这一点,在不同平台(操作系统默认编码不同)间可能导致解密后乱码。
  4. 异常处理doFinal()方法可能抛出BadPaddingException等异常。这通常意味着密钥错误、IV错误或数据在传输过程中被篡改。在实际应用中,需要妥善处理这些异常,避免将详细的错误信息暴露给最终用户。

3.4 实现更现代的GCM模式加密与解密

GCM模式是当前更推荐的选择,因为它提供了认证功能,能同时防范窃听和篡改。

public class AESUtil { // ... 上文代码 /** * 使用GCM模式加密 * @param plainText 明文 * @param secretKey 密钥 * @return 一个包含IV和密文和认证标签的字符串(格式: IV_base64:cipherText_base64) */ public static String encryptWithGCM(String plainText, SecretKey secretKey) throws Exception { Cipher cipher = Cipher.getInstance(TRANSFORMATION_GCM); // GCM模式也需要一个IV,有时称为Nonce byte[] iv = new byte[12]; // 通常推荐12字节的Nonce SecureRandom secureRandom = new SecureRandom(); secureRandom.nextBytes(iv); // 使用GCMParameterSpec指定IV和认证标签长度 GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv); cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec); byte[] cipherTextBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); String ivBase64 = BASE64_ENCODER.encodeToString(iv); String cipherTextBase64 = BASE64_ENCODER.encodeToString(cipherTextBytes); return ivBase64 + ":" + cipherTextBase64; } /** * 使用GCM模式解密 * @param encryptedText 加密后的字符串(格式: IV_base64:cipherText_base64) * @param secretKey 密钥 * @return 解密后的明文 * @throws javax.crypto.AEADBadTagException 如果认证失败(数据被篡改或密钥错误) */ public static String decryptWithGCM(String encryptedText, SecretKey secretKey) throws Exception { String[] parts = encryptedText.split(":"); if (parts.length != 2) { throw new IllegalArgumentException("Invalid encrypted text format"); } byte[] iv = BASE64_DECODER.decode(parts[0]); byte[] cipherTextBytes = BASE64_DECODER.decode(parts[1]); Cipher cipher = Cipher.getInstance(TRANSFORMATION_GCM); GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv); cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec); byte[] plainTextBytes = cipher.doFinal(cipherTextBytes); return new String(plainTextBytes, StandardCharsets.UTF_8); } }

GCM模式的优势与注意点:

  1. 认证加密:GCM在加密的同时会生成一个认证标签(Tag)。解密时,会先验证这个标签。如果密文在传输中被哪怕修改了一个比特,或者使用了错误的密钥/IV,解密都会抛出AEADBadTagException。这比CBC模式能提供更强的安全保障(CBC模式下,篡改可能只导致部分解密乱码,而不会被立即发现)。
  2. Nonce(IV)要求:GCM对Nonce的唯一性要求极其严格。绝对不能用同一个(密钥,Nonce)对加密两条不同的消息,否则会完全破坏安全性。因此,确保Nonce的唯一性(通常通过强随机数生成)至关重要。
  3. 性能:GCM模式在现代CPU上通常有很好的硬件加速支持,性能优异。

4. Java实现DES加密解密(为了兼容性与理解)

尽管不推荐在新项目中使用DES,但为了理解原理和维护旧代码,我们同样实现一个工具类。其结构与AES类似,但参数不同。

4.1 DES工具类实现

import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Base64; public class DESUtil { private static final String ALGORITHM = "DES"; private static final String TRANSFORMATION_CBC = "DES/CBC/PKCS5Padding"; private static final int DES_KEY_SIZE = 56; // 实际是56位,但KeyGenerator参数通常用64 private static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder(); private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder(); // 生成DES密钥 public static SecretKey generateKey() throws NoSuchAlgorithmException { KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM); keyGen.init(DES_KEY_SIZE, SecureRandom.getInstanceStrong()); return keyGen.generateKey(); } // DES CBC模式加密 (IV长度是8字节) public static String encryptWithCBC(String plainText, SecretKey secretKey) throws Exception { Cipher cipher = Cipher.getInstance(TRANSFORMATION_CBC); byte[] iv = new byte[8]; // DES块大小是8字节 SecureRandom secureRandom = new SecureRandom(); secureRandom.nextBytes(iv); IvParameterSpec ivSpec = new IvParameterSpec(iv); cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); byte[] cipherTextBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); String ivBase64 = BASE64_ENCODER.encodeToString(iv); String cipherTextBase64 = BASE64_ENCODER.encodeToString(cipherTextBytes); return ivBase64 + ":" + cipherTextBase64; } // DES CBC模式解密 public static String decryptWithCBC(String encryptedText, SecretKey secretKey) throws Exception { String[] parts = encryptedText.split(":"); if (parts.length != 2) { throw new IllegalArgumentException("Invalid encrypted text format"); } byte[] iv = BASE64_DECODER.decode(parts[0]); byte[] cipherTextBytes = BASE64_DECODER.decode(parts[1]); Cipher cipher = Cipher.getInstance(TRANSFORMATION_CBC); IvParameterSpec ivSpec = new IvParameterSpec(iv); cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec); byte[] plainTextBytes = cipher.doFinal(cipherTextBytes); return new String(plainTextBytes, StandardCharsets.UTF_8); } // 密钥与字符串的转换(与AES类似,略) }

DES实现的特殊说明:

  1. 密钥长度:虽然我们指定DES_KEY_SIZE=56,但KeyGenerator可能会根据实现有所不同。DES的有效密钥长度是56位,加上8位奇偶校验位,共64位。
  2. 块大小与IV:DES的块大小是64位(8字节),因此IV的长度也是8字节。
  3. 再次强调安全性:请仅在处理历史遗留数据或与无法升级的旧系统交互时使用DES。对于新数据,务必使用AES。

5. 实战中的常见问题与排查技巧实录

在实际开发中,直接运行上面的代码可能一帆风顺,但一旦集成到复杂系统或遇到边缘情况,各种问题就会接踵而至。下面是我总结的几个最常见的问题和解决方法。

5.1 异常:java.security.InvalidKeyException: Illegal key size

问题现象:使用AES-256生成密钥或初始化Cipher时,抛出此异常。根本原因:Java默认的加密强度受限于美国的出口管制政策。标准JRE自带的“local_policy.jar”和“US_export_policy.jar”策略文件限制了加密密钥的长度。解决方案

  1. 确认你的Java版本java -version)。对于Java 8 Update 161或更高版本,以及Java 9及以上版本,默认已启用无限制强度策略。
  2. 如果版本较旧,需要手动下载并替换JRE的lib/security目录下的那两个策略文件。
    • 从Oracle官网下载“Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files”。
    • 备份原有文件,将下载的jar包解压后得到的local_policy.jarUS_export_policy.jar复制到<JAVA_HOME>/jre/lib/security/目录下。
  3. 对于容器化部署(如Docker),确保基础镜像中已包含无限制策略文件,或在Dockerfile中执行替换操作。

5.2 异常:javax.crypto.BadPaddingException: Given final block not properly padded

问题现象:解密时抛出此异常。排查思路:这是对称加密中最常见的异常之一,原因多样。

  1. 密钥错误:加密和解密使用的密钥不一致。检查密钥的生成、存储和传递流程。确保用于解密的密钥字符串或字节数组与加密时完全一致。
  2. IV错误:CBC或GCM模式下,解密时使用的IV与加密时不同。检查IV的拼接、传输和解析逻辑。确保从加密结果中正确拆分出了IV。
  3. 数据被篡改:密文在存储或传输过程中发生了改变(哪怕一个字节)。GCM模式会抛出AEADBadTagException,而CBC模式在解密最后校验填充时才发现问题,抛出BadPaddingException
  4. 算法/模式/填充不匹配:加密时使用AES/CBC/PKCS5Padding,解密时却用了AES/ECB/PKCS5Padding。务必保证Cipher.getInstance()中的字符串完全一致。
  5. 字符编码问题:加密前将字符串转为字节,和解密后将字节转为字符串时,使用了不同的字符编码。始终显式指定StandardCharsets.UTF_8

5.3 如何安全地存储和传递密钥?

这是一个架构问题,而非单纯的编码问题。上面代码中将密钥转为Base64字符串只是权宜之计。生产环境建议:

  • 环境变量:将加密后的密钥或密钥标识存储在服务器的环境变量中。
  • 配置中心:使用带有加密功能的配置中心(如Spring Cloud Config Server with Encryption)。
  • 密钥管理服务(KMS):使用云服务商(AWS KMS, Azure Key Vault, Google Cloud KMS)或开源的密钥管理服务。应用在运行时动态向KMS请求密钥或执行加解密操作,密钥本身不出现在应用内存之外。
  • 硬件安全模块(HSM):最高安全等级的场景下使用。

5.4 加密结果不一致?注意“盐值”和“随机性”

有时你会发现,用相同的密钥和明文加密两次,结果却不同。这是正常的,而且是安全的体现!

  • 在CBC和GCM模式下,只要IV是随机生成的,每次加密的密文都会不同。
  • 这防止了攻击者通过观察密文是否相同来推断明文是否相同。
  • 因此,永远不要试图去“固定”IV来获得相同的密文输出。IV的随机性是安全性的重要组成部分。

5.5 性能考量:如何加密大文件?

上述示例都是对字符串(小数据)进行操作。对于大文件,将整个文件读入内存(byte[])再调用doFinal()会导致内存溢出(OutOfMemoryError)。正确做法是使用流式操作:

public static void encryptFile(SecretKey key, IvParameterSpec iv, File inputFile, File outputFile) throws Exception { Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, key, iv); try (FileInputStream inputStream = new FileInputStream(inputFile); FileOutputStream outputStream = new FileOutputStream(outputFile); CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher)) { byte[] buffer = new byte[8192]; // 8KB缓冲区 int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { cipherOutputStream.write(buffer, 0, bytesRead); } } }

解密过程类似,使用CipherInputStream包装输入流即可。这种方式只在内存中维护一个固定大小的缓冲区,适合处理任意大小的文件。

6. 从理论到实践:一个完整的模拟场景

为了把所有知识点串联起来,我们模拟一个“用户手机号加密存储”的常见场景。

需求:在数据库中,用户的手机号需要加密存储。系统需要支持加密写入和解密读取。

设计

  1. 算法选择:使用AES-256-GCM。因为它提供认证,能防止密文被篡改,且是当前推荐的标准。
  2. 密钥管理:在应用启动时,从环境变量APP_AES_KEY中读取Base64编码的密钥。密钥本身由运维人员通过安全渠道生成并注入环境变量。
  3. IV处理:每次加密生成随机IV,与密文一起拼接,存储在同一数据库字段中(例如,用分隔符$连接)。
  4. 数据库字段:定义一个VARCHARTEXT类型的字段encrypted_phone

核心服务层代码片段:

@Service public class UserService { private final SecretKey aesKey; public UserService(@Value("${app.aes.key}") String base64Key) { this.aesKey = AESUtil.convertStringToKey(base64Key); // 假设的转换方法 } public String encryptPhoneNumber(String phoneNumber) throws Exception { // 使用GCM模式加密,返回 "IV$CIPHERTEXT" 格式 String encryptedResult = AESUtil.encryptWithGCM(phoneNumber, aesKey); // 将加密结果中的冒号替换为数据库友好的分隔符,或者直接存储 return encryptedResult.replace(':', '$'); } public String decryptPhoneNumber(String storedData) throws Exception { // 从数据库读取的数据格式是 "IV$CIPHERTEXT" String standardFormat = storedData.replace('$', ':'); return AESUtil.decryptWithGCM(standardFormat, aesKey); } public void saveUser(User user) { String encryptedPhone = encryptPhoneNumber(user.getPlainPhone()); user.setEncryptedPhone(encryptedPhone); // ... 保存用户到数据库,不保存明文手机号 } public User getUserById(Long id) { User user = userRepository.findById(id); String decryptedPhone = decryptPhoneNumber(user.getEncryptedPhone()); user.setPlainPhone(decryptedPhone); // 仅供业务逻辑使用,不持久化 return user; } }

这个设计的好处

  • 安全性:使用了强算法(AES-256-GCM),密钥不落地代码,IV随机。
  • 可检索性(受限):由于每次加密密文都不同,无法直接通过密文在数据库里进行等值查询。如果需要有根据手机号查询用户的需求,需要设计额外的方案,如使用确定的加密方式(但会降低安全性)或在另一个加密字段存储手机号的哈希值(用于模糊匹配,但无法解密)。
  • 完整性:GCM模式能检测数据是否被篡改,无论是网络传输中还是数据库存储后。

最后,记住加密是安全链条中的一环,而非全部。还需要结合安全的网络传输(HTTPS)、完善的访问控制、日志审计、漏洞管理等一系列措施,才能构建真正健壮的系统。希望这篇从原理到踩坑指南的完整梳理,能让你在Java加密开发中更加得心应手。