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的公钥加密一个随机生成的对称密钥,然后发给B,B用自己的私钥解密得到对称密钥。后续通信就用这个对称密钥进行高速的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通过JCA(Java密码体系结构)和JCE(Java密码学扩展)提供了丰富的加密支持。我们不用重复造轮子,但必须学会正确、安全地使用这些轮子。
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); } }这种方式无盐、哈希速度快,一个简单的彩虹表就能破解大部分常用密码。
正确实践一:使用PBKDF2PBKDF2(Password-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 Castle(BC)Provider。
4.1 引入Bouncy Castle依赖
以Maven为例:
<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.70</version> <!-- 使用最新稳定版 --> </dependency>4.2 SM4 ECB模式加密示例(仅作演示,生产慎用ECB)
import 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与应用层加密的边界
很多开发者会问:“我们的服务已经用了HTTPS(TLS/SSL),为什么还要在应用代码里做加密?” 这是一个非常好的问题,涉及到安全边界的划分。
TLS/SSL(传输层安全)保障的是数据在网络传输过程中的安全,即“管道安全”。它解决了窃听、篡改和冒充问题。数据到达目标服务器后,会被解密。如果服务器被入侵,或者数据需要落盘(存入数据库、缓存、日志),那么这些静态数据就处于明文状态。
应用层加密(即本文讨论的加密)保障的是数据本身的安全,而不管它处于传输中还是静止中。即使传输管道是安全的,或者数据已经存储在数据库里,只要没有解密密钥,数据就是一堆乱码。
最佳实践:
- 内外兼修:对外提供的API、网站,必须使用HTTPS(TLS 1.2+)。这是底线。
- 按需加密:对于特别敏感的数据(如身份证号、银行卡号、医疗记录),即使在内网传输或存储,也应进行应用层加密。这就是“端到端加密”或“字段级加密”的思想。
- 密钥隔离:TLS证书的私钥和应用数据的加密密钥应该分开管理,降低单点沦陷的风险。
我个人在实际项目中的做法是,对于核心的用户PII(个人身份信息)数据,在持久化到数据库之前,一定会用应用层的AES-GCM再进行一次加密。TLS管“路上”的安全,我自己的加密管“家里”的安全。这样,就算数据库备份文件被拖库,或者有内鬼直接访问了数据库,看到的也是加密后的密文,心里会踏实很多。
最后再分享一个小技巧:在团队中推行加密规范时,不要只给出一份文档。最好能提供一个经过安全审计的、开箱即用的工具类库(就像本文中的代码示例那样),并附上清晰的单元测试和场景示例。降低开发者的使用门槛,才能让安全实践真正落地。毕竟,最安全的算法,如果因为用起来太麻烦而被绕过,那也等于零。