☰
Spring Boot国密改造实战:TLCP传输加密与SM4密码加密完整指南
2026/10/19 10:14:30 网站建设 项目流程

前阵子接手了一个国密改造项目,后端主体是 Spring Boot 的微服务集群,前端是 H5 + Web 端。外部明确要求两点:一是对外访问链路必须支持国密标准的传输加密(TLCP),不能只靠普通 HTTPS 走默认 RSA 体系;二是像用户密码这样的敏感字段,在应用层要再用 SM4 做一次加密处理。最初我按惯性思维,以为给 Spring Boot 的内嵌 Tomcat 配上国密证书就能搞定,结果被现实教育了一整周。这篇文章把我最终采用的方案、关键技术原理、可以直接参考的代码,以及前后端联调时踩过的各种坑都整理出来。适合正在做国密改造的团队,也适合想了解 TLCP 和 SM4 到底怎么落地的人。

1. 方案选型:为什么不让 Spring Boot 直接面对 TLCP

1.1 国密生态的现状:硬解 TLCP 的成本有多高

先说结论:不要试图让 Spring Boot 直接完成 TLCP 握手。

Spring Boot 内嵌 Tomcat 的 HTTPS 走的是 JSSE 体系,而 JDK 自带的安全提供者根本没有注册 SM2、SM4 对应的密码套件。想在 Tomcat 里直接跑 TLCP,至少要做一套自定义的 SSLContext、KeyManager、TrustManager,再引入一个能把这些算法串起来的国密 Provider。这个工程量不只是写几千行代码的问题,后续证书更新、性能调优、客户端兼容性排查,每一项都是持续维护成本。

外部环境更不配合。主流浏览器默认不支持国密套件,开发和调试时必须用支持国密的浏览器或带国密组件的中间件。如果一开始就把 TLCP 的协议终结做在业务应用里,等于每台应用服务器都要面对这个兼容性难题,还要在每个微服务里重复处理。这不是一个可持续的方案。

我的做法是反向操作:把 TLCP 的协议终结放到最前面的网关层,由一台或多台支持国密的 Nginx / 负载均衡器来完成。应用服务器完全不感知国密协议,内部只走 HTTP。国密证书、私钥、套件配置全部收口在网关,后续证书轮换也只是改网关,不动应用。

这里有个很直观的类比:小区门口有保安查证件,楼栋住户不需要自己再配一个保安。只要大门把好人放进来了,楼内流转就按楼栋规则来。网关就是那个保安,TLCP 就是查证件的动作。

1.2 前置网关终结 TLCP 的拓扑与配置

实测跑通的拓扑是:

浏览器/客户端 | | TLCP(国密套件) v 国密 Nginx / 负载均衡器(TLCP 终结点) | | 内网 HTTP(明文) v Spring Boot 应用服务

国密 Nginx 需要基于支持国密套件的 OpenSSL 分支构建,这类开源分支很多,比如基于 OpenSSL 的国密增强版本 BabaSSL 就是这样一种选择。配置层面最核心的是双证书体系:

server { listen 443 ssl; server_name api.example.com; ssl_protocols TLSv1.2; # 国密套件,部分实现也叫 ECC-SM4-CBC-SM3 或 TLS_SM4_GCM_SM3 ssl_ciphers ECC-SM4-CBC-SM3:ECC-SM4-GCM-SM3; # 签名证书 ssl_certificate /etc/gm/sign_cert.pem; ssl_certificate_key /etc/gm/sign_key.pem; # 加密证书(国密双证书的第二张) ssl_enc_certificate /etc/gm/enc_cert.pem; ssl_enc_certificate_key /etc/gm/enc_key.pem; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }

注意,具体指令名在不同国密分支里可能略有差异,有的叫ssl_enc_certificate,有的叫sm2_cert_file,配置前先看对应版的文档。四层负载均衡也可以做同样的事:在最外层用支持国密的四层 LB 终结 TLCP,再把流量转发给后端的七层网关或直接转发到应用服务器。四层 LB 只看端口不分发业务,性能更好;七层 Nginx 可以做更细的路由和 Header 透传。按基础设施选型来即可。

实操中我还建议在测试环境额外开一个普通 TLS 端口给开发者调试。因为 Chrome 和 Edge 默认不支持国密,如果唯一入口是国密端口,开发时浏览器根本打不开页面,前后端联调会很痛苦。生产环境可以只暴露国密端口,测试环境保留普通 TLS 端口,两者互不干扰。

1.3 分层防御:TLCP、SM4、哈希各管一段

这套方案最终形成了三条安全线,我用一张表把它理清:

安全层所处环节保护对象主要手段
传输层浏览器到网关整个 HTTP 报文TLCP(国密套件全链路加密)
应用层前端到后端密码等敏感字段SM4 会话密钥加密
存储层数据库用户口令BCrypt 不可逆哈希

很多人有个误区:觉得既然外层已经有 TLCP 了,密码字段就不需要再加密。但 TLCP 只保护到网关这一跳,网关到 Spring Boot 之间是内网 HTTP 明文。内网虽然相对安全,但纵深防御不能省。尤其是密码字段,不管在哪一段链路里,都不应该以明文出现。

所以前端在 TLCP 之上,把密码用 SM4 加密后再发;后端拿到密文后先解密,紧接着做不可逆哈希,明文不落库、不落日志。标题里的“传输加密(TLCP)”解决的是链路问题,“密码加密(SM4)”解决的是字段问题,两者不是二选一,而是要叠加使用。

2. 核心原理拆解:TLCP 与 SM4 到底是怎么工作的

2.1 国密算法三兄弟:SM2、SM3、SM4 分别负责什么

国密并不是单一算法,而是一整套公开的密码算法体系,最常用的是这三个:

  • SM2:非对称密码算法,本质上是基于椭圆曲线(曲线名 sm2p256v1)的公钥算法,类似 RSA / ECDSA 的结合体。它既能做签名验签,也能做密钥交换和加密。本文里的数字信封就是用它加密 SM4 会话密钥。
  • SM3:密码杂凑算法,输出 256 位摘要,对标 SHA-256。主要用于完整性校验、消息认证和密钥派生。
  • SM4:对称分组密码,分组长度 128 位,密钥长度 128 位,对标 AES-128。适用于大批量数据的加解密,也就是本文密码字段加密的主力。

TLCP 全称叫《传输层密码协议》,标准号是 GB/T 38636。它本质上是在 TLS 框架内只使用国密算法套件的传输加密协议。整体握手流程和 TLS 1.2 很像:ClientHello、ServerHello、证书交换、密钥协商、Finished。区别在于它只支持国密套件,并且引入了“双证书”体系——一张签名证书负责对握手参数签名,一张加密证书负责密钥协商。常见的套件名有ECC-SM4-CBC-SM3、ECC-SM4-GCM-SM3等。

这里有个细节值得留意:并不是两张证书都能做密钥协商。只有加密证书对应的 SM2 私钥能做密钥交换,签名证书只负责证明“我是我”。这也是国密体系看起来比普通 TLS 多一层的原因。

2.2 SM4 加解密模型:分组、模式、填充是必修课

SM4 是分组密码,这意味着它每次只能处理 16 字节的数据块。如果要加密的内容超过 16 字节,就得选一种“工作模式”来把多个分组串起来。如果内容不是 16 字节的整数倍,还得做“填充”。这两个概念不搞清楚,跨语言联调时一定会出事。

工作模式常用两种:

  • ECB:最简单的模式,每个 16 字节分组独立加密。缺点很明显——相同的明文分组会得到相同的密文分组,容易泄露数据模式,实战中不建议单独使用。
  • CBC:引入一个 16 字节的初始化向量(IV),每个明文分组先和前一组的密文异或,再执行 SM4 加密。相同的明文块在不同位置、不同 IV 下,密文完全不同,比 ECB 安全很多。
  • GCM:在 CBC 基础上增加了认证标签,能检测密文是否被篡改,适合对完整性要求更高的场景。

填充方式实战中最常用 PKCS7:缺几个字节就补几个值为该字节数的数据。Java 的 JCE 接口里习惯写PKCS5Padding,但对 SM4/AES 这种 16 字节分组的算法来说,实际效果和 PKCS7 一样。

用生活类比来理解:SM4 相当于一个固定字数的换字表,每 16 个字符换一次。CBC 模式就像写长篇文档时,每句话都要参考上一句话的结尾字符,这样即使两段内容一样,由于上下文不同,最终写出来的密文也不一样。IV 就是这个过程的随机起点。前端和后端只要有一个节点的工作模式、IV、填充不一致,解密出来的就是乱码。

2.3 传输加密不等于密码安全:为什么还得单独处理密码

TLCP 解决的是“链路被偷听”的问题,但它完全不管“数据库被拖库”之后会怎样。如果密码只是明文经过 TLCP 传输,后端收到后直接存明文,一旦数据库泄露,全量密码就暴露了。

正确的思路是:密码到了后端,必须立刻转成不可逆哈希。哈希不是加密,它是单向的,比如 BCrypt 这种自带盐值和迭代成本的算法,是目前行业里口令存储的主流选择。传输过程中,密码也不能以明文形式出现在应用日志、网关日志、抓包工具里,所以最好从源头——也就是前端——就开始加密。

如果合规要求存储也必须用国密算法可逆加密(比如某些遗留系统的审计和回查需要),退而求其次可以用 SM4 做存储加密,但这时候密钥必须独立管理、定期轮换,并且要清醒地认识到:可逆存储的口令一旦被拖库,理论上存在被批量解密的巨大风险。我的建议是传输用 SM4,存储用 BCrypt(或带盐的 SM3 多轮迭代),各司其职。

3. Spring Boot 接入实战:SM4 密码加解密全流程

3.1 引入 BouncyCastle 依赖并注册 Provider

JDK 原生不识别 SM4,所以第一步是引入 BouncyCastle 作为安全提供者。Maven 里加入这两个依赖:

<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15to18</artifactId> <version>1.70</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk15to18</artifactId> <version>1.70</version> </dependency>

然后注册 Provider:

import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.Security; @Configuration public class GmConfig { static { if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { Security.addProvider(new BouncyCastleProvider()); } } }

不注册这一步,后面的Cipher.getInstance("SM4/CBC/PKCS7Padding", "BC")会直接抛NoSuchProviderException。这个问题我见过太多次了,很多人第一反应是换依赖,其实是 Provider 没注册上。

3.2 SM4 工具类:密钥和 IV 用 Hex 表示更稳妥

SM4 的密钥和 IV 都必须是 16 字节。跨语言对接时,直接用 UTF-8 字符串当 key 很容易踩坑——比如中文字符串的字节数不是 16,或者欧洲语言字符在不同编码下长度不同。

我强烈建议密钥和 IV 都用hex(十六进制)编码表示:16 字节对应 32 个 hex 字符。这样前后端都能精确定长,还没有编码问题。

package com.example.gm; import org.bouncycastle.jce.provider.BouncyCastleProvider; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.Security; import java.util.Base64; public class Sm4Util { static { if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { Security.addProvider(new BouncyCastleProvider()); } } /** * SM4/CBC/PKCS7Padding 加密 * * @param hexKey 16字节密钥,hex 编码,共32个字符 * @param hexIv 16字节 IV,hex 编码,共32个字符 * @param plainText 明文 * @return Base64 编码的密文 */ public static String encrypt(String hexKey, String hexIv, String plainText) throws Exception { SecretKeySpec keySpec = new SecretKeySpec(hexToBytes(hexKey), "SM4"); IvParameterSpec ivSpec = new IvParameterSpec(hexToBytes(hexIv)); Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS7Padding", BouncyCastleProvider.PROVIDER_NAME); cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(encrypted); } /** * SM4/CBC/PKCS7Padding 解密 * * @param hexKey 16字节密钥,hex 编码 * @param hexIv 16字节 IV,hex 编码 * @param base64CipherText Base64 编码的密文 * @return 明文字符串 */ public static String decrypt(String hexKey, String hexIv, String base64CipherText) throws Exception { SecretKeySpec keySpec = new SecretKeySpec(hexToBytes(hexKey), "SM4"); IvParameterSpec ivSpec = new IvParameterSpec(hexToBytes(hexIv)); Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS7Padding", BouncyCastleProvider.PROVIDER_NAME); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(base64CipherText)); return new String(decrypted, StandardCharsets.UTF_8); } private static byte[] hexToBytes(String hex) { int len = hex.length(); byte[] data = new byte[len / 2]; for (int i = 0; i < len; i += 2) { data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i + 1), 16)); } return data; } }

强调三个细节:

  1. 解密时用Base64.getDecoder()对应前端output: 'base64'的输出。
  2. 明文字符串统一 UTF-8,前端也要确保传入的密码字符串是 UTF-8。
  3. hex 字符串全部用小写,前端生产 hex 时用b.toString(16).padStart(2, '0')这种写法,避免丢前导零。

3.3 数字信封:用 SM2 保护每一次会话的 SM4 密钥

这里有个很关键的决策:SM4 会话密钥不能由服务端固定下发,更不能前后端写死同一个密钥。固定密钥一旦被逆向或日志泄露,整个加密体系就废了。

安全的做法是“数字信封”:

  1. 服务端启动时生成 SM2 密钥对,把公钥通过接口下发(公钥不需要保密,但真实性依赖 TLCP 链路保证)。
  2. 前端每次提交密码前,随机生成一个 16 字节的 SM4 会话密钥。
  3. 前端用服务端 SM2 公钥加密这个 SM4 密钥,得到envelopeKey。
  4. 前端用 SM4 会话密钥加密密码,得到encPassword。
  5. 后端先拿 SM2 私钥解envelopeKey得到 SM4 会话密钥,再用会话密钥解encPassword。

这样做的好处是:服务端不需要保存任何 SM4 密钥状态,每个请求都有独立的会话密钥,用完即弃。就算某次会话密钥泄露,也只影响那一条请求,不会波及其它数据。

后端 SM2 解密的核心代码长这样:

import org.bouncycastle.asn1.gm.GMNamedCurves; import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.crypto.engines.SM2Engine; import org.bouncycastle.crypto.params.ECDomainParameters; import org.bouncycastle.crypto.params.ECPrivateKeyParameters; import org.bouncycastle.util.encoders.Hex; import java.math.BigInteger; import java.nio.charset.StandardCharsets; public class Sm2Util { /** * 使用 SM2 私钥解密数字信封 * * @param privateKeyHex SM2 私钥 hex 字符串(无 0x 前缀) * @param envelopeKeyHex 前端传过来的 envelopeKey,hex 字符串 * @return 明文数据,这里就是 SM4 会话密钥的 hex 字符串 */ public static String decryptEnvelope(String privateKeyHex, String envelopeKeyHex) throws Exception { byte[] cipherBytes = Hex.decode(envelopeKeyHex); X9ECParameters x9 = GMNamedCurves.getByName("sm2p256v1"); ECDomainParameters domainParams = new ECDomainParameters( x9.getCurve(), x9.getG(), x9.getN(), x9.getH()); BigInteger d = new BigInteger(privateKeyHex, 16); ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(d, domainParams); SM2Engine engine = new SM2Engine(SM2Engine.Mode.C1C3C2); engine.init(false, privateKeyParameters); byte[] plainBytes = engine.processBlock(cipherBytes, 0, cipherBytes.length); return new String(plainBytes, StandardCharsets.UTF_8); } }

一个容易被忽略的坑:公钥下发时,如果直接拿PublicKey.getEncoded()转 hex,得到的是带 DER 头的 X.509 编码,前端sm2.doEncrypt根本不认。正确做法是取椭圆曲线点的未压缩编码:

org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey pubKey = ...; String publicKeyHex = org.bouncycastle.util.encoders.Hex.toHexString( pubKey.getQ().getEncoded(false));

这个publicKeyHex以04开头,长度是 130 个字符,前端直接可用。

3.4 登录与注册接口的密码处理完整流程

先约定请求体结构:

{ "account": "user01", "encPassword": "BASE64编码的SM4密文", "envelopeKey": "hex编码的SM2数字信封", "iv": "hex编码的16字节IV" }

后端登录接口的核心逻辑:

public LoginResult login(LoginRequest request) { // 1. 拆数字信封,拿到 SM4 会话密钥(hex 字符串) String sm4Key = sm2Util.decryptEnvelope(sm2PrivateKeyHex, request.getEnvelopeKey()); // 2. SM4 解密密码 String password = sm4Util.decrypt(sm4Key, request.getIv(), request.getEncPassword()); // 3. 业务校验 User user = userMapper.selectByAccount(request.getAccount()); if (user == null || !passwordEncoder.matches(password, user.getPasswordHash())) { throw new LoginException("账号或密码错误"); } // 4. 签发 token return loginSuccess(user); }

注册接口同理,只是把校验替换成写入:

String password = sm4Util.decrypt(sm4Key, request.getIv(), request.getEncPassword()); String hash = passwordEncoder.encode(password); userMapper.insert(request.getAccount(), hash);

这里passwordEncoder用 Spring Security 的BCryptPasswordEncoder实现。解密出来的明文密码只在方法栈里存活几毫秒,接着就被 BCrypt 哈希覆盖,不落日志、不落数据库。

如果业务接口的响应体也要加密,也可以复用同一个会话 SM4 密钥:返回的数据用encPassword同一把钥匙加密,前端收到后用同一个 SM4 密钥解密。登录成功后的用户信息、token 都属于敏感数据,值得加密返回。

3.5 前端 H5/Web 的配合方式(sm-crypto)

前端我用的sm-crypto这个库,SM2、SM3、SM4 都覆盖。核心代码:

import { sm2, sm4 } from 'sm-crypto' // 1. 获取服务端 SM2 公钥(带 04 前缀的 130 字符 hex) const { publicKey } = await fetch('/api/gm/public-key').then(r => r.json()) // 2. 随机生成 SM4 会话密钥(16 字节 -> 32 个 hex 字符) const sm4KeyArr = new Uint8Array(16) crypto.getRandomValues(sm4KeyArr) const sm4Key = Array.from(sm4KeyArr, b => b.toString(16).padStart(2, '0')).join('') // 3. 随机生成 IV(同样 16 字节) const ivArr = new Uint8Array(16) crypto.getRandomValues(ivArr) const iv = Array.from(ivArr, b => b.toString(16).padStart(2, '0')).join('') // 4. SM4 加密密码字段 const encPassword = sm4.encrypt(password, sm4Key, { mode: 'cbc', iv: iv, padding: 'pkcs7', output: 'base64' }) // 5. SM2 加密 SM4 会话密钥,数字信封 // 第二个参数 1 表示 C1C3C2,必须和后端 SM2Engine.Mode.C1C3C2 一致 const envelopeKey = sm2.doEncrypt(sm4Key, publicKey, 1) // 6. 提交登录 fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ account, encPassword, envelopeKey, iv }) })

前端最容易踩的三个点:一是忘记cipherMode参数导致 SM2 格式不一致;二是sm4.encrypt没传mode: 'cbc'导致默认 ECB,后端却按 CBC 解;三是随机数生成时没有补前导零,导致 hex 长度不足 32 位。最后这个坑很隐蔽,Array.from(arr, b => b.toString(16).padStart(2, '0'))一定要写全。

4. 常见问题与排查技巧实录

4.1 SM4 解密失败的排查顺序

SM4 解不出来的时候,不要盲目调参。按这张表逐项排查:

排查项具体位置备注
工作模式mode: 'cbc'vsSM4/CBC/...前端默认可能是 ECB
IV前端传的iv是否被后端正确解码用同一段 hex 对比字节序
填充pkcs7vsPKCS7Padding一般不会错,但不同库命名不同
密钥长度hex 解码后是否 16 字节少一位前导零就全乱
Base64密文是否被 URL 编码破坏+变空格是最常见的大坑

我的实操心法是:前后端各写一个自测用例,用同一组key、iv、明文做加密,对比输出密文是否一致。哪个不一致就查哪端,不要去线上环境抓瞎。以前踩过最离谱的一次,是后端代码里把 IV 写死了,前端明明每次都传新 IV,后端解密却一直用旧 IV,结果十几分钟都没找到原因。

4.2 SM2 跨端不兼容:C1C3C2 与 C1C2C3

国密标准的 SM2 加密结果排列是C1C3C2,但很多老库默认输出C1C2C3。前端sm-crypto用第二个参数控制:

// 1 表示 C1C3C2(国标推荐) const envelopeKey = sm2.doEncrypt(sm4Key, publicKey, 1) // 0 表示 C1C2C3(老式兼容)

后端 BC 里对应:

SM2Engine engine = new SM2Engine(SM2Engine.Mode.C1C3C2);

这个模式不统一时的表现是:后端能“解密”但结果是乱码,或者直接抛异常说数据长度不正确。联调前两边先确认好模式,避免浪费半天。

另外,不同编程语言的 SM2 实现在编码、字节序上的细节差异很多。我的建议是先用官方测试向量做一次离线验证,再接入真实请求。不要指望“反正都是标准算法”就一定兼容。

4.3 国密 TLS 握手失败的经典原因

国密握手失败在网关层的排查,常见就这几种:

  • 双证书只配了签名证书,没配加密证书:TLCP 要求签名证书和加密证书成对出现,少一张直接握手失败。
  • 证书链不完整:国密证书也要保证根证书、中间证书完整,缺中间证书时客户端会报证书链错误。
  • 套件名不对:Nginx 配置文件里的ssl_ciphers如果写错或顺序不对,客户端找不到可用套件。
  • 协议版本不匹配:部分国密客户端只支持特定 TLS 版本,ssl_protocols别随意开 TLS1.3 或只开 TLS1.2,要看客户端能力。

调试命令可以这样用(要求本机 openssl 也是支持国密套件的版本):

openssl s_client -connect api.example.com:443 -servername api.example.com \ -cipher ECC-SM4-CBC-SM3 -brief

如果你的系统自带 openssl 不识别国密套件,报 “no cipher match” 不代表服务器问题,换个支持国密的 openssl 版本再试。

4.4 网关回源后协议信息丢失

网关终结 TLCP 后,Spring Boot 收到的请求是普通 HTTP。如果应用里通过request.getScheme()或request.isSecure()判断请求是否加密,会得到错误结果。

解决方式是在 Nginx 里透传原始协议信息。前面的配置里已经加了proxy_set_header X-Forwarded-Proto $scheme;,如果还需要区分是不是国密链路,再自定义一个头:

proxy_set_header X-Gm-Channel tlcp;

Spring Boot 侧读取这个自定义头即可。注意:如果网关后面还有一层代理,代理没有继续透传这些头,应用还是会误判。排查时用curl -v看实际到达应用的请求头,不要猜。

4.5 Base64 和 URL 编码的隐形暗坑

Base64 编码里包含+、/、=这几个字符。如果前端把密文拼到 URL 参数里,+会被浏览器或服务端框架转成空格,导致 Base64 解码失败或解出错误的字节序列。

所以我强烈建议:所有密文都放在 JSON body 里传输,不要用 query 参数。如果历史接口必须走 query / form,至少在前端把密文做一次encodeURIComponent,后端再用URLDecoder.decode还原。另外,任何日志组件都别打整个请求体——里面带着encPassword和envelopeKey,一旦密文和密钥同时出现在日志里,整个加密方案就形同虚设。

4.6 密钥管理的最后一道红线

SM2 私钥和固定的 SM4 密钥必须放到配置中心、KMS 或环境变量里,绝不能硬编码在代码仓库。我见过有人把私钥直接写在application.yml里提交到 Git,这种事故属于安全事件级别,不是“粗心”两个字能解释的。

SM4 会话密钥不需要服务端保存,随请求来随请求走。但如果业务接口也要加密,且不想每个请求都重新走一次 SM2 解密,可以把会话密钥放到 Redis,设置很短的过期时间,比如 5 分钟。前端随请求带上一个会话ID,后端从 Redis 取对应的 SM4 密钥。

还有一个容易被忽略的设计:密钥轮换。SM2 密钥对和证书都是要定期更换的。切换时前端拿到的公钥会变,所以接口设计上最好预留一个公钥版本号字段,前端存储version和publicKey,加密时带上版本号,后端根据版本号选择对应的私钥。这个设计越早做越好,不然到了证书到期那天就只能临时停机换了。

这套方案做完,我最大的体会是:国密并没有想象中那么“玄”,它就是一整套标准化的算法和协议。真正的难点从来不在算法本身,而在生态兼容和端到端的一致性——浏览器、网关、前端库、后端依赖,每一个环节都要对齐协议细节。另外一个体会是:安全改造一定要分层思考,别以为套上一个 TLCP 就万事大吉,关键字段加密、存储哈希、密钥管理,每一层都不能省。如果你也在做类似改造,建议先把方案图画清楚,再动手写代码。尤其是前端和后端的模式、格式、编码,开工前就定好,能省掉至少一周的联调时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询