CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
DevSecOps 入门/课时/密码学
RawBlame
课时目录12 节

DevSecOps 入门

  1. 01DevSecOps 概览
  2. 02网络安全基础
  3. 03密码学
  4. 04身份与访问
  5. 05应用安全
  6. 06威胁建模
  7. 07云安全
  8. 08容器安全
  9. 09供应链安全
  10. 10检测与 SIEM
  11. 11Incident 响应
  12. 12合规与扫描

密码学

对称/非对称、哈希与 PKI。

35 分钟

密码学

这一课你将能做到

  1. 说清对称加密、非对称加密、哈希各自解决什么问题,并能判断一个场景该用哪个
  2. 在 Spring Boot 中用 BCrypt 安全地存储用户密码,说清为什么不能 MD5/SHA-256 裸存
  3. 用 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");
    }
}

用 GCM 模式而不是 ECB/CBC:GCM 自带完整性校验(AEAD),密文被篡改会直接解密失败抛异常。

步骤 3:哈希校验文件完整性

# 发布文件时附上 SHA-256,下载方校验防篡改
certutil -hashfile app.jar SHA256        # Windows
sha256sum app.jar                        # Linux

Java 里算摘要:

import java.security.MessageDigest;
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(data);

步骤 4:理解密钥管理(别把密钥写死在代码里)

错误示范(无数生产事故的来源):

// 千万别这么干!密钥进 git 历史 = 已泄漏
private static final byte[] KEY = "hardcoded-key-1234".getBytes();

正确分层:

开发环境  → 环境变量 / .env (gitignore)
测试/生产 → 密钥管理服务: 阿里云 KMS / AWS KMS / HashiCorp Vault
密钥本身  → 定期轮换 (rotation),旧密钥保留一段时间用于解密历史数据

Spring Boot 从环境变量读:

# application.yml
security:
  aes-key: ${AES_KEY}   # 启动时从环境变量注入,不落盘不进 git

讲清楚要点

  1. 密码存储专用算法:BCrypt、scrypt、Argon2。它们的共同点是「故意慢+加盐」。SHA-256 再快也不是为密码设计的——快恰恰是缺点。登录接口配合限流(防在线爆破)+ BCrypt(防离线爆破)才完整。

  2. 加盐(Salt)防彩虹表:相同密码 + 不同盐 = 不同哈希。攻击者无法预计算。BCrypt 自动处理盐并把它存在输出里,你不需要单独存盐字段。

  3. AES 模式选择:ECB 禁用(相同明文块产生相同密文块,企鹅图梗),CBC 需小心填充预言攻击,GCM 是现代首选(加密+认证一体)。IV 绝不能复用,随机生成即可(GCM 下 IV 唯一性即可,不需保密)。

  4. 非对称的典型用途不是加密数据:RSA 加解密慢且有长度限制。实际用法是「加密对称密钥」(数字信封)和「签名」(私钥签、公钥验,证明身份+防篡改)。JWT 的 RS256 就是 RSA 签名。

  5. 密钥生命周期:生成(真随机源 SecureRandom)→ 分发(绝不走明文凭据)→ 存储(KMS/HSM,不硬编码)→ 轮换(定期更换)→ 销毁。泄漏应急:立刻轮换,旧密钥标记为「仅解密」。

你可能会懵的地方

  • 「哈希」和「加密」到底什么区别? 加密可逆(有密钥能还原),哈希不可逆。所以密码存哈希(连你自己都不知道用户密码),手机号存加密(业务上还要展示和拨打,必须能还原)。

  • Base64 是加密吗? 不是,是编码,任何人都能解码。把密码 Base64 后存库等于明文存储,这是真实面试翻车点和真实生产事故。

  • BCrypt 慢会不会拖垮登录接口? 单次约 100-300ms,正常登录无感。要防的是攻击者用登录接口在线爆破——用限流(同 IP 每分钟 5 次)+ 验证码 + 账号锁定解决,而不是把 BCrypt 的 cost 调低。

  • JWT 的 secret 放哪? HS256(对称):secret 放环境变量/KMS,至少 256 位随机。RS256(非对称):私钥 KMS 保管,公钥可以写在配置里分发。微服务架构推荐 RS256——验签服务不需要拿到私钥。

  • HTTPS 都加密了,数据库里手机号为什么还要加密? HTTPS 只保护传输链路。数据库加密防的是:DBA 拖库、备份文件泄漏、SQL 注入读到明文。层不同,防的人不同。

错法对照

错法问题正解
MD5(密码) 存库彩虹表秒破BCrypt(12) / Argon2
SHA-256(密码) 存库GPU 每秒数十亿次爆破BCrypt 故意慢
Base64「加密」编码非加密,人人可解AES-GCM 或 BCrypt
AES-ECB 模式相同明文块泄露模式AES-GCM + 随机 IV
密钥硬编码进代码/git仓库泄漏=密钥泄漏环境变量 + KMS + 定期轮换
用 new Random() 生成密钥/IV伪随机可预测SecureRandom

卡点急救

  1. 解密报 BadPaddingException / AEADBadTagException:三种可能——密钥错了、iv 和密文的拼接格式和加密端不一致、密文被截断/篡改。打印双方 base64 后的字节长度逐一比对。

  2. 跨语言 AES 对不上(Java 加密 Python 解不开):逐项核对四个参数必须完全一致:模式(GCM)、填充(NoPadding)、IV 长度(12)、认证标签长度(128 bit)。GCM 标签在 Java 里是附加在密文末尾的,别的语言可能要拆开来传。

  3. BCrypt 校验偶发失败:检查密码前后是否被 trim/URL 编码改了;BCrypt 只认密码前 72 字节(超长密码会被截断,前后端行为可能不一致);确认数据库字段够长(BCrypt 输出 60 字符,列宽至少 VARCHAR(60))。

面试常问

  • 密码为什么不能 MD5 存?该用什么? MD5/SHA 系列是「快哈希」,为完整性校验设计,GPU 每秒可算数十亿次,配合彩虹表可秒破常见密码。密码存储要用慢哈希:BCrypt/scrypt/Argon2,特点是可调 cost 因子(迭代次数)+ 自动加盐,把单次哈希成本提到百毫秒级,让暴力破解在经济上不可行。

  • 对称和非对称加密的区别,各举个例子? 对称(AES):同一把密钥加解密,快,适合大数据量,难点是密钥分发。非对称(RSA/ECC):公钥加密私钥解密(机密性),私钥签名公钥验签(身份认证),慢,适合小数据。实际系统混用:TLS 用非对称协商出对称会话密钥,之后用对称传数据——结合两者优点。

  • JWT 安全吗?注意什么? JWT 本身只是「签名防篡改」,payload 是 Base64 可读的——别放敏感信息。注意点:算法用 RS256 而非 HS256(多服务场景);必须校验签名和 exp;密钥要够长且保密;敏感操作配合短 TTL + refresh token 轮换;禁用 alg=none 和算法降级。

自测三问

  1. BCrypt 的「慢」为什么是优点而不是缺点?
  2. 什么数据用哈希存、什么数据用加密存、什么数据可以明文存?各举本站一例。
  3. AES-GCM 里的 IV 能复用吗?为什么?

小结一句话

密码学的工程纪律只有几条:密码用 BCrypt 慢哈希、敏感字段用 AES-GCM、签名用 RSA/ECC、密钥永不硬编码——剩下的都是细节。

延伸

  • OWASP Password Storage Cheat Sheet
  • 下一课:身份与访问——RBAC、最小权限、Zero Trust,以及本站 Spring Security 的权限模型设计。
加载本节练习…
← 上一课网络安全基础下一课 →身份与访问