密码学
对称/非对称、哈希与 PKI。
密码学
这一课你将能做到
- 说清对称加密、非对称加密、哈希各自解决什么问题,并能判断一个场景该用哪个
- 在 Spring Boot 中用 BCrypt 安全地存储用户密码,说清为什么不能 MD5/SHA-256 裸存
- 用 Java 标准库完成 AES 加密解密、RSA 签名验签、SHA-256 摘要,并理解密钥生命周期管理
为什么要学
密码学不是数学游戏,是后端工程师的日常:用户密码怎么存?JWT 怎么签名?HTTPS 证书凭什么可信?数据库里的手机号要不要加密?答错任何一题,就是一次数据泄露事故。
人话:对称加密是「同一把钥匙开锁和上锁」——快,但钥匙怎么给对方是个问题;非对称是「信箱」——公钥是投信口人人可用,私钥是开箱钥匙只有你有,慢但解决了分发问题;哈希是「指纹」——不可逆,用来校验「东西没被动过」。
先建立图像
三种原语的分工:
┌────────────┬──────────────┬────────────┬─────────────────┐
│ │ 可逆? │ 密钥 │ 本站用途 │
├────────────┼──────────────┼────────────┼─────────────────┤
│ 对称 AES │ 可逆 │ 一把密钥 │ 敏感字段加密存储 │
│ 非对称 RSA │ 可逆 │ 公钥+私钥 │ JWT 签名、密钥交换│
│ 哈希 SHA │ 不可逆 │ 无 │ 文件完整性校验 │
│ 密码哈希 │ 不可逆(慢) │ 盐+cost │ 用户密码存储 │
│ BCrypt │ │ │ │
└────────────┴──────────────┴────────────┴─────────────────┘
一次登录的密码学全景:
用户输入密码
│
├─ HTTPS (TLS: RSA/ECDHE 协商出 AES 会话密钥) ──→ 服务器
│
└─ 服务器: BCrypt.matches(输入, 数据库存的哈希)
│ 不存明文,不存可逆加密
▼
登录成功 → 签发 JWT (用 RSA 私钥签名)
客户端后续请求带 JWT
服务器用公钥验签
跟着做
步骤 1:用户密码用 BCrypt 存(本站做法)
Spring Security 内置支持,先加依赖(spring-boot-starter-security 自带 crypto):
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class PasswordService {
// strength=12 表示 2^12 次迭代,单次哈希约 200ms+
private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
public String hash(String rawPassword) {
// 自动生成随机盐,输出形如 $2a$12$N9qo8uLOickgx2ZMRZoMye...
return encoder.encode(rawPassword);
}
public boolean verify(String rawPassword, String storedHash) {
return encoder.matches(rawPassword, storedHash);
}
}
关键点:永远不要自己实现「SHA-256(密码+盐)」。BCrypt 的慢是特性——攻击者拿到数据库后,暴力破解每个密码要算 2^12 次,而 MD5 一秒能试几十亿个。
步骤 2:AES 加密敏感字段(如手机号)
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;
public class AesGcmService {
private static final int IV_LEN = 12; // GCM 推荐 96 位 IV
private final SecretKeySpec key;
public AesGcmService(byte[] keyBytes) { // 32 字节 = AES-256
this.key = new SecretKeySpec(keyBytes, "AES");
}
public String encrypt(String plaintext) throws Exception {
byte[] iv = new byte[IV_LEN];
new SecureRandom().nextBytes(iv); // 每次随机 IV,禁止复用
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));
byte[] ct = cipher.doFinal(plaintext.getBytes("UTF-8"));
// 存储格式: base64(iv) + ":" + base64(密文含认证标签)
return Base64.getEncoder().encodeToString(iv) + ":"
+ Base64.getEncoder().encodeToString(ct);
}
public String decrypt(String stored) throws Exception {
String[] parts = stored.split(":");
byte[] iv = Base64.getDecoder().decode(parts[0]);
byte[] ct = Base64.getDecoder().decode(parts[1]);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
return new String(cipher.doFinal(ct), "UTF-8");
}
}