从X.509到国密SM2:证书解析、生成与应用实战指南

📅 发布时间:2026/7/9 20:40:43 👁️ 浏览次数:
从X.509到国密SM2:证书解析、生成与应用实战指南
1. 项目概述为什么我们需要深入理解证书与国密SM2最近在调试一个微服务间的HTTPS调用时又遇到了那个熟悉又恼人的错误SSL certificate hostname mismatch。这让我想起无论是拉取代码时遇到的self signed certificate还是调用API时抛出的各种SSL异常其根源往往都指向同一个东西——数字证书。对于开发者而言证书不再是运维的专属领域它已经深度嵌入到我们日常的开发、部署和联调中。而随着信息安全要求的提升国密算法SM2及其证书体系正从金融、政务等特定领域逐步走向更广泛的互联网应用。这个项目就是一次从最基础的.cer证书文件解析开始到最终在代码中应用国密SM2证书进行签名验签、加解密的完整实战记录。我将带你亲手拆解一个证书的二进制结构理解X.509标准的每一个字段然后跨入国密的世界用代码实现SM2证书的生成、解析与应用。无论你是在处理一个棘手的SSL连接错误还是需要为你的应用集成国密合规能力希望这篇详尽的流程能成为你手边可靠的参考。2. 证书基础与.cer文件深度解析在开始敲代码之前我们必须先搞清楚我们要处理的对象到底是什么。一个.cer或.crt文件通常包含了一个X.509格式的数字证书。你可以把它想象成一张数字世界的“身份证”它绑定了某个实体如服务器、个人或机构的身份信息和一把公钥并由一个可信的第三方CA签名担保。2.1 X.509证书的核心结构解剖一个标准的X.509证书并非一堆乱码它遵循着非常严谨的ASN.1抽象语法标记一结构并使用DER可辨别编码规则进行二进制编码。这就是为什么我们能用程序精准地解析出每一个字段。其主要部分包括TBSCertificate证书主体这是证书的核心内容。版本号标识证书格式的版本v1, v2, v3。序列号由CA分配的唯一标识在吊销查询时至关重要。签名算法CA用来对这张证书进行签名的算法如sha256WithRSAEncryption。颁发者签发此证书的CA的名称采用X.500可辨别名DN格式。有效期证书生效和失效的起止时间。主体证书持有者的名称同样是DN格式。主体公钥信息这是最关键的部分之一包含了公钥算法如RSA、ECC和公钥本身的比特串。扩展域v3证书引入包含密钥用法、增强密钥用法、主题备用名称等丰富信息。签名算法与TBSCertificate中的签名算法标识一致。签名值CA使用自己的私钥对TBSCertificate部分进行签名后产生的值。任何对证书内容的篡改都会导致签名验证失败。注意我们常说的“证书链”或“信任链”就是基于这种签名关系建立的。根CA证书自签名中间CA证书由根CA签名最终的用户证书由中间CA签名。验证时需要逐级向上验证签名直到一个受信任的根证书。2.2 手动与编程解析.cer证书实战理解了结构我们就可以动手解析了。这里我以Java生态最常用的BouncyCastle库为例因为它对国密支持也最好。首先准备环境。在你的Maven项目中添加依赖dependency groupIdorg.bouncycastle/groupId artifactIdbcpkix-jdk15on/artifactId version1.70/version !-- 请使用最新稳定版 -- /dependency然后让我们写一段代码来解析一个标准的RSA证书import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.style.BCStyle; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.util.encoders.Hex; import java.io.FileReader; import java.security.cert.X509Certificate; import java.security.cert.CertificateFactory; public class CerParserDemo { public static void main(String[] args) throws Exception { // 1. 加载证书文件这里以PEM格式为例DER格式原理相同 CertificateFactory cf CertificateFactory.getInstance(X.509); X509Certificate certificate (X509Certificate) cf.generateCertificate( new FileInputStream(server.cer) ); // 2. 转换为BouncyCastle对象以便使用其丰富API X509CertificateHolder certHolder new JcaX509CertificateHolder(certificate); // 3. 提取关键信息 System.out.println( 证书解析结果 ); System.out.println(序列号: certHolder.getSerialNumber()); System.out.println(版本: V certHolder.getVersion()); // 获取颁发者信息使用BCStyle可读性更好 X500Name issuer certHolder.getIssuer(); System.out.println(颁发者 CN: issuer.getRDNs(BCStyle.CN)[0].getFirst().getValue()); System.out.println(颁发者 O: issuer.getRDNs(BCStyle.O)[0].getFirst().getValue()); X500Name subject certHolder.getSubject(); System.out.println(主体 CN: subject.getRDNs(BCStyle.CN)[0].getFirst().getValue()); System.out.println(生效时间: certHolder.getNotBefore()); System.out.println(失效时间: certHolder.getNotAfter()); // 4. 解析公钥信息这是重点 System.out.println(\n 公钥信息 ); System.out.println(算法: certHolder.getSubjectPublicKeyInfo().getAlgorithm().getAlgorithm()); // 公钥的DER编码字节对于RSA/ECC可以进一步解析出模数/指数或曲线点 byte[] publicKeyDer certHolder.getSubjectPublicKeyInfo().getPublicKeyData().getBytes(); System.out.println(公钥DER编码(Hex): Hex.toHexString(publicKeyDer)); // 5. 检查扩展例如SAN常用于解决hostname mismatch System.out.println(\n 关键扩展 ); certHolder.getExtensions().getExtensionParsers().forEachRemaining(ext - { System.out.println(OID: ext.getExtension().getExtnId()); // 可以进一步解析具体的扩展值如SubjectAlternativeName }); } }实操心得格式判断证书文件可能是DER二进制.cer,.der或PEM文本.pem,.crt。PEM文件以-----BEGIN CERTIFICATE-----开头。上述代码使用标准CertificateFactory它能自动识别常见格式。如果遇到纯DER直接读取字节流解析即可。公钥提取代码中打印的公钥是经过ASN.1 DER编码的比特串。对于RSA公钥这个比特串本身又是一个ASN.1序列包含模数(n)和公开指数(e)。如果需要获取java.security.PublicKey对象进行加密操作直接使用certificate.getPublicKey()即可。扩展的重要性Subject Alternative Name (SAN)扩展是解决certificate for localhost doesnt match错误的关键。服务器证书的SAN字段必须包含客户端连接时使用的主机名或IP。开发环境常使用自签名证书如果只设置了Common Name (CN)为localhost但某些库严格检查SAN就会报错。生成证书时务必同时配置CN和SAN。3. 国密SM2算法与证书体系探秘解决了标准证书的解析我们进入国密领域。SM2是一套基于椭圆曲线密码学ECC的国产商用密码算法包含数字签名、密钥交换和公钥加密。与RSA相比在相同安全强度下SM2的密钥更短256位SM2约等于3072位RSA计算更快且由国家密码管理局认定符合国内信息安全合规要求。3.1 SM2与标准ECC/X.509的差异与兼容SM2虽然也基于椭圆曲线但它使用一条特定的曲线参数sm2p256v1并且签名算法流程与ECDSA有所不同。这带来了一个核心挑战如何将SM2密钥对装入标准的X.509证书结构中答案是可以但有特定规则。国密标准定义了使用SM2算法的X.509证书格式。在证书的Subject Public Key Info字段中算法标识符不再是id-ecPublicKey而是特定的SM2 OID1.2.156.10197.1.301对于签名或1.2.156.10197.1.301.1对于加密。公钥数据仍然是椭圆曲线点的DER编码通常是非压缩格式但曲线必须是SM2曲线。这意味着一个解析标准ECC证书的工具如果不认识SM2的OID可能无法正确识别或使用这个公钥。这就是为什么我们需要BouncyCastle这样的库它内置了对国密OID和算法的支持。3.2 生成SM2密钥对与自签名证书让我们从零开始创建一对SM2密钥并生成一张自签名证书。这将帮助我们彻底理解SM2证书的构成。import org.bouncycastle.asn1.gm.GMObjectIdentifiers; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x509.*; import org.bouncycastle.cert.X509v3CertificateBuilder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.pkcs.PKCS10CertificationRequest; import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder; import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder; import org.bouncycastle.util.encoders.Base64; import java.math.BigInteger; import java.security.*; import java.security.cert.X509Certificate; import java.util.Date; public class SM2CertGenerator { static { Security.addProvider(new BouncyCastleProvider()); // 注册BC为安全提供者 } public static void main(String[] args) throws Exception { // 1. 生成SM2密钥对 KeyPairGenerator kpg KeyPairGenerator.getInstance(EC, BC); kpg.initialize(256); // SM2使用256位素数域曲线BC会默认使用sm2p256v1参数 KeyPair keyPair kpg.generateKeyPair(); PublicKey publicKey keyPair.getPublic(); PrivateKey privateKey keyPair.getPrivate(); System.out.println(SM2公钥算法: publicKey.getAlgorithm()); System.out.println(SM2私钥算法: privateKey.getAlgorithm()); // 2. 创建证书请求CSR或直接构建证书 X500Name subject new X500Name(CNSM2 Test, OMy Company, CCN); BigInteger serial BigInteger.valueOf(System.currentTimeMillis()); // 用时间戳当序列号 Date notBefore new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000); // 昨天生效 Date notAfter new Date(System.currentTimeMillis() 365L * 24 * 60 * 60 * 1000); // 一年后失效 // 3. 构建证书生成器 X509v3CertificateBuilder certBuilder new JcaX509v3CertificateBuilder( subject, // 这里颁发者和主体相同因为是自签名 serial, notBefore, notAfter, subject, publicKey ); // 4. 添加扩展关键 // 基本约束CAfalse certBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false)); // 密钥用法数字签名非否认 certBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation)); // 增强密钥用法客户端认证、服务器认证 certBuilder.addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth)); // 主题备用名称解决主机名验证问题 GeneralNames san new GeneralNames(new GeneralName(GeneralName.dNSName, test.example.com)); certBuilder.addExtension(Extension.subjectAlternativeName, false, san); // 5. 使用SM3withSM2算法进行签名国密标准签名算法 JcaContentSignerBuilder signerBuilder new JcaContentSignerBuilder(SM3withSM2).setProvider(BC); ContentSigner signer signerBuilder.build(privateKey); // 6. 生成证书 X509CertificateHolder certHolder certBuilder.build(signer); X509Certificate certificate new JcaX509CertificateConverter().setProvider(BC).getCertificate(certHolder); // 7. 验证证书自签名用自身的公钥验证 certificate.verify(publicKey); System.out.println(\n自签名SM2证书生成并验证成功); // 8. 输出证书信息可保存为文件 System.out.println(序列号: certificate.getSerialNumber()); System.out.println(颁发者: certificate.getIssuerX500Principal()); System.out.println(主体: certificate.getSubjectX500Principal()); System.out.println(签名算法: certificate.getSigAlgName()); // 以PEM格式输出 String pemCert -----BEGIN CERTIFICATE-----\n new String(Base64.encode(certificate.getEncoded())) \n-----END CERTIFICATE-----; System.out.println(\nPEM格式证书:\n pemCert); } }注意事项算法名称在BouncyCastle中SM3withSM2是标准的国密签名算法标识。SM3是国密哈希算法SM2是签名算法。曲线参数代码中kpg.initialize(256)BouncyCastle在检测到国密提供者BC且算法为“EC”时通常会默认使用SM2的曲线参数。为了绝对明确也可以使用ECGenParameterSpec指定曲线名称但BC对国密的支持使得简单初始化即可。扩展域务必根据证书用途正确设置扩展。特别是subjectAlternativeName对于服务器证书必须包含所有需要访问的域名这是避免SSL certificate hostname mismatch的黄金法则。4. 解析与应用SM2证书进行签名验签生成了证书接下来我们就要用它来做点实际的事情签名和验签。这是确保数据完整性和身份认证的核心操作。4.1 从证书中提取SM2公钥并验签假设我们收到一段数据及其SM2签名以及签发者的SM2证书。我们需要验证签名是否有效。import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.util.encoders.Hex; import java.security.*; import java.security.cert.X509Certificate; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; public class SM2VerifyDemo { static { Security.addProvider(new BouncyCastleProvider()); } public static void main(String[] args) throws Exception { // 模拟场景已有证书、原始数据和签名 String pemCert -----BEGIN CERTIFICATE-----\n...; // 上一步生成的证书PEM字符串 byte[] originalData 这是需要验签的原始数据.getBytes(); String receivedSignatureHex 3045022100...; // 接收到的签名值Hex格式 // 1. 加载证书 CertificateFactory cf CertificateFactory.getInstance(X.509); X509Certificate certificate (X509Certificate) cf.generateCertificate( new java.io.ByteArrayInputStream(pemCert.getBytes()) ); // 2. 从证书中获取公钥 PublicKey publicKey certificate.getPublicKey(); System.out.println(从证书中提取的公钥算法: publicKey.getAlgorithm()); // 应显示 EC 或 1.2.156.10197.1.301 // 3. 准备签名验证器 // 注意算法名称必须与签名时使用的完全一致这里是 SM3withSM2 Signature verifier Signature.getInstance(SM3withSM2, BC); // 4. 使用公钥初始化验证器 verifier.initVerify(publicKey); // 5. 传入原始数据 verifier.update(originalData); // 6. 验证签名 byte[] receivedSignature Hex.decode(receivedSignatureHex); boolean isValid verifier.verify(receivedSignature); if (isValid) { System.out.println(验签成功数据完整且来自可信证书持有者。); } else { System.out.println(验签失败数据可能被篡改或签名不匹配。); } // 补充直接使用证书对象验签某些API支持 try { certificate.verify(publicKey); // 验证证书自身的签名如果是根证书或已知CA System.out.println(证书自身签名验证通过。); } catch (SignatureException e) { System.out.println(证书签名无效。); } } }4.2 使用SM2私钥进行数据签名作为数据发送方我们需要使用自己的SM2私钥对数据进行签名。import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.*; import java.util.HexFormat; public class SM2SignDemo { static { Security.addProvider(new BouncyCastleProvider()); } public static void main(String[] args) throws Exception { // 假设我们拥有之前生成的SM2密钥对 KeyPairGenerator kpg KeyPairGenerator.getInstance(EC, BC); kpg.initialize(256); KeyPair keyPair kpg.generateKeyPair(); PrivateKey privateKey keyPair.getPrivate(); byte[] dataToSign 这是一条重要的交易信息.getBytes(); // 1. 创建签名实例 Signature signer Signature.getInstance(SM3withSM2, BC); // 2. 使用私钥初始化签名器 signer.initSign(privateKey); // 3. 传入待签名数据 signer.update(dataToSign); // 4. 生成签名 byte[] digitalSignature signer.sign(); String signatureHex HexFormat.of().formatHex(digitalSignature); System.out.println(生成的SM2签名(Hex): signatureHex); // 5. 可选立即用配对的公钥验证一下确保签名有效 signer.initVerify(keyPair.getPublic()); signer.update(dataToSign); if (signer.verify(digitalSignature)) { System.out.println(自验证成功签名有效。); } } }实操心得与避坑指南算法标识一致性签名和验签必须使用完全相同的算法字符串。SM3withSM2是国标规范。不要误用SHA256withECDSA虽然底层都是椭圆曲线但算法流程和结果不同。数据编码签名操作是针对数据的原始字节或哈希值。确保双方对数据的编码如UTF-8理解一致否则验签必定失败。对于文本明确指定getBytes(“UTF-8”)是好习惯。签名输出格式SM2签名输出通常是ASN.1 DER编码的r, s对。这是一个标准的二进制格式。直接传输二进制或将其转换为Hex、Base64字符串都是常见的做法。验签时需要先解码回字节数组。私钥安全私钥是核心机密绝不能硬编码在代码中或提交到版本库。应使用安全的密钥库如JKS、PKCS12存储并通过密码访问。生产环境中考虑使用HSM硬件安全模块来保护私钥。5. 国密SSL/TLS集成与常见错误排查将SM2证书应用到实际的网络连接如HTTPS中是最终的实战场景。这通常意味着配置一个使用SM2证书和密码套件的TLS/SSL服务。5.1 在Java Web服务中配置SM2双证书国密HTTPS通常采用“双证书”模式一张加密证书加密公钥用于密钥交换一张签名证书签名公钥用于身份认证。但很多库和场景也支持单证书签名证书同时用于加密。以下以Spring Boot内嵌Tomcat配置单SM2证书为例首先将之前生成的SM2证书server.cer或.pem和对应的私钥server.key保存为文件。私钥如果是PKCS#8格式最好。创建application-sm2.yml配置文件server: port: 8443 ssl: enabled: true key-store-type: PEM # 使用PEM格式文件需要Tomcat Native支持或额外配置 key-store: classpath:sm2/server.pem # 包含证书和私钥的PEM文件 key-store-password: changeit # 如果私钥有密码则填写 key-alias: sm2-cert # 启用国密密码套件具体套件字符串需根据JRE/BC支持情况调整 ciphers: ECC-SM2-SM4-CBC-SM3, ECC-SM2-SM4-GCM-SM3 enabled-protocols: TLSv1.2由于Java原生SunJSSE可能不完全支持国密套件我们通常需要借助BouncyCastle的JSSE提供者。创建一个配置类import org.apache.catalina.connector.Connector; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import java.security.KeyStore; import java.security.Security; Configuration Profile(sm2) public class SM2SSLConfig { Bean public WebServerFactoryCustomizerTomcatServletWebServerFactory tomcatCustomizer() { return factory - { // 注册BouncyCastle作为JSSE提供者 Security.insertProviderAt(new BouncyCastleProvider(), 1); Security.insertProviderAt(new BouncyCastleJsseProvider(), 2); factory.addConnectorCustomizers((Connector connector) - { if (connector.getSecure()) { try { // 加载PEM格式的密钥和证书需要BC支持 // 这里简化处理实际中可能需要使用BC的PEMParser来分别读取私钥和证书 // 或者将PEM转换为PKCS12格式使用标准方式加载 KeyStore ks KeyStore.getInstance(PKCS12, BC); // ... 加载密钥库的代码 ... KeyManagerFactory kmf KeyManagerFactory.getInstance(PKIX, BCJSSE); kmf.init(ks, changeit.toCharArray()); SSLContext sslContext SSLContext.getInstance(TLS, BCJSSE); sslContext.init(kmf.getKeyManagers(), null, null); connector.setSSLContext(sslContext); } catch (Exception e) { throw new RuntimeException(Failed to setup SM2 SSL context, e); } } }); }; } }重要提示上述配置是一个方向性示例。在实际生产中直接使用PEM文件配置Tomcat可能比较棘手。更通用的做法是使用openssl命令将SM2证书和私钥合并成一个PKCS12文件.p12。openssl pkcs12 -export -out server.p12 -inkey server.key -in server.cer -name sm2-cert在application.yml中配置key-store-type: PKCS12和key-store指向这个.p12文件。确保应用类路径下包含了BouncyCastle的JSSE jar包bcpkix-jdk15on,bcprov-jdk15on,bctls-jdk15on并在启动时通过java.security文件或代码注册提供者。5.2 常见SSL/TLS错误排查实录结合网络热词我们来诊断几个高频错误1.SSL certificate hostname mismatch/certificate for localhost doesnt match根本原因客户端如浏览器、curl、HttpClient连接到的主机名如https://api.myapp.com与服务器证书中Subject Alternative Name (SAN)或Common Name (CN)字段列出的名称不匹配。排查步骤使用我们第二章的解析代码检查服务器证书的Subject和SAN扩展。确认客户端实际连接使用的URL中的主机名是否在证书的允许列表中。解决方案开发环境为自签名证书添加包含localhost、127.0.0.1以及你所用主机名的SAN条目。生产环境确保证书申请CSR时包含了所有需要使用的域名多域名证书或通配符证书。临时绕过仅测试在客户端代码中自定义一个跳过主机名验证的HostnameVerifier强烈不推荐用于生产环境。2.self signed certificate/unable to connect to api: ssl certificate has expired根本原因客户端不信任服务器证书的签发链。自签名证书的颁发者就是自己不在客户端的信任库中。证书过期则是因为当前时间不在证书的有效期内。排查步骤检查证书有效期notBefore,notAfter。检查证书链。客户端需要能够追溯到它信任的根证书。解决方案自签名证书将服务器的自签名证书或根证书导入到客户端的信任库如Java的cacerts或浏览器的受信任根证书颁发机构。证书过期联系证书颁发机构CA续订证书。测试环境在代码中自定义一个信任所有证书的TrustManager极端危险仅用于本地开发测试。3.no required ssl certificate was sent根本原因在双向TLS认证mTLS中服务器要求客户端提供证书但客户端没有发送或发送的证书不符合要求。排查步骤确认服务器是否启用了客户端证书认证。检查客户端代码是否正确配置了包含客户端证书和私钥的密钥库KeyStore。检查客户端证书是否由服务器信任的CA签发。解决方案确保客户端在发起SSL握手时加载了有效的客户端证书并将其发送给服务器。4.javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated根本原因SSL握手失败无法验证对端身份。可能是证书无效、过期、主机名不匹配或者根本未收到证书。排查思路这是一个比较泛化的错误。需要查看根本原因Cause。结合上述几点检查证书链、信任关系、主机名和有效期。5.The certificate format is incorrect根本原因程序无法识别提供的证书文件格式。可能是文件损坏、不是有效的X.509证书、或者编码DER/PEM不符合预期。解决方案使用openssl命令检查证书格式openssl x509 -in your.crt -text -noout。确保文件内容正确并根据库的要求提供正确格式如Java的KeyStore通常需要PKCS12或JKS格式而不是裸的PEM。6. 进阶SM2证书在API签名认证中的应用除了HTTPSSM2证书还可以用于API请求的签名认证这是一种比简单API Key更安全的方式。其流程类似于OAuth中的私钥签名JWT。基本流程客户端持有自己的SM2私钥。签名对请求的特定部分如Method、Path、Timestamp、Nonce、Body的摘要使用SM2私钥进行签名。传输将签名通常Base64编码、客户端证书序列号或标识一同放入HTTP头如X-Ca-Signature。服务端 a. 根据客户端标识找到对应的SM2证书公钥。 b. 使用相同的规则拼接出待验签字符串。 c. 使用证书中的公钥验证签名。示例客户端生成签名public class ApiSigner { private PrivateKey privateKey; private String certSn; // 证书序列号用于服务端查找公钥 public String generateSignature(String method, String path, String timestamp, String nonce, String body) throws Exception { // 1. 拼接待签名字符串规范需双方约定 String dataToSign String.format(%s\n%s\n%s\n%s\n%s, method.toUpperCase(), path, timestamp, nonce, computeBodySm3(body) // 对请求体计算SM3哈希 ); // 2. 使用SM3withSM2签名 Signature signer Signature.getInstance(SM3withSM2, BC); signer.initSign(privateKey); signer.update(dataToSign.getBytes(StandardCharsets.UTF_8)); byte[] signature signer.sign(); // 3. Base64编码后返回 return Base64.getEncoder().encodeToString(signature); } private String computeBodySm3(String body) throws NoSuchAlgorithmException { // 使用BouncyCastle计算SM3此处简化 MessageDigest md MessageDigest.getInstance(SM3, BC); byte[] digest md.digest(body.getBytes(StandardCharsets.UTF_8)); return Hex.toHexString(digest); } }服务端验证签名public class ApiVerifier { private MapString, PublicKey clientCertMap; // key: 证书序列号 value: 公钥 public boolean verifySignature(String clientSn, String method, String path, String timestamp, String nonce, String body, String receivedSignatureBase64) throws Exception { PublicKey publicKey clientCertMap.get(clientSn); if (publicKey null) { return false; } // 1. 按照相同规则拼接字符串 String dataToVerify String.format(%s\n%s\n%s\n%s\n%s, method.toUpperCase(), path, timestamp, nonce, computeBodySm3(body) ); // 2. 验证签名 Signature verifier Signature.getInstance(SM3withSM2, BC); verifier.initVerify(publicKey); verifier.update(dataToVerify.getBytes(StandardCharsets.UTF_8)); byte[] receivedSig Base64.getDecoder().decode(receivedSignatureBase64); return verifier.verify(receivedSig); } }这种方式实现了“无状态”的、基于非对称密码学的强身份认证安全性远高于简单的对称密钥或Token非常适合微服务间的高安全等级调用。