信息安全从零到精通验收清单
信息安全不能学成“记住 AES、RSA、HTTPS、JWT 几个名词”。商业系统真正需要的是:知道每种安全手段解决什么问题,为什么单独使用不够,不这样做会出什么事故,代码应该怎么写,线上怎么排查,面试怎么讲清楚。
本页按“安全目标 -> 原理 -> Demo -> 商业场景 -> 排查 -> 面试”的顺序,把加密、哈希、签名、证书、TLS、JWT、接口签名、防重放、字段加密、脱敏、密钥管理串成一条完整学习链路。
最终学习目标
学完信息安全模块,至少要能做到:
| 能力 | 合格标准 | 不合格表现 |
|---|---|---|
| 安全目标识别 | 能区分防窃听、防篡改、防伪造、防重放、防泄露 | 只说“加密一下就安全” |
| 加密 | 能讲清对称加密、非对称加密、混合加密 | 认为 RSA 一定比 AES 安全 |
| 哈希 | 能解释哈希不可逆、摘要、慢哈希、加盐 | 把 SHA-256 当成可逆加密 |
| 签名 | 能解释私钥签名、公钥验签、HMAC、JWT 签名 | 认为签名会隐藏内容 |
| TLS | 能讲清证书链、CA、域名校验、握手、会话密钥 | 只知道 HTTPS 比 HTTP 安全 |
| 接口安全 | 能设计 appId、timestamp、nonce、sign、防重放、幂等 | 只验签不防重放 |
| 数据安全 | 能做字段加密、脱敏、数据分级、权限、审计 | 加密后就不做权限 |
| 密钥管理 | 能设计 keyVersion、轮换、禁用、重加密、泄露应急 | 密钥写代码或配置仓库 |
| 生产排查 | 能排查证书错误、验签失败、JWT 无效、密钥版本不匹配 | 出问题只能改代码重试 |
总学习路线
flowchart TD
A["安全目标"] --> B["编码、加密、哈希、签名边界"]
B --> C["对称加密 AES/SM4"]
B --> D["非对称加密 RSA/ECC/SM2"]
C --> E["混合加密"]
D --> E
B --> F["哈希、HMAC、密码慢哈希"]
E --> G["HTTPS/TLS 和证书链"]
F --> H["JWT 和接口签名"]
G --> I["开放接口防重放"]
H --> I
I --> J["字段加密、脱敏、审计"]
J --> K["密钥管理和泄露应急"]
K --> L["生产排查和面试闭环"]阶段一:先识别安全目标
很多安全事故不是因为“没用算法”,而是安全目标判断错了。
| 目标 | 要防什么 | 常用手段 | 只做错手段会怎样 |
|---|---|---|---|
| 防窃听 | 抓包看到内容 | HTTPS、AES、字段加密 | 只签名不加密,内容仍可见 |
| 防篡改 | 参数被改 | HMAC、RSA 签名、TLS 完整性校验 | 只加密不验完整性,可能被替换 |
| 防伪造 | 假冒调用方 | 证书、签名、appId/secret | 只传 appId 可被冒用 |
| 防重放 | 旧请求重复提交 | timestamp + nonce + Redis | 只验签无法阻止旧请求原样重放 |
| 防泄露 | 数据库、日志、导出泄露 | 字段加密、脱敏、权限、审计 | 加密但日志明文打印仍泄露 |
| 可追踪 | 出事后能审计 | 操作日志、密钥访问日志 | 无法知道谁导出了数据 |
为什么不能只说“加密”
加密只解决“内容不可见”。它不自动证明是谁发的,也不自动防止旧请求重放,更不替代权限控制。
flowchart TD
A["业务请求"] --> B{"需要内容保密?"}
B -- "是" --> C["加密"]
A --> D{"需要证明身份?"}
D -- "是" --> E["签名或证书"]
A --> F{"需要防旧请求重复提交?"}
F -- "是" --> G["timestamp + nonce"]
A --> H{"需要防越权访问?"}
H -- "是" --> I["认证、授权、数据权限"]阶段二:编码、加密、哈希、签名边界
| 概念 | 是否保密 | 是否可逆 | 解决什么 |
|---|---|---|---|
| Base64 | 否 | 是 | 二进制转文本 |
| AES | 是 | 是,持密钥可解 | 内容保密 |
| RSA 加密 | 是 | 是,持私钥可解 | 保护小段数据或会话密钥 |
| SHA-256 | 否 | 否 | 内容摘要、完整性指纹 |
| HMAC | 否 | 否 | 共享密钥签名 |
| RSA 签名 | 否 | 否 | 私钥身份、防篡改 |
Base64 为什么不是加密
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class Base64IsNotEncrypt {
public static void main(String[] args) {
String text = "idCard=110101199001011234";
String encoded = Base64.getEncoder().encodeToString(text.getBytes(StandardCharsets.UTF_8));
String decoded = new String(Base64.getDecoder().decode(encoded), StandardCharsets.UTF_8);
System.out.println(encoded);
System.out.println(decoded);
}
}任何人都能解码 Base64,所以不能把 Base64 当作安全保护。
阶段三:对称加密
是什么
对称加密使用同一把密钥加密和解密。
密文 = AES(明文, key)
明文 = AES-Decrypt(密文, 同一个 key)为什么需要
数据库字段、文件、网络数据量通常较大,需要高性能加密。AES 这类对称加密速度快,适合大量数据。
工作原理
flowchart TD
A["明文手机号"] --> B["随机 IV"]
A --> C["AES-GCM 加密"]
D["对称密钥 key"] --> C
B --> C
C --> E["密文 + 认证标签"]
E --> F["保存 IV + 密文"]
F --> G["解密时取出 IV"]
D --> H["AES-GCM 解密"]
G --> H
H --> I["恢复明文"]AES-GCM 推荐原因:它不仅加密内容,还能发现密文是否被篡改。ECB 不推荐,因为相同明文块会产生相同密文块,暴露数据模式。
Demo:AES-GCM 字段加密
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
public class AesGcmFieldEncrypt {
private static final int IV_LENGTH = 12;
private static final int TAG_BITS = 128;
private static final SecureRandom RANDOM = new SecureRandom();
public static String encrypt(String plainText, byte[] key) throws Exception {
byte[] iv = new byte[IV_LENGTH];
RANDOM.nextBytes(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(TAG_BITS, iv));
byte[] cipherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
ByteBuffer buffer = ByteBuffer.allocate(iv.length + cipherText.length);
buffer.put(iv);
buffer.put(cipherText);
return Base64.getEncoder().encodeToString(buffer.array());
}
public static String decrypt(String encrypted, byte[] key) throws Exception {
byte[] all = Base64.getDecoder().decode(encrypted);
ByteBuffer buffer = ByteBuffer.wrap(all);
byte[] iv = new byte[IV_LENGTH];
buffer.get(iv);
byte[] cipherText = new byte[buffer.remaining()];
buffer.get(cipherText);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(TAG_BITS, iv));
return new String(cipher.doFinal(cipherText), StandardCharsets.UTF_8);
}
}不这样会怎样
| 错误 | 后果 |
|---|---|
| IV 固定 | 相同明文关系可能暴露,GCM 下风险严重 |
| 密钥硬编码 | 代码泄露等于数据泄露 |
| 使用 ECB | 数据模式暴露 |
| 密文无 keyVersion | 密钥轮换时不知道用哪把旧密钥解 |
| 加密后不做权限 | 有密钥或接口权限仍可批量读取敏感数据 |
阶段四:非对称加密和数字签名
非对称加密是什么
非对称加密使用一对密钥:公钥可以公开,私钥必须保密。
flowchart TD
A["服务端生成公私钥"] --> B["公钥给调用方"]
A --> C["私钥自己保存"]
D["调用方用公钥加密 AES key"] --> E["encryptedKey"]
E --> F["服务端用私钥解密"]数字签名是什么
签名不是加密,不隐藏内容。它证明“确实是私钥持有者发的,并且内容没被改”。
flowchart TD
A["请求内容"] --> B["计算摘要"]
B --> C["调用方私钥签名"]
C --> D["签名值 sign"]
A --> E["请求 + sign"]
D --> E
E --> F["服务端用调用方公钥验签"]
F --> G{"验签通过?"}
G -- "是" --> H["来源可信且未篡改"]
G -- "否" --> I["拒绝请求"]Demo:RSA 签名验签
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.util.Base64;
public class RsaSignVerify {
public static void main(String[] args) throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
KeyPair pair = generator.generateKeyPair();
String body = "appId=hospital-a&nonce=n1×tamp=1782892800000";
String sign = sign(body, pair.getPrivate());
System.out.println(verify(body, sign, pair.getPublic()));
}
static String sign(String body, PrivateKey privateKey) throws Exception {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(body.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(signature.sign());
}
static boolean verify(String body, String sign, PublicKey publicKey) throws Exception {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);
signature.update(body.getBytes(StandardCharsets.UTF_8));
return signature.verify(Base64.getDecoder().decode(sign));
}
}阶段五:哈希、HMAC、密码存储
哈希不是加密
哈希是单向摘要,不能解密。它适合校验内容是否变化,不适合保存需要恢复的明文。
flowchart TD
A["原文"] --> B["SHA-256"]
B --> C["摘要"]
D["原文改一个字符"] --> E["SHA-256"]
E --> F["完全不同的摘要"]为什么密码不能用 AES
密码存储的目标不是“以后解密拿出原密码”,而是“用户输入密码时能验证是否正确”。如果用 AES,密钥泄露后所有密码都能被还原。正确做法是慢哈希:BCrypt、Argon2、PBKDF2。
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class PasswordHashDemo {
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
String hash = encoder.encode("123456");
System.out.println(hash);
System.out.println(encoder.matches("123456", hash));
}
}BCrypt 的密文中包含算法版本、成本因子、盐和哈希结果。校验时从密文中取出盐和成本因子,用同样规则再算一次并比较。
HMAC 是什么
HMAC 是“带共享密钥的哈希”,常用于内部接口签名。
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class HmacSha256Demo {
public static String sign(String data, String secret) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes(StandardCharsets.UTF_8)));
}
}HMAC 简单快,但双方都保存同一个 secret。任何一方泄露,攻击者就能伪造请求。
阶段六:HTTPS/TLS 和证书
TLS 解决什么
TLS 在不可信网络上解决:
- 服务器身份可信。
- 会话密钥安全协商。
- 后续数据加密传输。
- 数据完整性校验。
flowchart TD
A["ClientHello"] --> B["ServerHello"]
B --> C["服务端证书"]
C --> D["客户端校验证书链、域名、有效期"]
D --> E{"证书可信?"}
E -- "否" --> F["握手失败"]
E -- "是" --> G["协商会话密钥"]
G --> H["使用对称密钥传 HTTP 数据"]证书为什么可信
证书可信不是因为服务器自己说可信,而是浏览器或 Java 信任库能沿着证书链追溯到可信根 CA。
flowchart TD
A["站点证书"] --> B["中间 CA"]
B --> C["根 CA"]
C --> D["浏览器或操作系统信任"]常见排查
| 错误 | 常见原因 | 排查 |
|---|---|---|
PKIX path building failed | Java 不信任证书链 | 检查 truststore 和中间证书 |
| 证书域名不匹配 | SAN 没有当前域名 | 查看证书 SAN |
| 证书过期 | 有效期到期 | 配证书到期告警 |
handshake_failure | TLS 版本或套件不兼容 | 检查 JDK、Nginx、网关配置 |
阶段七:JWT、Token 和 Session 安全
普通 JWT 通常是签名,不是加密。header 和 payload 是 Base64Url 编码,别人拿到 Token 可以解码看到内容。
flowchart TD
A["header.payload"] --> B["用密钥或私钥签名"]
B --> C["signature"]
C --> D["JWT"]
D --> E["服务端验签"]
E --> F{"签名正确且未过期?"}
F -- "是" --> G["恢复身份"]
F -- "否" --> H["拒绝请求"]JWT 生产规则:
| 规则 | 原因 |
|---|---|
| 不放身份证、手机号、密码 | payload 可解码 |
| 设置短有效期 | 降低泄露风险 |
| 使用 refresh token 或 tokenVersion | 支持续期和踢下线 |
| 退出登录使用黑名单或版本号 | JWT 默认无状态,不能自动失效 |
| 全站 HTTPS | 防止 Token 被窃听 |
阶段八:开放接口签名与防重放
为什么 HTTPS 后还要接口签名
HTTPS 保护传输链路,但开放接口还要解决:
- 业务调用方是谁。
- 报文有没有被代理、网关、日志系统或内部链路篡改。
- 同一个已签名请求是否被重复提交。
- 请求是否可审计。
标准请求结构
{
"appId": "hospital-a",
"timestamp": 1782892800000,
"nonce": "8f1c6d2a0c7b4a4d",
"bodyHash": "SHA256请求体摘要",
"sign": "HMAC或RSA签名"
}验签流程
flowchart TD
A["收到请求"] --> B["校验 appId"]
B --> C["校验 timestamp 时间窗口"]
C --> D["校验 nonce 未使用"]
D --> E["计算 bodyHash"]
E --> F["重建签名串"]
F --> G["HMAC 或 RSA 验签"]
G --> H{"验签通过?"}
H -- "否" --> I["拒绝并记录安全日志"]
H -- "是" --> J["写入 nonce 防重放"]
J --> K["进入业务幂等校验"]防重放和幂等区别
| 概念 | 目标 | 常用字段 |
|---|---|---|
| 防重放 | 防止同一个已签名请求重复提交 | nonce |
| 幂等 | 防止同一业务重复生效 | requestId、orderNo、eventId |
如果客户端超时后重新发起创建订单,每次生成新 nonce,防重放不会拦截;业务仍要靠 requestId 保证不会创建两笔订单。
阶段九:字段加密、脱敏、审计
数据分级
| 数据 | 风险 | 常见保护 |
|---|---|---|
| 密码 | 极高 | 慢哈希,不可逆 |
| 身份证号、手机号 | 高 | AES 字段加密、脱敏展示 |
| 病历、检验报告 | 高 | 权限、审计、加密存储 |
| 订单号、流水号 | 中 | 权限控制、日志脱敏 |
| 普通配置 | 低 | 基础权限 |
脱敏不是加密
脱敏是展示层保护,例如 138****1234。它不能恢复完整明文,通常用于页面展示、日志、导出预览。加密是存储层保护,持密钥可解密。
public class MaskDemo {
public static String maskPhone(String phone) {
if (phone == null || phone.length() != 11) {
return phone;
}
return phone.substring(0, 3) + "****" + phone.substring(7);
}
}审计要记录什么
| 操作 | 审计字段 |
|---|---|
| 查询敏感数据 | 用户、时间、IP、查询条件、数据范围 |
| 导出数据 | 导出人、审批单、导出字段、数量、文件摘要 |
| 修改密钥 | 操作人、旧版本、新版本、原因 |
| 接口验签失败 | appId、IP、错误类型、nonce、traceId |
阶段十:密钥管理和泄露应急
密钥管理原则
| 原则 | 解释 |
|---|---|
| 不硬编码 | 不写 Java 代码、前端代码、Git 仓库 |
| 分环境隔离 | dev/test/prod 不共用密钥 |
| 最小权限 | 服务只能读自己的密钥 |
| keyVersion | 密文记录使用的密钥版本 |
| 定期轮换 | 新数据用新密钥,旧数据逐步迁移 |
| 访问审计 | 谁读了密钥要可追踪 |
| 泄露应急 | 能禁用旧密钥、换新、重加密 |
字段加密表设计
create table patient_secure_info (
id bigint primary key,
patient_id bigint not null,
id_card_cipher varchar(512) not null,
phone_cipher varchar(512),
key_version varchar(32) not null,
created_at datetime not null,
updated_at datetime not null
);密钥轮换流程
flowchart TD
A["生成新密钥 v2"] --> B["配置服务支持 v1 和 v2"]
B --> C["新写入数据使用 v2"]
C --> D["后台任务读取 v1 密文"]
D --> E["用 v1 解密"]
E --> F["用 v2 重新加密"]
F --> G["更新 keyVersion"]
G --> H["确认无 v1 数据"]
H --> I["禁用或归档 v1"]商业常用场景
医疗数据采集平台
链路:
flowchart TD
A["医院系统"] --> B["HTTPS 或双向 TLS"]
B --> C["appId 识别调用方"]
C --> D["timestamp + nonce 防重放"]
D --> E["HMAC/RSA 验签"]
E --> F["业务权限校验"]
F --> G["敏感字段加密落库"]
G --> H["查询脱敏展示"]
H --> I["导出审批和审计"]重点:
- HTTPS 保护传输。
- 签名证明调用方和防篡改。
- nonce 防止重复提交。
- 数据落库按分级决定加密策略。
- 展示和日志必须脱敏。
- 导出、下载、批量查询必须审计。
开放平台接口
适合 appId + secret 或 appId + 公钥的模式。内部系统可用 HMAC,外部合作方更适合 RSA/SM2 签名,因为服务端只保存公钥,不保存对方私钥。
后台管理系统
- 登录必须 HTTPS。
- 密码使用 BCrypt/Argon2。
- JWT 不放敏感信息。
- 权限变更要让 Token 或权限缓存失效。
- 敏感操作二次确认和审计。
生产排查流程
验签失败
flowchart TD
A["验签失败"] --> B["appId 是否正确"]
B --> C["密钥或公钥版本是否匹配"]
C --> D["签名串字段顺序是否一致"]
D --> E["bodyHash 是否基于原始 body"]
E --> F["字符集是否 UTF-8"]
F --> G["Base64/URL 编码是否一致"]
G --> H["客户端和服务端时间是否偏差过大"]JWT 无效
flowchart TD
A["JWT 无效"] --> B["请求头是否 Bearer"]
B --> C["签名密钥是否一致"]
C --> D["算法是否匹配"]
D --> E["是否过期"]
E --> F["issuer/audience 是否正确"]
F --> G["是否被黑名单或 tokenVersion 拦截"]证书错误
flowchart TD
A["证书错误"] --> B["域名 SAN 是否匹配"]
B --> C["证书是否过期"]
C --> D["证书链是否完整"]
D --> E["客户端是否信任根 CA"]
E --> F["TLS 版本和套件是否兼容"]字段解密失败
flowchart TD
A["字段解密失败"] --> B["keyVersion 是否存在"]
B --> C["密钥是否被轮换或禁用"]
C --> D["IV 是否保存完整"]
D --> E["密文是否被截断或转码"]
E --> F["算法和 GCM tag 是否匹配"]常见坑
| 坑 | 后果 | 正确做法 |
|---|---|---|
| Base64 当加密 | 敏感数据直接泄露 | 只当编码 |
| JWT 放身份证号 | Token 泄露后可直接解码 | JWT 只放必要声明 |
| 只签名不防重放 | 旧请求可重复提交 | timestamp + nonce |
| JSON 原文直接拼签名 | 空格、顺序不同导致验签失败 | 规范化或 bodyHash |
| AES-GCM 重复 IV | 严重破坏安全性 | 每次随机 IV |
| 密钥无版本 | 轮换困难 | keyVersion |
| 日志打印明文 | 加密存储也挡不住日志泄露 | 日志脱敏 |
| 证书无监控 | 到期后接口中断 | 到期告警和续期流程 |
面试标准回答
对称加密和非对称加密区别
对称加密使用同一把密钥加解密,性能高,适合加密大量数据,比如字段、文件和 HTTPS 业务数据。它的问题是密钥分发和保存困难。非对称加密使用公钥和私钥,公钥可以公开,私钥必须保密,适合密钥交换和数字签名,但性能较低,不适合直接加密大报文。生产中常用混合加密:用非对称能力保护或协商会话密钥,再用对称加密传输业务数据。哈希、签名、加密怎么区分
加密解决内容保密,持有密钥可以解密;哈希是单向摘要,不能解密,主要用于完整性校验和密码慢哈希;签名解决身份可信和防篡改,私钥签名、公钥验签,签名本身不隐藏内容。Base64 只是编码,不是安全手段。HTTPS 后为什么还要接口签名
HTTPS 保护的是传输链路,可以防窃听和中间人篡改。但开放接口还需要业务层识别调用方、防止旧请求重放、做审计和权限控制,所以常用 appId、timestamp、nonce、bodyHash 和 sign。签名防篡改和证明身份,timestamp 限制时间窗口,nonce 防止同一个请求重复使用。密码为什么不能可逆加密
密码存储不需要还原明文,只需要验证用户输入是否正确。如果用 AES 这类可逆加密,密钥泄露后所有密码都能被解出。正确做法是使用 BCrypt、Argon2、PBKDF2 这类慢哈希,并使用盐和成本因子,提高撞库和暴力破解成本。密钥泄露怎么办
首先要冻结或禁用泄露密钥,切换到新密钥版本,排查密钥访问日志和影响范围。字段加密要依赖 keyVersion 支持新旧密钥并存,再通过后台任务逐步用旧密钥解密、用新密钥重加密。接口签名密钥泄露则要更换 appSecret 或公私钥,必要时让调用方重新接入,并分析异常请求和重放风险。学懂验收问题
下面问题答不上来,就还没有真正学懂:
- 为什么 Base64 不是加密?
- 为什么 AES-GCM 的 IV 不能重复?
- 为什么 RSA 不适合直接加密整个大 JSON?
- 签名为什么不能隐藏内容?
- HMAC 和 RSA 签名怎么选?
- 为什么 HTTPS 使用混合加密?
- 证书链、CA、域名校验分别解决什么?
- JWT 为什么不能放身份证号和手机号?
- 只做签名不做 nonce 会有什么风险?
- 防重放和幂等为什么不是一回事?
- 密码为什么不能用 AES 存?
- 字段加密为什么要保存 keyVersion?
- 日志脱敏和数据库加密分别解决什么?
- 验签失败要从哪些维度排查?
- 密钥泄露后的应急流程是什么?
关联知识点跳转
| 主题 | 继续学习 |
|---|---|
| 信息安全总览 | 信息安全 |
| 加密基础 | 对称加密、非对称加密、哈希与签名 |
| 商业落地 | 信息安全商业场景训练营 |
| TLS | HTTPS/TLS 全过程原理 |
| 接口签名 | 开放接口签名与防重放 |
| Spring Security | Spring Security |
| JWT | Spring Security JWT |
| K8s Secret | K8s 配置与密钥 |
| DevOps 证书和密钥 | DevOps 从零到精通验收清单 |
| 面试 | 信息安全面试题 |
