阅读时间:约 40-50 分钟 | 知识点编号:227 适用人群:Java 高级工程师、技术专家、架构师
目录
- 为什么加密是架构师的必修课
- 对称加密:一把钥匙开一把锁
- 非对称加密:数学魔术般的公私钥体系
- 哈希算法:数据世界的"指纹"技术
- 数字签名:谁写的?有没有被改过?
- TLS 协议:互联网安全的基石
- 密钥管理:安全体系的阿喀琉斯之踵
- 面试官视角:Q&A 模块
1. 为什么加密是架构师的必修课
1.1 一个真实的事故
2023 年某电商平台在一次大促中,因 Redis 缓存中存储的用户手机号被明文泄露,导致数百万用户数据流入黑产。事后复盘发现:开发团队认为"内网就是安全的",跳过了数据加密环节。这起事故的直接损失超过 2000 万,CTO 引咎辞职。
内网不等于安全,信任边界需要密码学来守护。
1.2 密码学解决的核心问题
密码学不是一门"有就行"的学科,它要解决四个核心问题,业界称为 CIA 三元组 + 不可否认性:
| 问题 | 密码学方案 | 典型场景 |
|---|---|---|
| 机密性 (Confidentiality) | 加密(对称 + 非对称) | 用户密码存储、支付信息传输 |
| 完整性 (Integrity) | 哈希 + MAC | 文件下载校验、API 签名 |
| 可用性 (Availability) | 密钥备份、容灾 | 证书轮换、密钥托管 |
| 不可否认性 (Non-repudiation) | 数字签名 | 电子合同、区块链交易 |
1.3 一张图看清全局

▲ 图1:密码学全景图——对称加密、非对称加密、哈希、数字签名、TLS的关系
上图展示了密码学五大模块之间的协作关系。简单来说:
- 对称加密负责"快",用于大量数据的加密传输
- 非对称加密负责"安全地交换密钥",解决对称加密中密钥分发的问题
- 哈希算法负责"防篡改",确保数据未被修改
- 数字签名结合哈希和非对称加密,实现"身份认证 + 防篡改 + 不可否认"
- TLS 协议是上述所有技术的"工程化组装",让浏览器和服务器之间安全通信
2. 对称加密:一把钥匙开一把锁
2.1 核心思想
对称加密的"对称"指的是:加密和解密使用同一把密钥。
明文 + 密钥 → [加密算法] → 密文
密文 + 密钥 → [解密算法] → 明文
就像你和朋友约定好一个暗号本——同一本暗号本既用来加密也用来解密。谁拿到了暗号本,谁就能读懂所有消息。
2.2 两种工作模式:流密码 vs 分组密码
流密码(Stream Cipher):逐比特加密,适合实时通信场景。
- 代表算法:RC4(已废弃)、ChaCha20
- 特点:速度快,无填充开销,但对密钥流质量要求极高
分组密码(Block Cipher):将明文分成固定大小的块(如 128 bit),逐块加密。
- 代表算法:AES、DES、3DES、SM4(国密)
- 特点:需要填充(Padding),需要选择工作模式
2.3 分组密码的工作模式——极易踩坑
分组密码不能简单地"一块一块加密",那样会暴露数据模式。下图展示了 ECB 模式的问题:

▲ 图2:ECB模式 vs CBC模式加密效果对比——同一张图片用ECB加密后轮廓清晰可见
常见工作模式对比:
| 模式 | 全称 | 特点 | 安全性 | 使用建议 |
|---|---|---|---|---|
| ECB | Electronic Codebook | 每块独立加密 | ❌ 极不安全 | 永远不要用 |
| CBC | Cipher Block Chaining | 前一块密文与当前块异或 | ⚠️ 需要 IV | 传统选择,注意 Padding Oracle |
| CTR | Counter | 计数器模式,可并行 | ✅ 较好 | 推荐,可并行加密 |
| GCM | Galois/Counter Mode | CTR + 认证 | ✅ 最佳 | 首选,自带完整性校验 |
GCM 模式为什么是首选? 因为它不仅加密,还附带认证标签(Authentication Tag),防止密文被篡改。这叫做 AEAD(Authenticated Encryption with Associated Data)。
// Java 中使用 AES-GCM 的正确姿势
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
public class AESGCMExample {
private static final int GCM_IV_LENGTH = 12; // 96 bits, GCM 推荐值
private static final int GCM_TAG_LENGTH = 128; // 128 bits 认证标签
public static byte[ ] encrypt(byte[ ] plaintext, SecretKeySpec key) throws Exception {
byte[ ] iv = new byte[GCM_IV_LENGTH];
SecureRandom.getInstanceStrong().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);
// 将 IV 拼接在密文前面传输
byte[ ] result = new byte[iv.length + ciphertext.length];
System.arraycopy(iv, 0, result, 0, iv.length);
System.arraycopy(ciphertext, 0, result, iv.length, ciphertext.length);
return result;
}
public static byte[ ] decrypt(byte[ ] encrypted, SecretKeySpec key) throws Exception {
// 从密文前12字节提取 IV
byte[ ] iv = new byte[GCM_IV_LENGTH];
System.arraycopy(encrypted, 0, iv, 0, GCM_IV_LENGTH);
byte[ ] ciphertext = new byte[encrypted.length - GCM_IV_LENGTH];
System.arraycopy(encrypted, GCM_IV_LENGTH, ciphertext, 0, ciphertext.length);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
return cipher.doFinal(ciphertext);
}
}
2.4 AES 内部原理:深入理解轮函数
AES 的核心是"轮变换",每轮包含四个步骤。以 AES-128 为例,共 10 轮:
- SubBytes:每个字节通过 S-Box(替换盒)进行非线性替换——这是 AES 安全性的核心,破坏了明文和密文之间的线性关系
- ShiftRows:行移位,打乱字节位置
- MixColumns:列混合,在 GF(2⁸) 有限域上进行矩阵乘法,实现扩散
- AddRoundKey:与轮密钥异或——密钥由此注入

▲ 图3:AES加密一轮的四个步骤流程图——SubBytes→ShiftRows→MixColumns→AddRoundKey
为什么 AES 至今没有被攻破? 关键在于 S-Box 的设计基于有限域上的乘法逆元,具有极强的非线性特性。线性密码分析和差分密码分析都无法在可接受的计算复杂度内攻破完整轮数的 AES。目前最佳攻击是 biclique 攻击,复杂度为 2<sup>126.1,仅比暴力搜索(2</sup>128)略好,实践中无威胁。
2.5 对称加密的致命弱点:密钥分发
对称加密快且安全,但有一个根本问题:通信双方如何安全地共享密钥?
假设 Alice 和 Bob 想在互联网上安全通信,他们需要先约定一把密钥。但如果他们能安全地传递密钥,为什么不能直接安全地传递消息?
这就是 密钥分发问题(Key Distribution Problem),也是非对称加密诞生的直接动机。
3. 非对称加密:数学魔术般的公私钥体系
3.1 核心思想
非对称加密使用一对数学上关联的密钥:
- 公钥(Public Key):可以公开,任何人都能拿到
- 私钥(Private Key):必须保密,只有持有者知道
公钥加密 → 私钥解密:任何人用公钥加密的消息,只有私钥持有者能解密。
明文 + 公钥 → [加密算法] → 密文
密文 + 私钥 → [解密算法] → 明文
3.2 RSA:基于大数分解的经典算法
RSA 的安全性建立在"大整数质因数分解很难"这一数学事实上。
密钥生成过程:
- 随机选择两个大质数 p 和 q(通常各 1024 bit 以上)
- 计算 n = p × q,n 的位数即密钥长度(如 2048 bit)
- 计算 φ(n) = (p-1)(q-1)
- 选择公钥指数 e(通常取 65537)
- 计算私钥指数 d,满足 e × d ≡ 1 (mod φ(n))
公钥 = (n, e),私钥 = (n, d)
加密:c = m^e mod n 解密:m = c^d mod n
// Java 中使用 RSA 加密
import javax.crypto.Cipher;
import java.security.*;
public class RSAExample {
public static KeyPair generateKeyPair() throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048); // 至少 2048 位
return generator.generateKeyPair();
}
public static byte[ ] encrypt(byte[ ] data, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(data);
}
public static byte[ ] decrypt(byte[ ] data, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(data);
}
}
注意:RSA 加密有长度限制——2048 位密钥最多加密 245 字节(OAEP 填充后)。实际工程中,RSA 通常只用来加密对称密钥(即密钥交换),不会直接加密大量数据。
3.3 ECC:短小精悍的椭圆曲线
椭圆曲线密码学(Elliptic Curve Cryptography)基于椭圆曲线离散对数问题(ECDLP)。
为什么 ECC 比 RSA 更受欢迎?
| 对比维度 | RSA-2048 | ECC-256 |
|---|---|---|
| 安全强度 | ~112 bit | ~128 bit |
| 公钥大小 | 256 字节 | 64 字节 |
| 私钥大小 | 256 字节 | 32 字节 |
| 签名大小 | 256 字节 | 64 字节 |
| 密钥生成速度 | 慢(需要找大质数) | 快 |
| 移动端友好 | 一般 | ✅ 极佳 |

▲ 图4:RSA与ECC密钥大小对比——相同安全强度下ECC密钥远小于RSA
椭圆曲线的数学直觉:在椭圆曲线 y² = x³ + ax + b 上定义"点加"运算——两个点 P 和 Q 相加得到第三个点 R。给定点 G 和 kG,求 k 是计算上不可行的。这就是 ECDLP,也是 ECC 安全性的基础。
最常用的曲线:
- secp256r1(也叫 P-256):NIST 标准,应用最广
- Curve25519:Daniel Bernstein 设计,实现简单,不易出错,TLS 1.3 强制支持
- SM2:中国国家密码管理局发布的椭圆曲线公钥密码算法
3.4 混合加密:工程实践中的标配
非对称加密解决了密钥分发问题,但速度慢(比对称加密慢 100-1000 倍)。实际工程中采用 混合加密:
- 生成一个随机的会话密钥(Session Key)
- 用会话密钥 + 对称加密(如 AES-GCM)加密大量数据
- 用对方的公钥 + 非对称加密(如 RSA/ECC)加密会话密钥
- 将加密后的会话密钥和密文一起发送
这就是 HTTPS 的基本工作原理。TLS 握手过程中,客户端和服务器通过非对称加密协商出会话密钥,后续通信全部使用对称加密。
4. 哈希算法:数据世界的"指纹"技术
4.1 核心概念
哈希函数将任意长度的输入映射为固定长度的输出:
H: {0,1}* → {0,1}^n
密码学哈希的三个核心性质:
| 性质 | 含义 | 工程意义 |
|---|---|---|
| 抗原像(Preimage Resistance) | 给定 h,找不到 m 使 H(m) = h | 密码不能从哈希值反推 |
| 抗第二原像(Second Preimage Resistance) | 给定 m1,找不到 m2 使 H(m1) = H(m2) | 无法伪造相同哈希的文件 |
| 抗碰撞(Collision Resistance) | 找不到任意 m1 ≠ m2 使 H(m1) = H(m2) | 数字签名安全的基础 |
4.2 主流哈希算法对比
| 算法 | 输出长度 | 安全性 | 使用建议 |
|---|---|---|---|
| MD5 | 128 bit | ❌ 已破解 | 绝不用于安全场景 |
| SHA-1 | 160 bit | ❌ 已破解 | 绝不用于安全场景 |
| SHA-256 | 256 bit | ✅ 安全 | 推荐,应用最广 |
| SHA-3 | 224/256/384/512 | ✅ 安全 | 新一代标准 |
| BLAKE3 | 256 bit | ✅ 安全 | 极快,适合大数据量 |
| SM3 | 256 bit | ✅ 安全 | 国密标准 |
SHA-256 的内部结构——Merkle-Damgård 构造:

▲ 图5:SHA-256的Merkle-Damgård结构——消息分块→压缩函数→链接变量迭代
- 消息填充:添加 1 个 '1' 和若干 '0',最后 64 bit 记录原始消息长度
- 分成 512 bit 的块
- 每块通过压缩函数处理,输出 256 bit 链接变量
- 64 轮迭代,每轮包含移位、异或、与、加等操作
- 最终链接变量即哈希值
4.3 哈希的工程应用
场景一:密码存储
// 错!直接用哈希存密码
String hashed = sha256(password); // 彩虹表攻击!
// 对!加盐哈希
import java.security.SecureRandom;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.util.Base64;
public class PasswordHasher {
private static final int SALT_LENGTH = 16;
private static final int ITERATIONS = 600_000;
private static final int KEY_LENGTH = 256;
public static String hash(String password) throws Exception {
SecureRandom random = new SecureRandom();
byte[ ] salt = new byte[SALT_LENGTH];
random.nextBytes(salt);
PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, KEY_LENGTH);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[ ] hash = factory.generateSecret(spec).getEncoded();
// 存储格式:迭代次数$salt的Base64编码$hash的Base64编码
return ITERATIONS + "$" + Base64.getEncoder().encodeToString(salt)
+ "$" + Base64.getEncoder().encodeToString(hash);
}
public static boolean verify(String password, String stored) throws Exception {
String[ ] parts = stored.split("\\$");
int iterations = Integer.parseInt(parts[0]);
byte[ ] salt = Base64.getDecoder().decode(parts[1]);
byte[ ] expectedHash = Base64.getDecoder().decode(parts[2]);
PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, KEY_LENGTH);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[ ] actualHash = factory.generateSecret(spec).getEncoded();
return MessageDigest.isEqual(expectedHash, actualHash); // 常量时间比较
}
}
为什么用 PBKDF2/bcrypt/Argon2 而不是 SHA-256? 因为 SHA-256 设计目标就是快,而密码哈希需要慢——故意增加计算成本,让暴力破解变得不可行。PBKDF2 通过多次迭代实现这一点,bcrypt 则内置了内存开销,Argon2 更是同时对抗 GPU 和 ASIC 攻击。
场景二:文件完整性校验
# 下载文件后验证
sha256sum downloaded_file.tar.gz
# 与官网公布的哈希值对比
场景三:Merkle Tree——区块链的基础
每个叶子节点是数据块的哈希,每个非叶子节点是子节点哈希的哈希。Merkle Tree 使得可以在不下载整个文件的情况下验证某个数据块是否属于该文件。

▲ 图6:Merkle Tree结构——叶子节点是数据块哈希,上层节点是子节点哈希的哈希
4.4 哈希碰撞的现实案例
2017 年,Google 和 CWI 联合团队成功构造了 SHA-1 的碰撞:
- 两个不同的 PDF 文件具有相同的 SHA-1 哈希值
- 计算量:约 2^63.1 次 SHA-1 压缩函数调用
- 成本:约 11 万美元的云计算资源
这就是为什么所有主流浏览器在 2017 年后停止信任 SHA-1 证书。
5. 数字签名:谁写的?有没有被改过?
5.1 数字签名的工作原理
数字签名结合了哈希算法和非对称加密,但注意:签名用的是私钥,验证用的是公钥——与非对称加密的方向相反。
签名过程:
1. 对原文计算哈希值 H
2. 用私钥对 H 进行加密(签名),得到签名 S
3. 将原文 + S 一起发送
验证过程:
1. 对收到的原文计算哈希值 H1
2. 用公钥对签名 S 解密,得到 H2
3. 比较 H1 和 H2,相等则验证通过

▲ 图7:数字签名流程——签名方用私钥签名→验证方用公钥验证
5.2 主流签名算法
| 算法 | 类型 | 签名大小 | 安全强度 | 应用 |
|---|---|---|---|---|
| RSA-PSS | RSA | 256 字节 | ~112 bit | 传统 Web PKI |
| ECDSA | 椭圆曲线 | 64 字节 | ~128 bit | 比特币、TLS |
| Ed25519 | Edwards 曲线 | 64 字节 | ~128 bit | 现代首选 |
| SM2 | 国密椭圆曲线 | 64 字节 | ~128 bit | 国内合规 |
Ed25519 为什么是"现代首选"?
- 确定性签名:不需要高质量的随机数(ECDSA 的致命弱点)
- 速度极快:签名和验证都比 ECDSA 快
- 实现简单:不容易出错
- 密钥和签名都很小:32 字节私钥,32 字节公钥,64 字节签名
// Java 15+ 原生支持 Ed25519
import java.security.*;
import java.util.Base64;
public class Ed25519Example {
public static KeyPair generateKeyPair() throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("Ed25519");
return generator.generateKeyPair();
}
public static byte[ ] sign(byte[ ] data, PrivateKey privateKey) throws Exception {
Signature sig = Signature.getInstance("Ed25519");
sig.initSign(privateKey);
sig.update(data);
return sig.sign();
}
public static boolean verify(byte[ ] data, byte[ ] signature, PublicKey publicKey)
throws Exception {
Signature sig = Signature.getInstance("Ed25519");
sig.initVerify(publicKey);
sig.update(data);
return sig.verify(signature);
}
}
5.3 数字证书与 PKI
数字签名解决了"这条消息是不是 Alice 写的"问题,但引出了新问题:你怎么知道这个公钥真的是 Alice 的?
PKI(Public Key Infrastructure,公钥基础设施)通过证书链来解决:
根 CA 证书(自签名,预装在操作系统/浏览器中)
└── 中间 CA 证书(由根 CA 签名)
└── 服务器证书(由中间 CA 签名)
验证过程:
- 用中间 CA 的公钥验证服务器证书的签名
- 用根 CA 的公钥验证中间 CA 证书的签名
- 根 CA 证书是自签名的,必须被信任方预装
这就是 X.509 证书体系,也是 TLS 协议中身份认证的基础。
6. TLS 协议:互联网安全的基石
6.1 TLS 在协议栈中的位置
TLS(Transport Layer Security)位于 TCP 之上、应用层协议(HTTP、SMTP 等)之下:
┌─────────────────┐
│ HTTP / HTTP/2 │ ← 应用层
├─────────────────┤
│ TLS │ ← 安全层
├─────────────────┤
│ TCP │ ← 传输层
├─────────────────┤
│ IP │ ← 网络层
└─────────────────┘
6.2 TLS 1.3 握手过程(简化版)
TLS 1.3 相比 TLS 1.2 做了重大简化,握手从 2-RTT 减少到 1-RTT:
客户端 服务器
│ │
│── ClientHello ───────────────→│
│ (支持的密码套件、密钥分享) │
│ │
│←─ ServerHello ────────────────│
│ (选定的密码套件、密钥分享、证书) │
│ Certificate │
│ CertificateVerify │
│ Finished │
│ │
│── Finished ──────────────────→│
│ │
│←══════ 加密通信开始 ═══════════→│
TLS 1.3 的核心改进:
- 只支持前向安全的密钥交换:废除了 RSA 密钥交换,只保留 (EC)DHE,确保即使服务器私钥泄露,历史通信也无法被解密
- 握手加密:从 ServerHello 之后的消息就开始加密,减少明文元数据泄露
- 0-RTT 恢复:之前连接过的客户端可以直接发送加密数据,无需完整握手
- 精简密码套件:从 TLS 1.2 的几十种减少到 5 种,消除弱算法
6.3 密码套件详解
TLS 1.3 的密码套件格式:
TLS_AES_256_GCM_SHA384
│ │ │ │
│ │ │ └── 哈希算法(用于 HMAC 和 HKDF)
│ │ └─────── 加密模式(AEAD)
│ └────────────── 加密算法 + 密钥长度
└────────────────── 协议版本
一条密码套件告诉双方:用什么算法做密钥交换(密钥分享)、用什么算法加密数据、用什么算法做消息认证。
6.4 证书透明与 OCSP 装订
证书透明(Certificate Transparency):所有 CA 签发的证书必须公开记录到不可篡改的日志中,任何域名所有者都可以监控是否有未授权的证书被签发。
OCSP 装订(OCSP Stapling):服务器主动向 CA 查询自己证书的吊销状态,将 OCSP 响应"装订"到 TLS 握手中,客户端无需额外查询,保护隐私的同时提升速度。

▲ 图8:TLS 1.3完整握手流程——包含密钥交换、证书验证、加密通信建立
7. 密钥管理:安全体系的阿喀琉斯之踵
7.1 密钥管理为什么比算法更重要
密码学算法本身是公开的——AES 的源码你可以在网上找到,RSA 的数学原理每个计算机系学生都学过。但密钥一旦泄露,再强的算法也形同虚设。
密钥管理的核心原则:
- 最小权限:每个系统只能访问自己需要的密钥
- 密钥分离:不同用途使用不同密钥(加密密钥 ≠ 签名密钥)
- 密钥轮换:定期更换密钥,限制单个密钥泄露的影响范围
- 密钥不落地:密钥永远不写入代码、日志、配置文件
- 审计追踪:每次密钥访问都要记录
7.2 密钥管理架构
┌─────────────────────────────────────────┐
│ 应用层 │
│ ┌─────────────────────────────────┐ │
│ │ DEK(数据加密密钥) │ │
│ │ - 加密业务数据 │ │
│ │ - 生命周期短,频繁轮换 │ │
│ └──────────────┬──────────────────┘ │
│ │ 加密存储 │
│ ┌──────────────▼──────────────────┐ │
│ │ KEK(密钥加密密钥) │ │
│ │ - 加密 DEK │ │
│ │ - 生命周期较长 │ │
│ └──────────────┬──────────────────┘ │
│ │ │
├─────────────────┼────────────────────────┤
│ KMS / HSM │
│ ┌──────────────▼──────────────────┐ │
│ │ Master Key(主密钥) │ │
│ │ - 存储在 HSM 硬件中 │ │
│ │ - 永不出 HSM 边界 │ │
│ │ - 加密/解密 KEK │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
7.3 密钥管理最佳实践
开发环境:
// 错!密钥硬编码
String apiKey = "sk-abc123def456";
// 对!从环境变量或密钥管理服务读取
String apiKey = System.getenv("API_KEY");
// 或
String apiKey = SecretsManager.getSecret("prod/payment/api-key");
密钥轮换策略:
- DEK:每次会话或每天轮换
- KEK:每月或每季度轮换
- Master Key:每年或每两年轮换
密钥版本管理: 轮换密钥时,旧密钥不能立即删除——因为可能有使用旧密钥加密的数据需要解密。引入密钥版本号,解密时根据版本选择正确的密钥。
7.4 云原生密钥管理
在云环境中,推荐使用云厂商的 KMS(Key Management Service):
- AWS KMS / 阿里云 KMS:管理主密钥,提供加密/解密 API
- HashiCorp Vault:跨平台密钥管理,支持动态密钥生成
- Azure Key Vault:密钥、证书、密码的统一管理
// 阿里云 KMS 使用示例(简化)
import com.aliyun.kms20160120.Client;
import com.aliyun.kms20160120.models.EncryptRequest;
import com.aliyun.kms20160120.models.DecryptRequest;
public class KMSExample {
// DEK 从不离开 KMS——加密和解密都在 KMS 中完成
public byte[ ] encryptWithKMS(String keyId, byte[ ] plaintext) {
EncryptRequest req = new EncryptRequest()
.setKeyId(keyId)
.setPlaintext(Base64.getEncoder().encodeToString(plaintext));
// ciphertextBlob 是密文,包含密钥版本信息
return client.encrypt(req).getCiphertextBlob();
}
}
7.5 前向安全性(Forward Secrecy)
前向安全性是指:即使长期密钥(如服务器私钥)在未来被泄露,过去的通信内容仍然无法被解密。
实现方式:使用临时密钥对(Ephemeral Key Pair)进行密钥交换:
- (EC)DHE:临时 Diffie-Hellman 密钥交换
- 每次会话生成新的临时密钥对
- 会话结束后立即销毁临时私钥
- 即使攻击者记录了所有密文,未来拿到服务器私钥也无法解密
TLS 1.3 强制要求前向安全性——这就是为什么 TLS 1.3 废除了静态 RSA 密钥交换。
8. 面试官视角:Q&A 模块
以下问题从面试官视角出发,考察你对密码学原理和工程实践的深入理解。回答时不仅要"对",更要展示你的思考过程。
Q1:你设计了一个微服务间通信的加密方案,使用 AES-256-GCM 加密。现在需要把密钥安全地分发给上百个服务实例,你会怎么做?如果某个实例被攻破,如何最小化影响范围?
Q2:HTTPS 的证书链验证中,如果中间 CA 的私钥泄露,攻击者可以伪造任意域名的证书吗?浏览器如何防御这种情况?
Q3:你在日志中发现 AES-GCM 解密时抛出了 AEADBadTagException,这意味着什么?可能的原因有哪些?该如何排查?
Q4:公司要求所有密码存储必须使用 bcrypt 或 Argon2,而不是 SHA-256。请从攻击者视角解释:如果使用 SHA-256(salt + password) 存储密码,攻击者在拿到数据库后如何高效破解?
Q5:TLS 1.3 移除了 RSA 密钥交换,只保留 (EC)DHE。这个设计决策背后的安全考量是什么?如果你的系统中有大量 IoT 设备,它们的计算能力有限,你会如何选择 TLS 版本和密码套件?
Q6:你负责的系统需要实现"数据加密存储 + 按用户 ID 搜索"的功能。直接加密后无法搜索,你会如何设计这个方案?请评估可搜索加密(Searchable Encryption)的几种实现方式的优劣。
Q7:代码审查时发现同事使用了 `Cipher.getInstance("AES")` 而不是 `Cipher.getInstance("AES/GCM/NoPadding")`。这个差异意味着什么?Java 中 `Cipher.getInstance("AES")` 实际使用的默认模式和填充是什么?不同 JDK 提供商是否一致?
下一篇预告:228|安全框架与 OAuth 2.0——授权码模式、JWT 令牌、Spring Security 架构与最佳实践
参考答案
A1:微服务加密密钥分发方案
这是一个典型的密钥分发问题,核心思路是引入中心化密钥管理服务(KMS)作为密钥分发的可信中介:
- 密钥不在代码中:每个服务实例启动时通过 KMS SDK 向 KMS 请求 DEK(数据加密密钥),KMS 通过实例身份(如 K8s ServiceAccount、RAM Role)进行认证授权
- 密钥分层:KMS 管理 Master Key,服务实例获取的是短期 DEK。DEK 由 Master Key 加密后分发,KMS 侧可随时吊销
- 密钥轮换:DEK 设置 TTL(如 1 小时),过期后自动向 KMS 申请新密钥。旧 DEK 仅用于解密历史数据,不用于加密新数据
- 审计追踪:每次密钥请求记录到 KMS 审计日志中,异常访问模式(如某实例频繁请求密钥)触发告警
如果某个实例被攻破,最小化影响的措施:
- 密钥隔离:不同服务/不同环境使用不同的密钥。攻击者拿到实例 A 的 DEK,只能解密实例 A 处理的数据,无法横向扩展到实例 B
- 即时吊销:通过 KMS 立即吊销该实例的密钥访问权限,新的 DEK 不再分发给该实例
- 密钥版本化:加密数据时附带密钥版本号,轮换到新版本后,攻击者持有的旧 DEK 无法解密新数据
- 网络隔离:被攻破实例从服务网格中摘除,防止其继续接收请求
面试官看重的点:是否理解密钥分层的概念(DEK vs KEK vs Master Key),是否考虑密钥轮换和吊销机制,以及是否具备防御纵深(Defense in Depth)的思维。
A2:中间 CA 私钥泄露的影响与防御
是的,攻击者可以用泄露的中间 CA 私钥签发任意域名的证书,这些证书会被所有信任该中间 CA 的客户端(浏览器、操作系统)接受。历史上真实发生过:2011 年 DigiNotar 中间 CA 被攻破,攻击者签发了包括 *.google.com 在内的 531 张伪造证书,用于伊朗的中间人攻击。事件直接导致 DigiNotar 破产。
浏览器的多层防御机制:
- CRL(证书吊销列表):CA 定期发布被吊销证书的序列号列表。但更新不及时,下载量大,已逐渐被 OCSP 取代
- OCSP(在线证书状态协议):浏览器实时查询证书是否被吊销。缺点:隐私问题(CA 知道你在访问哪个网站),增加延迟
- OCSP Stapling(装订):服务器主动向 CA 获取 OCSP 响应,在 TLS 握手中装订给客户端。客户端无需额外查询,保护隐私且低延迟。TLS 1.3 推荐
- 证书透明(Certificate Transparency):所有公开签发的证书必须记录到不可篡改的 Merkle Tree 日志中。域名所有者可以监控是否有未授权的证书被签发。Chrome 从 2015 年开始要求 EV 证书必须在 CT 日志中
- CRLSet / CRLite:浏览器维护热门证书的吊销状态压缩列表,本地快速查询
- 根证书存储管理:浏览器/操作系统厂商可以快速移除被攻破的根 CA 或中间 CA 的信任(如 DigiNotar 事件中,各大浏览器在 48 小时内移除了 DigiNotar 的根证书)
面试官看重的点:不仅要知道有防御机制,还要能说出各机制的优缺点和演进路径。提到 CT 和 OCSP Stapling 是加分项。
A3:AEADBadTagException 排查
AEADBadTagException 表示 GCM 模式解密时,计算出的认证标签与密文附带的认证标签不匹配。这意味着以下三种情况之一发生了:
- 密文在传输或存储过程中被篡改
- 解密时使用了错误的密钥
- 解密时使用了错误的 IV(初始化向量)或关联数据(AAD)
排查步骤(按可能性排序):
- 第一步:确认密钥是否正确。检查是否使用了错误的密钥版本——密钥轮换后,旧数据需要用旧版本的密钥解密。查看密钥版本号是否正确
- 第二步:确认 IV 是否正确。GCM 的 IV 通常是 12 字节,每次加密必须唯一。检查 IV 是否从密文中正确提取(常见错误:相位偏移,提取了错误位置的字节)
- 第三步:检查编码问题。密文在传输过程中是否经过了 Base64/Hex 编码?解码时是否使用了正确的编码方式?字节顺序是否正确?
- 第四步:检查是否有中间件篡改。如果密文经过网关、代理、消息队列,确认中间件没有对二进制数据进行任何转换(如字符集转换、换行符转换)
- 第五步:检查 AAD(关联认证数据)。如果加密时使用了 AAD(如协议头信息),解密时必须传入完全相同的 AAD,否则认证失败
- 第六步:排除真实的篡改攻击。如果以上都排除了,考虑安全事件——检查日志中的异常访问记录,确认是否有攻击者修改了数据
面试官看重的点:是否有系统化的排查思路(从最常见到最罕见),是否理解 GCM 认证标签的工作原理,以及能否区分代码 bug 和安全事件。
A4:SHA-256 存密码为什么不行
攻击者拿到数据库后的攻击路径:
第一步——彩虹表攻击:如果只用 SHA-256(password),攻击者可以预先计算常见密码的 SHA-256 值,建立明文到哈希的映射表。拿到数据库后直接查表即可。即使加了盐 SHA-256(salt+password),如果盐已知(salt 通常和密码存在同一张表),攻击者可以针对每个盐单独构建彩虹表。
第二步——GPU 暴力破解:SHA-256 设计目标就是快。一块 RTX 4090 每秒可以计算约 80 亿次 SHA-256。8 位纯数字密码(10^8 种组合)约 0.01 秒;8 位小写字母+数字(36^8,约 2.8×10^12)约 6 分钟。而 bcrypt(cost=12) 在同样硬件上每秒只能计算约 3000 次,同样 8 位小写字母+数字需要约 30 年。
专门的密码哈希函数(bcrypt / Argon2)的核心设计:
- 计算成本可配置:bcrypt 的 cost 参数控制迭代次数(2^cost 次),Argon2 可以分别控制时间成本、内存成本和并行度
- 内存硬化:Argon2 需要大量内存(如 64MB),GPU 的显存有限,无法大规模并行——这正是对抗 GPU/ASIC 的关键
- 内置盐:不需要开发者自己管理盐,降低出错概率
面试官看重的点:是否理解快是 SHA-256 的特性而非缺点——它在正确的场景(文件完整性、数字签名)中快是优点,在密码存储场景中快是致命缺陷。考察对不同算法设计目标的深入理解。
A5:TLS 1.3 移除 RSA 密钥交换的安全考量
移除 RSA 密钥交换的核心原因——前向安全性(Forward Secrecy):
- RSA 密钥交换:客户端生成预主密钥,用服务器公钥加密后发送。服务器用私钥解密。但如果服务器私钥未来泄露,攻击者可以解密所有历史会话——因为所有会话的密钥交换都依赖于同一个静态私钥
- (EC)DHE 密钥交换:每次会话生成临时密钥对(Ephemeral Key),会话结束后销毁临时私钥。即使服务器长期私钥泄露,历史会话因临时私钥已销毁而无法解密
2013 年斯诺登事件揭露了全球大规模监控项目,NSA 等手段正是收集加密流量,寄希望于未来获取服务器私钥后解密。这直接推动了 TLS 1.3 强制要求前向安全。
IoT 设备的 TLS 选择:
- TLS 1.3 仍然推荐,但只启用轻量密码套件:TLS_AES_128_GCM_SHA256(128 位密钥比 256 位快,对 IoT 设备足够安全)或 TLS_CHACHA20_POLY1305_SHA256(ChaCha20 在 ARM 处理器上比 AES 快得多,因为 AES 依赖 AES-NI 指令集,而 IoT 设备通常没有)
- 如果设备确实无法支持 TLS 1.3(如只有 TLS 1.2),确保使用 ECDHE 密码套件(如 TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),避免 RSA 密钥交换
- 证书选择:ECC 证书(如 P-256)比 RSA 证书小得多,TLS 握手中的证书传输开销更小。对于带宽有限的 IoT 设备尤其重要
- 会话恢复:使用 TLS Session Resumption 或 TLS 1.3 的 0-RTT(需评估重放攻击风险),减少握手开销
面试官看重的点:能否将安全理论(前向安全)与工程约束(IoT 计算能力)结合起来做 trade-off。
A6:可搜索加密方案设计
加密后无法搜索的原因是:加密算法(如 AES-GCM)破坏了明文的结构特性——相同明文加密后产生不同的密文,且密文之间没有顺序关系。解决加密+搜索需要在安全性和可用性之间做 trade-off。
方案一:确定性加密(Deterministic Encryption)——最简单但最弱
- 原理:使用固定的 IV(或从明文派生的 IV),使相同明文总是产生相同密文。搜索时对查询条件加密后,直接匹配密文
- 优点:实现简单,查询效率高,支持精确匹配
- 缺点:泄露了数据分布——攻击者可以看到哪些用户有相同的值。例如,如果同一个用户 ID 的密文相同,攻击者可以推断出哪些记录属于同一用户
- 适用场景:低敏感度的精确匹配查询,如按状态字段搜索
方案二:盲索引(Blind Index)——工程上最实用
- 原理:对明文计算 HMAC(plaintext, search_key),将 HMAC 值作为索引列存储。搜索时对查询条件计算同样的 HMAC,匹配索引列
- 优点:搜索密钥(search_key)与加密密钥分离,即使加密密钥泄露,搜索模式仍受保护;支持精确匹配
- 缺点:需要额外存储索引列;对于低熵数据(如性别、状态),攻击者可以暴力枚举所有可能值的 HMAC
- 增强:对低熵字段使用慢哈希(如 PBKDF2)计算盲索引,增加枚举成本
方案三:同态加密(Homomorphic Encryption)——最安全但最慢
- 原理:在密文上直接进行计算,结果解密后等于对明文计算的结果。部分同态加密支持一种运算,全同态加密支持任意运算
- 优点:最高安全性,服务器完全看不到明文
- 缺点:计算开销巨大(比明文计算慢 10^5 到 10^6 倍),难以在生产环境使用
- 适用场景:对安全性要求极高且数据量小的场景,如医疗数据隐私计算
方案四:SGX/TEE 可信执行环境——硬件方案
- 原理:数据在 TEE 安全区内解密后搜索,安全区外的内存始终加密
- 优点:支持任意复杂的查询,无需修改应用逻辑
- 缺点:依赖特定硬件和可信计算基;有侧信道攻击风险(如 Spectre/Meltdown)
推荐方案:对于大多数业务场景,盲索引方案是最佳平衡点。具体实现时:使用 HMAC-SHA256 作为盲索引函数,搜索密钥与加密密钥分别管理,对低熵字段额外加盐或使用慢哈希。
面试官看重的点:能否清晰地分析每种方案的 trade-off(安全 vs 性能 vs 功能),而非简单地说做不到或只能用同态加密。展示工程判断力。
A7:Cipher.getInstance("AES") 的隐患
这是一个经典的 Java 加密陷阱,很多线上事故的根源。
Cipher.getInstance("AES") 在不同 JDK 上的行为:
- Oracle JDK / OpenJDK:默认使用 AES/ECB/PKCS5Padding。ECB 模式极不安全,完全不提供语义安全——相同明文块加密后产生相同密文块
- IBM JDK:可能使用不同的默认模式
- Android SDK:可能使用不同的默认模式
- BouncyCastle Provider:可能使用不同的默认模式
这意味着什么?
- 安全风险:ECB 模式是最不安全的模式。如果加密的是一张图片或多条结构化数据,密文会直接暴露明文的数据模式
- 跨平台不兼容:在 Oracle JDK 上加密的数据,在 IBM JDK 上可能无法解密(如果后者使用了不同的默认模式)
- 没有认证:ECB 和 CBC 模式都不提供完整性保护。攻击者可以修改密文(即使他们不知道解密结果),导致解密出被篡改的明文——这种攻击称为密文篡改攻击
正确的做法:始终显式指定完整的算法字符串,格式为 算法/模式/填充,如 AES/GCM/NoPadding。代码审查时,任何不完整的 Cipher.getInstance() 参数都应该被视为阻塞项。
面试官看重的点:是否了解 Java 加密 API 的默认行为陷阱,是否理解跨平台一致性的重要性,以及是否有代码审查中发现安全问题的意识。