全链路加解密:在 HTTPS 之外再套一层

全链路加解密:在 HTTPS 之外再套一层

  1. 🧱 后端开发与架构
  2. 2026.05.05
  3. 26 min read

从零开始:怎么实现一套应用层加解密

在讲实际项目之前,先把问题简化:假设你有一个前后端分离的 Web 应用,已经部署了 HTTPS,但现在要求在应用层再做一层加密。你会怎么设计?

为什么是混合加密

先想一个问题:能不能全用对称加密(比如 AES)?

可以。前端和后端约定一个固定的 AES 密钥,请求和响应都用它加解密。但问题是——密钥写在前端代码里,浏览器 Sources 面板一搜就有。密钥泄露后,所有人都能解密所有流量。而且这个密钥没法换——一换,所有前端代码都要重新构建部署。

那全用非对称加密(比如 RSA、SM2)呢?

也可以。前端用公钥加密数据,后端用私钥解密。公钥泄露无所谓,没有私钥解不开。但问题是——非对称加密很慢。RSA-2048 加密 1KB 数据的时间能做几百次 AES 加密,而且非对称加密对数据长度有限制(RSA-2048 最多加密 245 字节)。你要加密一个 JSON 请求体,可能几 KB 甚至几十 KB,根本塞不进非对称加密的明文长度上限。

所以业界标准做法是混合加密——用非对称加密保护对称密钥,用对称密钥加密业务数据:

sequenceDiagram
    participant Browser as 浏览器
    participant Server as 服务端

    Note over Browser: 1. 生成随机对称密钥 K
    Browser->>Browser: K = generateRandomKey(128bit)

    Note over Browser: 2. 用公钥加密 K
    Browser->>Browser: EncryptedK = SM2_Encrypt(K, PublicKey)

    Note over Browser: 3. 用 K 加密请求数据
    Browser->>Browser: EncryptedBody = SM4_Encrypt(requestBody, K)

    Note over Browser: 4. 发送:加密的密钥放在请求头,加密的数据放在请求体
    Browser->>Server: POST /api/xxx<br/>Header: X-Encrypted-Key = EncryptedK<br/>Body: { "encryptedPayload": EncryptedBody }

    Note over Server: 5. 用私钥解密出 K
    Server->>Server: K = SM2_Decrypt(EncryptedK, PrivateKey)

    Note over Server: 6. 用 K 解密请求体
    Server->>Server: plainBody = SM4_Decrypt(EncryptedBody, K)

    Note over Server: 7. 处理业务,用同一个 K 加密响应
    Server->>Server: EncryptedResponse = SM4_Encrypt(responseBody, K)

    Server->>Browser: Response: EncryptedResponse

    Note over Browser: 8. 用 K 解密响应
    Browser->>Browser: plainResponse = SM4_Decrypt(EncryptedResponse, K)

这个流程的精妙之处在于:对称密钥 K 是前端每次会话随机生成的,不写死在代码里,不持久化到 localStorage。公钥写在前端没关系——公钥本来就是公开的,没有私钥解不开 EncryptedK。而私钥只存在于后端配置中,前端永远拿不到。

最简实现

按照这个思路,一个最简的前端实现大概长这样:

// 1. 生成 128 位随机对称密钥
function generateKey() {
  const bytes = new Uint8Array(16);
  crypto.getRandomValues(bytes);
  return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
}

// 2. 请求加密
function encryptRequest(config) {
  const sm4Key = sessionStorage.getItem('sm4key') || generateKey();
  sessionStorage.setItem('sm4key', sm4Key);

  // SM2 公钥加密对称密钥,放进请求头
  config.headers['X-Encrypted-Key'] = sm2Encrypt(sm4Key, PUBLIC_KEY);

  // SM4 加密请求体
  if (config.data) {
    config.data = { encryptedPayload: sm4Encrypt(JSON.stringify(config.data), sm4Key) };
  }
  return config;
}

// 3. 响应解密
function decryptResponse(response) {
  const sm4Key = sessionStorage.getItem('sm4key');
  if (response.data) {
    response.data = JSON.parse(sm4Decrypt(response.data, sm4Key));
  }
  return response;
}

后端对应的处理也不复杂——从 X-Encrypted-Key 头取出加密的对称密钥,SM2 解密得到 K,再用 K 去 SM4 解密 encryptedPayload 字段。在网关层做这件事,后续的 Controller 拿到的就是明文,完全无感知。

但这个”最简实现”有几个明显的问题。密钥存在 sessionStorage 里,关了浏览器就没了——这意味着每次打开页面都要重新生成。而对称密钥一变,后端不认识新密钥,就解不开请求。所以后端必须每次从 X-Encrypted-Key 头里重新解密出对称密钥,每次请求都要做一次 SM2 非对称解密——这就带来了性能问题。

要解决这个问题,就需要密钥缓存——后端解密一次后,把这个对称密钥存起来,下次同一个用户再来请求时直接用。这就引出了我们实际项目中的完整实现。

实际项目中的实现

技术选型:为什么是 SM2/SM4

这个项目是政企方向的,客户明确要求使用国密算法。所以技术选型上没有纠结——SM2 做非对称加密(等效 RSA-2048,256 位),SM4 做对称加密(等效 AES-128,128 位)。前端国密库用的是 sm-crypto,随机数生成用 crypto-js

另外还有两个小场景用了 AES:

  • 验证码坐标加密——因为验证码组件是第三方的(AJ-Captcha),内部约定就是 AES
  • 配置回退——当 algorithm-type 不是 SM4 时,降级到 AES-ECB

前端:密钥生成与请求拦截器

前端核心代码在 encrypt.js 中。完整流程分三部分:密钥生成、请求加密、响应解密。

密钥生成。 每次会话开始时,前端生成一个 128 位的随机十六进制字符串作为 SM4 对称密钥:

export const generateKey = (bitLength = 128) => {
  const randomWordArray = CryptoJS.lib.WordArray.random(bitLength / 8);
  const key = CryptoJS.enc.Hex.stringify(randomWordArray);
  useUserStore().pk = key;  // 存到 Pinia store,仅内存中,不持久化
  return key;
};

两个关键设计决策:

  • 存在 Pinia store 而非 localStorage——页面刷新后密钥消失,会话结束。即使攻击者通过 XSS 读到了当前内存中的密钥,也只能解密当前会话的数据
  • 用 CryptoJS 的 WordArray.random 而非 Math.random——前者使用密码学安全的随机数生成器,后者是伪随机,可以被预测

请求加密。 在 Axios 请求拦截器中,每个请求被发出前都要经过 requestEncrypt 处理:

export const requestEncrypt = (req) => {
  if (req.skipEncrypt) return req;  // 文件上传/下载等场景跳过

  const pk = useUserStore().pk || generateKey();

  // SM2 公钥加密对称密钥,04 前缀是国密标准非压缩格式标识
  req.headers["X-Encrypted-Key"] = `04${smCrypto.sm2.doEncrypt(pk, import.meta.env.VITE_ENCRYPT_PUBLIC_KEY)}`;

  // GET 请求:URL 参数序列化后加密,放入 encryptedPayload
  if (req.params) {
    const str = Object.keys(req.params)
      .map((key) => `${key}=${window.encodeURIComponent(...)}`);
    req.params = { encryptedPayload: encryptSm4(str.join("&")) };
  }

  // POST 请求:body 直接加密,放入 encryptedPayload
  if (req.data) {
    req.data = { encryptedPayload: encryptSm4(req.data) };
  }
  return req;
};

这里有几个值得注意的细节:

  • 04 前缀——这是国密 SM2 非压缩公钥格式的标准前缀,表示椭圆曲线上的点使用非压缩表示(04 || x || y)。加密结果以 04 开头,后端解析时需要知道这个格式
  • VITE_ENCRYPT_PUBLIC_KEY 是环境变量——编译时会被 Vite 替换为实际的 SM2 公钥字符串。这意味着公钥写死在前端构建产物中,任何人都能在浏览器 Sources 面板搜到
  • encryptedPayload 是统一的密文容器——不管是 URL 参数还是 JSON body,加密后都塞进这个字段。后端看到 encryptedPayload 就知道这是加密数据

响应解密。 在 Axios 响应拦截器中:

export const responseDecrypt = (response) => {
  if (response.config.skipEncrypt) return response;
  if (response.status === 200) {
    response.data = response.data ? JSON.parse(decryptSm4(response.data)) : response.data;
  } else {
    response.response.data = response.response.data
      ? JSON.parse(decryptSm4(response.response.data))
      : response.response.data;
  }
  return response;
};

处理了两种情况:正常响应(200)的 data 在 response.data,错误响应的 data 在 response.response.data(Axios 错误拦截器的结构不同)。

开关控制。 加解密不是一直开的——通过环境变量 VITE_ENCRYPT_ENABLED 控制,请求配置中的 skipEncrypt: true 也能跳过单个请求:

const isEncrypt = import.meta.env.VITE_ENCRYPT_ENABLED === "true";

request.interceptors.request.use((config) => {
  // ... 添加 Token、Tenant-Id 等
  return isEncrypt ? requestEncrypt(config) : config;
});

request.interceptors.response.use(
  (response) => isEncrypt ? responseDecrypt(response) : response,
  (err) => { err = isEncrypt ? responseDecrypt(err) : err; /* ... */ }
);

后端:网关 Filter 链

后端加解密在网关层完成,以 Spring Cloud Gateway 的 GlobalFilter 形式实现。整个处理链如下:

graph TB
    subgraph Gateway["Spring Cloud Gateway"]
        direction TB

        Client["客户端请求"] --> DecryptFilter["RequestDecryptFilter<br/>Order: HIGHEST_PRECEDENCE"]

        DecryptFilter --> CheckWhitelist{"路径在白名单?"}
        CheckWhitelist -->|是| SkipDecrypt["跳过加解密"]
        CheckWhitelist -->|否| ExtractKey["从 X-Encrypted-Key 头<br/>提取加密的对称密钥"]

        ExtractKey --> ResolveKey["EncryptServiceImpl<br/>SM2 解密得到 SM4 密钥"]
        ResolveKey --> DecryptParams["解密 URL 参数<br/>encryptedPayload → 明文参数"]
        DecryptParams --> DecryptBody["解密请求体<br/>encryptedPayload → 明文 JSON"]

        DecryptBody --> Route["路由到下游服务"]
        SkipDecrypt --> Route

        Route --> Controller["业务 Controller<br/>(拿到的是明文)"]
        Controller --> EncryptFilter["ResponseEncryptFilter<br/>Order: WRITE_RESPONSE - 1"]

        EncryptFilter --> CheckWhitelist2{"路径在白名单?"}
        CheckWhitelist2 -->|是| SkipEncrypt["原样返回"]
        CheckWhitelist2 -->|否| EncryptResp["SM4 加密响应体"]

        EncryptResp --> Return["返回密文给客户端"]
        SkipEncrypt --> Return
    end

    style DecryptFilter fill:#4A90D9,color:#fff
    style EncryptFilter fill:#4A90D9,color:#fff
    style ResolveKey fill:#E8A838,color:#fff
    style Controller fill:#5CB85C,color:#fff

请求解密(RequestDecryptFilter)。 优先级设为 Ordered.HIGHEST_PRECEDENCE,确保在其他 Filter 之前执行——因为后续 Filter 和 Controller 都依赖解密后的明文。

核心流程:

  1. 检查请求路径是否在白名单中(白名单内的路径直接放行)
  2. 检查 Content-Type 是否为 application/jsonapplication/x-www-form-urlencoded(不是这两者的跳过,比如文件上传的 multipart)
  3. X-Encrypted-Key 请求头取出加密后的对称密钥,SM2 解密得到真正的 SM4 密钥
  4. encryptedPayload 参数/字段取出密文,SM4 解密
  5. DecryptedExchangeDecorator 包装请求,后续链路读取到的就是明文

响应加密(ResponseEncryptFilter)。 优先级设为 NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1,在响应写出之前最后一环拦截:

  1. 同样检查白名单
  2. EncryptedResponseDecorator 包装响应,捕获原始响应字节
  3. 根据响应 Content-Type 判断是否加密
  4. 用请求阶段解出的 SM4 密钥加密响应体
  5. 将密文写回客户端

EncryptServiceImpl——加解密的核心。 这是密钥解析和加解密操作的中心:

// 对称加密 —— 根据配置选择 SM4 或 AES
public String encrypt(String rawText, String symmetricKey) {
    if (AlgorithmType.SM4.getCode().equals(algorithmType)) {
        return SMUtil.SM4Encrypt(rawText, symmetricKey, codeType);
    }
    return AesUtil.encryptECB(rawText, symmetricKey, codeType);
}

// 对称解密
public String decrypt(String cipherText, String symmetricKey) {
    if (AlgorithmType.SM4.getCode().equals(algorithmType)) {
        return SMUtil.SM4Decrypt(cipherText, symmetricKey);
    }
    return AesUtil.decryptECB(cipherText, symmetricKey);
}

// 从请求中解析对称密钥 —— 核心逻辑
public String resolveSymmetricKey(ServerWebExchange exchange) {
    // 1. 读取 X-Encrypted-Key 请求头
    String encryptedKeyHeader = exchange.getRequest().getHeaders()
        .getFirst("X-Encrypted-Key");

    // 2. 尝试从缓存中获取(用 token 做 key)
    String token = tokenExtractor.extractToken(exchange);
    String cachedKey = loadCachedKey(token, encryptedKeyHeader);
    if (cachedKey != null) return cachedKey;

    // 3. 缓存未命中,SM2 解密 X-Encrypted-Key
    String symmetricKey = SMUtil.SM2Decrypt(encryptedKeyHeader, privateKey);

    // 4. 写入缓存(1 小时)
    CacheUtil.setObject("encrypt:header:" + token, encryptedKeyHeader, 3600000);
    CacheUtil.setObject("encrypt:key:" + token, symmetricKey, 3600000);
    return symmetricKey;
}

// SM2 解密 —— 最终调用
private String decryptSymmetricKey(String encryptedKeyHeader) {
    return SMUtil.SM2Decrypt(encryptedKeyHeader, keyProperties.getPrivateKey());
}

配置结构

encrypt:
  request:
    enable: true              # 总开关
    algorithm-type: SM4       # SM4 或 AES
    encrypt-code: base64      # 密文编码:hex 或 base64
    fallback-key: ""          # 回退密钥(无 X-Encrypted-Key 时使用)
    whites:                   # 白名单路径,跳过加解密
      - /auth/oauth2/token
      - /api/captcha
      - /api/attachment/**

  sm2:
    public-key: ""            # SM2 公钥(前端持有,用于加密)
    private-key: ""           # SM2 私钥(仅后端持有,用于解密)

algorithm-type 支持 SM4 和 AES 切换——SM4 是国密标准,AES 是国际标准。encrypt-code 控制密文的编码格式(hex 或 base64)。白名单里的路径整个加解密流程都不走——文件上传下载、图片查看、验证码获取等接口必须加进去,否则二进制数据会被当作文本加密,直接损坏。

登录密码的特殊处理

登录接口的加密方式和普通接口不同。普通接口的加密密钥是前端生成的,但登录时前端还没有 token,也没有和”后端建立会话”。所以登录密码使用 RSA 非对称加密,由独立的 PasswordDecryptFilter 处理,优先级高于全链路加解密 Filter。

流程是:

  1. 前端使用 RSA 公钥加密密码
  2. 请求到达网关,PasswordDecryptFilter 拦截(路径匹配 OAuth2 token 地址)
  3. 用 RSA 私钥解密 body
  4. 解密成功则将明文密码交给后续的认证流程
sequenceDiagram
    participant Browser as 浏览器
    participant Gateway as 网关
    participant Auth as 认证服务

    Note over Browser: 输入用户名密码
    Browser->>Browser: RSA 公钥加密密码
    Browser->>Gateway: POST /auth/oauth2/token<br/>Authorization: Basic xxx<br/>Body: RSA_Encrypted(password)

    Gateway->>Gateway: PasswordDecryptFilter 拦截
    Gateway->>Gateway: RSA 私钥解密 → 明文密码
    Gateway->>Auth: 转发认证请求(明文)
    Auth->>Gateway: access_token + refresh_token
    Gateway->>Browser: Token 响应

    Note over Browser: 存储 token,后续请求走全链路加密

特殊请求的跳过策略

不是所有请求都需要加密。以下场景通过 skipEncrypt: true 或白名单跳过:

场景原因
文件上传 (multipart/form-data)二进制数据加密会损坏文件
文件下载 (responseType: blob)响应是二进制流,不是文本
图片/附件查看同上
验证码获取验证码组件内部有独立的 AES 加密
登录接口有独立的 RSA 加密处理

密钥缓存:设计对了,实现错了

设计意图

按设计,同一个用户会话中前端不会换对称密钥(存在 Pinia store 中,页面刷新前一直有效)。所以后端的理想行为是:每个会话只做一次 SM2 解密,后续请求直接从缓存中拿对称密钥。

缓存设计是这样的:

sequenceDiagram
    participant F as 前端
    participant G as 网关
    participant R as Redis

    Note over F: 首次请求:生成 SM4 密钥 K1
    F->>G: X-Encrypted-Key: SM2_Encrypt(K1)<br/>Authorization: Bearer <token>

    G->>G: 提取 token = "xxx"
    G->>R: GET encrypt:key:xxx
    R->>G: null(缓存未命中)

    G->>G: SM2_Decrypt(X-Encrypted-Key) → K1
    G->>R: SET encrypt:key:xxx = K1 (1h)
    G->>G: 用 K1 解密请求 ✓

    Note over F: 第二次请求:密钥仍是 K1
    F->>G: X-Encrypted-Key: SM2_Encrypt(K1)(相同)<br/>Authorization: Bearer <token>

    G->>G: 提取 token = "xxx"
    G->>R: GET encrypt:key:xxx, GET encrypt:header:xxx
    R->>G: K1 + 上次的 X-Encrypted-Key

    G->>G: header 相同 → 缓存命中!直接用 K1 ✓

设计逻辑是合理的——用 token 作为缓存的维度,同用户同会话的 X-Encrypted-Key 不变,缓存就能命中。

实际 bug

但实现上有一个隐蔽的问题。看 TokenExtractor 的代码:

// TokenExtractor —— 从 Authorization 头提取 token
public String extractToken(ServerWebExchange exchange) {
    String authorization = httpHeaders.getFirst(HttpHeaders.AUTHORIZATION);
    // ...
    // 必须以 Bearer 开头
    if (!StringUtils.startsWithIgnoreCase(authorization, "Bearer")) {
        return serverHttpRequest.getQueryParams().getFirst("access_token");
    }
    // 正则匹配提取 token
    Matcher matcher = AUTHORIZATION_PATTERN.matcher(authorization);
    // ...
    return matcher.group("token");
}

关键在那一行 startsWithIgnoreCase(authorization, "Bearer")——它只认 Bearer 前缀

而登录接口的 Authorization 头是这样的:

// 登录请求
headers: { Authorization: "Basic base64EncodedClientCredentials==" }

登录成功后,后续请求确实会用 Bearer <access_token>,TokenExtractor 也就能正常提取 token。但问题出在登录请求本身也启用了加密的情况下——前端为登录请求生成了 SM4 密钥、SM2 加密后放入 X-Encrypted-Key,但后端从 Basic 认证头中提取不到 token,loadCachedKey(null, encryptedKeyHeader) 永远返回 null。

结果就是:

  • 缓存永远不命中(token 为 null 时直接跳过缓存查询)
  • 每个请求都做一次 SM2 非对称解密(CPU 开销比缓存命中高几个数量级)
  • 每次都写缓存,key 里带 null——Redis 里攒了一堆 encrypt:key:null 的脏数据

这个 bug 的修复也不复杂——要么让 TokenExtractor 兼容 Basic 和 Bearer 两种格式,要么根据实际情况用其他维度(如 sessionId、客户端 IP + UserAgent 哈希)做缓存 key。

为什么有了 HTTPS 还要加一层

上面花了大量篇幅讲”怎么实现”。但回到一个更根本的问题:HTTPS 已经提供了传输层加密,为什么还要在应用层再做一套?

这个问题要从三个维度来看。

一、技术维度:TLS 卸载带来的内网明文问题

很多企业网络架构中有这样一个环节——SSL 卸载(TLS Termination)

graph LR
    Browser["浏览器"] -->|"HTTPS (TLS 加密)"| LB["负载均衡 / 网关<br/>(SSL 卸载)"]
    LB -->|"HTTP (明文)"| Service1["内网服务 A"]
    LB -->|"HTTP (明文)"| Service2["内网服务 B"]
    LB -->|"HTTP (明文)"| Service3["内网服务 C"]

    style LB fill:#E8A838,color:#fff
    style Browser fill:#4A90D9,color:#fff

外部流量到网关这一跳是 HTTPS 加密的,但网关到内部服务是明文的 HTTP。这么做的原因很现实——内网服务数量多,如果每个服务都配 TLS 证书,证书管理和性能开销都不小。而内网被认为是”安全的”,所以网关解密后直接明文转发。

但”内网安全”这个假设在攻防演练中经常被打脸。内网的抓包工具、日志采集 agent、网络监控设备,理论上都能看到明文数据。如果一个内网机器被拿下,攻击者就可以在内网监听所有明文流量。

在应用层再做一层加密,即使内网流量被监听,攻击者拿到的也是 encryptedPayload 密文。

二、合规维度:审计要的不是”安全”,是”加密”这个动作

这是最现实的原因。政企项目的安全审计中,“数据传输加密”往往是一个硬性条款。审计人员不一定理解 TLS 的工作原理,但他们认”你们做了加密”这个事实。

说白了,安全审计要的不是技术上的”安全”,而是合规上的”已加密”这个状态。 你可以跟他们解释 HTTPS 已经加了密,但他们需要看到的是——应用系统层面有加密机制、有密钥管理、有算法选型。TLS 是基础设施层面的,不在应用系统审计的范围内。

这就跟”为什么有了防火墙还要在代码里做输入校验”一个道理——分层防护,各管各的。

三、安全边界:这层加密到底能防什么

诚实地说,这套方案的防护边界很窄。

它能防的:

  • 内网被动窃听。 TLS 卸载后的内网明文传输、日志系统意外采集的请求体、网络监控工具的流量抓包——这些场景下,攻击者拿到的是密文
  • 合规审计。 如前所述,这是它存在的主要意义
  • 日志脱敏的被动保护。 网关 access log 如果不小心记录了请求体,记录的是 encryptedPayload 密文而不是明文手机号/身份证号,算是多一层兜底

它不能防的:

  • 前端伪造。 SM2 公钥写在前端代码里,加密逻辑完全公开。攻击者可以用相同的逻辑构造任意请求——公钥加密一个新的 SM4 密钥,SM4 加密伪造的请求体,后端照单全收
  • 中间人劫持。 如果 TLS 被攻破,攻击者可以篡改前端 JS、替换公钥、注入恶意代码。应用层加密依赖前端的完整性,前端的完整性又依赖 TLS——这是一个循环依赖。TLS 一旦破了,整条链都断了
  • 重放攻击。 加密后的请求可以被原样重放。没有时间戳校验、没有 nonce、没有签名,同一个加密请求可以被无限重放

总结下来:这套方案防护的是”被动窃听”,对”主动攻击”基本无效。 它的安全模型建立在 TLS 正常工作、只是在内网段多一层防护的前提上。

改进方向

如果要让这套方案在合规之外提供真正的安全增量,有几个方向值得考虑:

1. 签名优于加密。 在加密的基础上加一层签名机制——前端用 SM2 私钥(或 HMAC)对 timestamp + nonce + body_hash 做签名,后端验证。这能防御伪造和重放,因为攻击者没有签名密钥,无法生成有效签名。

2. 密钥通过 CI/CD 注入而非写死。 当前 SM2 公钥在前端环境变量中,编译后就是字符串常量。如果改成构建时通过 CI/CD 注入,每天定时构建一次自动轮换密钥,前端代码中的公钥只有 24 小时有效窗口——攻击者即使找到公钥,第二天也失效了。

3. 蜜罐检测。 在前端添加一两个”无关紧要”的请求头,正常用户永远不会携带。如果后端监控到有请求携带了这些蜜罐请求头的异常值,说明有人在用脚本分析协议、构造请求——触发告警,拉黑 IP。

4. 修缓存。TokenExtractor 的提取逻辑修好,让 SM2 解密只在会话初次请求时执行一次。

5. 区分场景。 核心敏感接口(登录、支付、个人信息查询)走完整加解密+签名,普通列表查询可以只用签名不做加密,文件上传下载跳过所有处理。安全是有成本的,把成本花在刀刃上。

写在最后

应用层全链路加密是一个典型的”技术 vs 合规”交叉案例。从纯技术角度看,HTTPS + 请求签名可以解决绝大部分安全问题;从合规审计角度看,“你们做了加密”本身就是一个交付物。

两者并不矛盾。承认合规的现实必要性,然后在合规的框架内把方案做得更务实——签名校验防伪造、密钥轮换降风险、蜜罐监控做预警——这些增量改进不会显著增加复杂度,但能让安全水位实打实地提升。

安全不是一劳永逸的事。明天换个密钥,比相信”没人能找到我的公钥”靠谱得多。

架构设计 安全 国密 SM2 SM4