Skip to content

信息安全从零到精通验收清单

信息安全不能学成“记住 AES、RSA、HTTPS、JWT 几个名词”。商业系统真正需要的是:知道每种安全手段解决什么问题,为什么单独使用不够,不这样做会出什么事故,代码应该怎么写,线上怎么排查,面试怎么讲清楚。

本页按“安全目标 -> 原理 -> Demo -> 商业场景 -> 排查 -> 面试”的顺序,把加密、哈希、签名、证书、TLS、JWT、接口签名、防重放、字段加密、脱敏、密钥管理串成一条完整学习链路。

最终学习目标

学完信息安全模块,至少要能做到:

能力合格标准不合格表现
安全目标识别能区分防窃听、防篡改、防伪造、防重放、防泄露只说“加密一下就安全”
加密能讲清对称加密、非对称加密、混合加密认为 RSA 一定比 AES 安全
哈希能解释哈希不可逆、摘要、慢哈希、加盐把 SHA-256 当成可逆加密
签名能解释私钥签名、公钥验签、HMAC、JWT 签名认为签名会隐藏内容
TLS能讲清证书链、CA、域名校验、握手、会话密钥只知道 HTTPS 比 HTTP 安全
接口安全能设计 appId、timestamp、nonce、sign、防重放、幂等只验签不防重放
数据安全能做字段加密、脱敏、数据分级、权限、审计加密后就不做权限
密钥管理能设计 keyVersion、轮换、禁用、重加密、泄露应急密钥写代码或配置仓库
生产排查能排查证书错误、验签失败、JWT 无效、密钥版本不匹配出问题只能改代码重试

总学习路线

mermaid
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只验签无法阻止旧请求原样重放
防泄露数据库、日志、导出泄露字段加密、脱敏、权限、审计加密但日志明文打印仍泄露
可追踪出事后能审计操作日志、密钥访问日志无法知道谁导出了数据

为什么不能只说“加密”

加密只解决“内容不可见”。它不自动证明是谁发的,也不自动防止旧请求重放,更不替代权限控制。

mermaid
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 为什么不是加密

java
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 当作安全保护。

阶段三:对称加密

是什么

对称加密使用同一把密钥加密和解密。

text
密文 = AES(明文, key)
明文 = AES-Decrypt(密文, 同一个 key)

为什么需要

数据库字段、文件、网络数据量通常较大,需要高性能加密。AES 这类对称加密速度快,适合大量数据。

工作原理

mermaid
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 字段加密

java
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密钥轮换时不知道用哪把旧密钥解
加密后不做权限有密钥或接口权限仍可批量读取敏感数据

阶段四:非对称加密和数字签名

非对称加密是什么

非对称加密使用一对密钥:公钥可以公开,私钥必须保密。

mermaid
flowchart TD
    A["服务端生成公私钥"] --> B["公钥给调用方"]
    A --> C["私钥自己保存"]
    D["调用方用公钥加密 AES key"] --> E["encryptedKey"]
    E --> F["服务端用私钥解密"]

数字签名是什么

签名不是加密,不隐藏内容。它证明“确实是私钥持有者发的,并且内容没被改”。

mermaid
flowchart TD
    A["请求内容"] --> B["计算摘要"]
    B --> C["调用方私钥签名"]
    C --> D["签名值 sign"]
    A --> E["请求 + sign"]
    D --> E
    E --> F["服务端用调用方公钥验签"]
    F --> G{"验签通过?"}
    G -- "是" --> H["来源可信且未篡改"]
    G -- "否" --> I["拒绝请求"]

Demo:RSA 签名验签

java
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&timestamp=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、密码存储

哈希不是加密

哈希是单向摘要,不能解密。它适合校验内容是否变化,不适合保存需要恢复的明文。

mermaid
flowchart TD
    A["原文"] --> B["SHA-256"]
    B --> C["摘要"]
    D["原文改一个字符"] --> E["SHA-256"]
    E --> F["完全不同的摘要"]

为什么密码不能用 AES

密码存储的目标不是“以后解密拿出原密码”,而是“用户输入密码时能验证是否正确”。如果用 AES,密钥泄露后所有密码都能被还原。正确做法是慢哈希:BCrypt、Argon2、PBKDF2。

java
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 是“带共享密钥的哈希”,常用于内部接口签名。

java
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 在不可信网络上解决:

  1. 服务器身份可信。
  2. 会话密钥安全协商。
  3. 后续数据加密传输。
  4. 数据完整性校验。
mermaid
flowchart TD
    A["ClientHello"] --> B["ServerHello"]
    B --> C["服务端证书"]
    C --> D["客户端校验证书链、域名、有效期"]
    D --> E{"证书可信?"}
    E -- "否" --> F["握手失败"]
    E -- "是" --> G["协商会话密钥"]
    G --> H["使用对称密钥传 HTTP 数据"]

证书为什么可信

证书可信不是因为服务器自己说可信,而是浏览器或 Java 信任库能沿着证书链追溯到可信根 CA。

mermaid
flowchart TD
    A["站点证书"] --> B["中间 CA"]
    B --> C["根 CA"]
    C --> D["浏览器或操作系统信任"]

常见排查

错误常见原因排查
PKIX path building failedJava 不信任证书链检查 truststore 和中间证书
证书域名不匹配SAN 没有当前域名查看证书 SAN
证书过期有效期到期配证书到期告警
handshake_failureTLS 版本或套件不兼容检查 JDK、Nginx、网关配置

阶段七:JWT、Token 和 Session 安全

普通 JWT 通常是签名,不是加密。headerpayload 是 Base64Url 编码,别人拿到 Token 可以解码看到内容。

mermaid
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 保护传输链路,但开放接口还要解决:

  1. 业务调用方是谁。
  2. 报文有没有被代理、网关、日志系统或内部链路篡改。
  3. 同一个已签名请求是否被重复提交。
  4. 请求是否可审计。

标准请求结构

json
{
  "appId": "hospital-a",
  "timestamp": 1782892800000,
  "nonce": "8f1c6d2a0c7b4a4d",
  "bodyHash": "SHA256请求体摘要",
  "sign": "HMAC或RSA签名"
}

验签流程

mermaid
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。它不能恢复完整明文,通常用于页面展示、日志、导出预览。加密是存储层保护,持密钥可解密。

java
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密文记录使用的密钥版本
定期轮换新数据用新密钥,旧数据逐步迁移
访问审计谁读了密钥要可追踪
泄露应急能禁用旧密钥、换新、重加密

字段加密表设计

sql
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
);

密钥轮换流程

mermaid
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"]

商业常用场景

医疗数据采集平台

链路:

mermaid
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["导出审批和审计"]

重点:

  1. HTTPS 保护传输。
  2. 签名证明调用方和防篡改。
  3. nonce 防止重复提交。
  4. 数据落库按分级决定加密策略。
  5. 展示和日志必须脱敏。
  6. 导出、下载、批量查询必须审计。

开放平台接口

适合 appId + secret 或 appId + 公钥的模式。内部系统可用 HMAC,外部合作方更适合 RSA/SM2 签名,因为服务端只保存公钥,不保存对方私钥。

后台管理系统

  1. 登录必须 HTTPS。
  2. 密码使用 BCrypt/Argon2。
  3. JWT 不放敏感信息。
  4. 权限变更要让 Token 或权限缓存失效。
  5. 敏感操作二次确认和审计。

生产排查流程

验签失败

mermaid
flowchart TD
    A["验签失败"] --> B["appId 是否正确"]
    B --> C["密钥或公钥版本是否匹配"]
    C --> D["签名串字段顺序是否一致"]
    D --> E["bodyHash 是否基于原始 body"]
    E --> F["字符集是否 UTF-8"]
    F --> G["Base64/URL 编码是否一致"]
    G --> H["客户端和服务端时间是否偏差过大"]

JWT 无效

mermaid
flowchart TD
    A["JWT 无效"] --> B["请求头是否 Bearer"]
    B --> C["签名密钥是否一致"]
    C --> D["算法是否匹配"]
    D --> E["是否过期"]
    E --> F["issuer/audience 是否正确"]
    F --> G["是否被黑名单或 tokenVersion 拦截"]

证书错误

mermaid
flowchart TD
    A["证书错误"] --> B["域名 SAN 是否匹配"]
    B --> C["证书是否过期"]
    C --> D["证书链是否完整"]
    D --> E["客户端是否信任根 CA"]
    E --> F["TLS 版本和套件是否兼容"]

字段解密失败

mermaid
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
日志打印明文加密存储也挡不住日志泄露日志脱敏
证书无监控到期后接口中断到期告警和续期流程

面试标准回答

对称加密和非对称加密区别

text
对称加密使用同一把密钥加解密,性能高,适合加密大量数据,比如字段、文件和 HTTPS 业务数据。它的问题是密钥分发和保存困难。非对称加密使用公钥和私钥,公钥可以公开,私钥必须保密,适合密钥交换和数字签名,但性能较低,不适合直接加密大报文。生产中常用混合加密:用非对称能力保护或协商会话密钥,再用对称加密传输业务数据。

哈希、签名、加密怎么区分

text
加密解决内容保密,持有密钥可以解密;哈希是单向摘要,不能解密,主要用于完整性校验和密码慢哈希;签名解决身份可信和防篡改,私钥签名、公钥验签,签名本身不隐藏内容。Base64 只是编码,不是安全手段。

HTTPS 后为什么还要接口签名

text
HTTPS 保护的是传输链路,可以防窃听和中间人篡改。但开放接口还需要业务层识别调用方、防止旧请求重放、做审计和权限控制,所以常用 appId、timestamp、nonce、bodyHash 和 sign。签名防篡改和证明身份,timestamp 限制时间窗口,nonce 防止同一个请求重复使用。

密码为什么不能可逆加密

text
密码存储不需要还原明文,只需要验证用户输入是否正确。如果用 AES 这类可逆加密,密钥泄露后所有密码都能被解出。正确做法是使用 BCrypt、Argon2、PBKDF2 这类慢哈希,并使用盐和成本因子,提高撞库和暴力破解成本。

密钥泄露怎么办

text
首先要冻结或禁用泄露密钥,切换到新密钥版本,排查密钥访问日志和影响范围。字段加密要依赖 keyVersion 支持新旧密钥并存,再通过后台任务逐步用旧密钥解密、用新密钥重加密。接口签名密钥泄露则要更换 appSecret 或公私钥,必要时让调用方重新接入,并分析异常请求和重放风险。

学懂验收问题

下面问题答不上来,就还没有真正学懂:

  1. 为什么 Base64 不是加密?
  2. 为什么 AES-GCM 的 IV 不能重复?
  3. 为什么 RSA 不适合直接加密整个大 JSON?
  4. 签名为什么不能隐藏内容?
  5. HMAC 和 RSA 签名怎么选?
  6. 为什么 HTTPS 使用混合加密?
  7. 证书链、CA、域名校验分别解决什么?
  8. JWT 为什么不能放身份证号和手机号?
  9. 只做签名不做 nonce 会有什么风险?
  10. 防重放和幂等为什么不是一回事?
  11. 密码为什么不能用 AES 存?
  12. 字段加密为什么要保存 keyVersion?
  13. 日志脱敏和数据库加密分别解决什么?
  14. 验签失败要从哪些维度排查?
  15. 密钥泄露后的应急流程是什么?

关联知识点跳转

主题继续学习
信息安全总览信息安全
加密基础对称加密、非对称加密、哈希与签名
商业落地信息安全商业场景训练营
TLSHTTPS/TLS 全过程原理
接口签名开放接口签名与防重放
Spring SecuritySpring Security
JWTSpring Security JWT
K8s SecretK8s 配置与密钥
DevOps 证书和密钥DevOps 从零到精通验收清单
面试信息安全面试题