Skip to content

信息安全 商业场景训练营

这页把信息安全放到真实商业系统里学习。目标不是背 AES、RSA、SHA、JWT、TLS 这些名词,而是能解释一个开放接口如何防窃听、防篡改、防重放,敏感字段如何加密和脱敏,密码为什么不能可逆加密,密钥泄露后怎么止血和轮换。

训练目标

学完这页,你要能做到:

  1. 区分编码、哈希、对称加密、非对称加密、数字签名、HMAC。
  2. 解释 HTTPS/TLS 为什么使用“非对称加密协商密钥 + 对称加密传输数据”。
  3. 设计开放接口的 appId、timestamp、nonce、bodyHash、signature 防篡改和防重放方案。
  4. 设计身份证、手机号、银行卡等敏感字段的加密、脱敏、查询和审计方案。
  5. 解释密码为什么用 BCrypt/Argon2/PBKDF2 慢哈希,而不是 AES、RSA、MD5。
  6. 解释 JWT 是签名 Token,不是默认加密,payload 不能放敏感信息。
  7. 设计密钥版本、轮换、禁用、泄露应急和审计。
  8. 排查签名失败、时间戳失败、nonce 重复、证书过期、密钥版本不一致。

商业安全总图

以“医院系统调用医疗数据采集平台开放接口”为例:

mermaid
flowchart TD
    A["医院系统"] --> B["HTTPS 传输防窃听"]
    B --> C["appId 识别调用方"]
    C --> D["timestamp + nonce 防重放"]
    D --> E["bodyHash 防正文被改"]
    E --> F["HMAC/RSA 验签防篡改"]
    F --> G["业务权限校验"]
    G --> H["敏感字段加密落库"]
    H --> I["脱敏展示和审计日志"]

注意:HTTPS 保护传输链路,接口签名保护业务报文完整性和调用方身份,两者不是互相替代关系。很多开放平台同时使用 HTTPS 和业务签名。

基础概念边界

概念是否可逆解决什么例子
编码可逆数据表示转换Base64、URL Encode
哈希不可逆摘要、完整性校验SHA-256、SM3
HMAC不可逆带密钥的完整性校验HMAC-SHA256
对称加密可逆用同一密钥加解密数据AES、SM4
非对称加密可逆公钥加密、私钥解密RSA、SM2
数字签名不可逆校验私钥签名、公钥验签RSA-SHA256
TLS协议保护传输链路HTTPS

常见误区:

  1. Base64 不是加密,任何人都能解码。
  2. MD5/SHA 不是加密,是哈希。
  3. 密码不应该用 AES 加密保存,因为 AES 可逆。
  4. JWT 普通 payload 只是 Base64Url 编码,不是加密。
  5. RSA 不适合加密大报文,通常用于签名或密钥协商。

HTTPS/TLS 为什么这样设计

如果所有数据都用 RSA 加密,性能差且有长度限制;如果只用 AES,又无法安全把 AES 密钥交给对方。TLS 使用混合思路:

mermaid
flowchart TD
    A["客户端发起握手"] --> B["服务端返回证书"]
    B --> C["客户端验证证书链"]
    C --> D["协商会话密钥"]
    D --> E["后续数据使用对称加密传输"]

为什么安全:

  1. 证书证明服务端身份,防止访问假站点。
  2. 非对称能力用于安全协商密钥。
  3. 对称加密用于大量业务数据传输,性能更好。
  4. 完整性校验防止密文被篡改。

证书过期、域名不匹配、信任链不完整都会导致 HTTPS 失败。

Demo 一:开放接口签名

请求头设计

text
X-App-Id: hospital-a
X-Timestamp: 1720000000000
X-Nonce: 6f0b9d6d8f5a4d5a
X-Body-Hash: base64(sha256(body))
X-Signature: base64(hmacSha256(canonicalString, secret))

签名原文

text
HTTP_METHOD
PATH
QUERY_STRING
TIMESTAMP
NONCE
BODY_HASH

示例:

text
POST
/api/open/collect/push
hospitalId=1001
1720000000000
6f0b9d6d8f5a4d5a
5xY...bodyHash

为什么要规范化签名原文?

  1. 客户端和服务端必须对同一串内容签名。
  2. 参数顺序、大小写、空格、编码不一致都会导致验签失败。
  3. 规范化可以减少“我签了但你验不过”的问题。

Java HMAC Demo

java
public final class SignUtils {
    public static String hmacSha256Base64(String text, String secret) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec key = new SecretKeySpec(
                    secret.getBytes(StandardCharsets.UTF_8),
                    "HmacSHA256"
            );
            mac.init(key);
            byte[] result = mac.doFinal(text.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(result);
        } catch (GeneralSecurityException ex) {
            throw new IllegalStateException("签名失败", ex);
        }
    }

    public static String sha256Base64(byte[] body) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            return Base64.getEncoder().encodeToString(digest.digest(body));
        } catch (NoSuchAlgorithmException ex) {
            throw new IllegalStateException("摘要失败", ex);
        }
    }
}

服务端验签流程

mermaid
flowchart TD
    A["收到请求"] --> B["检查 appId 是否存在"]
    B --> C["检查 timestamp 是否在窗口内"]
    C --> D["检查 nonce 是否已使用"]
    D --> E["计算 bodyHash"]
    E --> F["重建 canonicalString"]
    F --> G["用 secret 计算签名"]
    G --> H{"是否一致"}
    H -- "否" --> I["拒绝请求"]
    H -- "是" --> J["保存 nonce 并进入业务"]

nonce 要存 Redis:

java
String nonceKey = "openapi:nonce:" + appId + ":" + nonce;
Boolean first = redisTemplate.opsForValue()
        .setIfAbsent(nonceKey, "1", Duration.ofMinutes(5));
if (!Boolean.TRUE.equals(first)) {
    throw new SecurityException("重复请求");
}

如果没有 timestamp 和 nonce,攻击者抓到一次合法请求后可以反复重放。

Demo 二:敏感字段加密和脱敏

字段加密设计

sql
create table patient_info (
  id bigint primary key,
  patient_no varchar(64) not null,
  id_card_cipher text not null,
  id_card_hash varchar(64) not null,
  phone_cipher text not null,
  phone_hash varchar(64) not null,
  key_version int not null,
  created_at datetime not null,
  updated_at datetime not null,
  index idx_id_card_hash (id_card_hash),
  index idx_phone_hash (phone_hash)
);

为什么同时存 cipher 和 hash?

  1. cipher 用于需要授权查看明文时解密。
  2. hash 用于等值查询,例如按身份证查人。
  3. 加密后的密文通常不能直接做普通索引等值查询。
  4. hash 要加盐或使用安全方案,避免被彩虹表反推。

Java AES-GCM Demo

java
public class FieldCryptoService {
    private final KeyProvider keyProvider;

    public EncryptedValue encrypt(String plainText) {
        int version = keyProvider.currentVersion();
        SecretKey key = keyProvider.getAesKey(version);
        byte[] iv = SecureRandom.getInstanceStrong().generateSeed(12);

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));
        byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));

        return new EncryptedValue(
                version,
                Base64.getEncoder().encodeToString(iv),
                Base64.getEncoder().encodeToString(encrypted)
        );
    }
}

GCM 提供机密性和完整性校验。不要自己发明加密模式,不要固定 IV,不要把密钥写死在代码里。

脱敏展示

java
public String maskIdCard(String idCard) {
    if (idCard == null || idCard.length() < 8) {
        return "****";
    }
    return idCard.substring(0, 4) + "**********" + idCard.substring(idCard.length() - 4);
}

脱敏不是加密。脱敏是展示层减少泄露,加密是存储层保护数据。

密码为什么不能可逆加密

密码校验不需要知道原文,只需要判断用户输入是否匹配。因此应该使用慢哈希:

java
PasswordEncoder encoder = new BCryptPasswordEncoder();
String hash = encoder.encode(rawPassword);
boolean matches = encoder.matches(inputPassword, hash);

为什么不用 MD5:

  1. 太快,容易被暴力破解。
  2. 常见密码有彩虹表。
  3. 没有合适工作因子。

为什么不用 AES:

  1. AES 可逆,密钥泄露后所有密码明文暴露。
  2. 密码系统应该避免任何人能解密出原密码。

JWT 安全边界

JWT 结构:

text
header.payload.signature

payload 是 Base64Url 编码,不是加密。不要放:

  1. 身份证号。
  2. 手机号。
  3. 密码。
  4. 详细权限列表。
  5. 医疗敏感数据。

适合放:

  1. userId。
  2. tenantId。
  3. tokenVersion。
  4. issuer。
  5. expireAt。

JWT 泄露后要能撤销,可以用短 access token、refresh token、Redis 黑名单或 tokenVersion

密钥管理

mermaid
flowchart TD
    A["生成密钥"] --> B["存入 KMS 或密钥系统"]
    B --> C["应用按权限读取"]
    C --> D["使用 keyVersion 加密"]
    D --> E["定期轮换新版本"]
    E --> F["旧数据逐步重加密"]
    F --> G["禁用旧密钥"]

密钥原则:

  1. 不进 Git。
  2. 不打印日志。
  3. 不写死在镜像。
  4. 最小权限访问。
  5. 分环境隔离。
  6. 定期轮换。
  7. 泄露后能禁用和重加密。

生产排查流程

签名失败

mermaid
flowchart TD
    A["签名失败"] --> B["appId 是否正确"]
    B --> C["secret 是否同版本"]
    C --> D["HTTP method/path 是否一致"]
    D --> E["query 参数排序是否一致"]
    E --> F["bodyHash 是否一致"]
    F --> G["编码和换行是否一致"]

重放校验失败

mermaid
flowchart TD
    A["重放校验失败"] --> B["timestamp 是否超窗口"]
    B --> C["服务器时间是否同步"]
    C --> D["nonce 是否重复"]
    D --> E["Redis nonce key 是否冲突"]

HTTPS 失败

mermaid
flowchart TD
    A["HTTPS 失败"] --> B["证书是否过期"]
    B --> C["域名是否匹配"]
    C --> D["证书链是否完整"]
    D --> E["客户端是否信任 CA"]
    E --> F["TLS 版本和加密套件是否支持"]

面试标准回答

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

text
对称加密加密和解密使用同一把密钥,速度快,适合大量数据加密,例如 AES、SM4;非对称加密使用公钥和私钥,公钥加密私钥解密,或私钥签名公钥验签,速度较慢,适合身份认证、签名或密钥协商,例如 RSA、SM2。HTTPS 会用非对称能力协商会话密钥,再用对称加密传输数据。

接口签名怎么防篡改和防重放

text
开放接口通常用 appId 识别调用方,用 timestamp 限制请求时间窗口,用 nonce 保证同一请求不能重复使用,用 bodyHash 保证正文没被改,再把 method、path、query、timestamp、nonce、bodyHash 组成规范字符串,用 HMAC 或 RSA 签名。服务端重建同样字符串验签,验签通过后把 nonce 写入 Redis,防止抓包重放。

密码为什么不能加密保存

text
密码校验不需要还原明文,只需要判断输入是否匹配。可逆加密意味着密钥泄露后所有密码都会变成明文,所以密码应该用 BCrypt、Argon2、PBKDF2 这类带盐慢哈希保存,而不是 AES、RSA 或 MD5。

关联知识点