1. 项目概述与背景最近在做一个需要对接特定行业系统的项目对方明确要求通信和数据存储必须使用国密算法其中非对称加密部分指定了SM2。说实话第一次接到这个需求时我脑子里第一反应是去找找有没有现成的、像RSA那样在Java里开箱即用的工具类。结果发现JDK标准库对国密算法的支持几乎为零尤其是SM2这种基于椭圆曲线的算法。网上搜了一圈方案五花八门有的推荐用国产的商用密码库有的说可以自己照着国标文档实现——后者显然不现实前者又涉及到商务流程和潜在的授权问题。经过一番调研和踩坑最终选定了**BouncyCastleBC**这个老牌的安全提供者库来作为解决方案。它就像一个“瑞士军刀”式的密码学工具箱不仅国际算法支持得好对国密算法SM2/SM3/SM4也有相当完善的支持。更重要的是它是开源的集成到Spring Boot项目里非常方便。这篇文章我就把自己从零开始在Spring Boot项目里集成BouncyCastle并实现SM2全套功能密钥生成、加密、解密、签名、验签的完整过程、核心代码以及那些文档里不会写的“坑”都梳理出来。无论你是第一次接触国密还是正在为项目合规性头疼希望这篇近万字的实战记录能让你少走弯路。2. 核心工具选型为什么是BouncyCastle在Java世界里做密码学相关开发你绕不开java.security这套API。但标准库的算法提供者是有限的像SM2、SM3、SM4这些国密算法并不在其中。这时候你就需要一个额外的“安全提供者Security Provider”来扩展JDK的能力。2.1 主流国密集成方案对比面对SM2集成市面上主要有几种路径使用国产商用密码库比如一些信创厂商提供的SDK。优点是通常经过国家密码管理局认证合规性有保障性能和稳定性可能更优。缺点是需要商业授权集成过程可能绑定特定环境且技术支持和社区活跃度参差不齐。基于OpenSSL/GMSSL自行编译JNI库通过Java Native Interface调用C语言库。性能最好但跨平台部署极其麻烦需要为不同操作系统准备不同的动态链接库且JNI调试和维护成本很高。使用纯Java实现的第三方库BouncyCastle就是其中的佼佼者。它是一个用Java编写的密码学库提供了JCEJava Cryptography Extension提供者实现可以无缝集成到java.security框架中。我选择BouncyCastle主要基于以下几点考量零依赖与易集成它是一个jar包直接引入项目即可无需处理复杂的本地库依赖和跨平台问题。这对于Spring Boot这种追求“一键启动”的微服务架构非常友好。功能全面且成熟BC不仅支持国密算法还支持几乎所有常见的国际算法RSA, AES, ECC等。其代码经过多年发展和安全审计可靠性较高。活跃的社区与文档遇到问题在Stack Overflow、GitHub Issues上能找到大量的讨论和解决方案学习成本相对较低。满足当前项目需求对于大多数企业内部应用、对性能不是极端敏感的政务或金融对接场景BC的性能完全足够。我们的项目并发量中等BC的纯Java实现简化了部署和运维。注意如果你的项目处于严格等保或密评要求下务必确认使用BouncyCastle的国密实现是否满足合规要求。有时合规性要求使用特定认证的密码产品。对于大多数需要“支持国密算法”而非“通过国密认证”的业务场景BC是一个极佳的起点。2.2 BouncyCastle版本选择与依赖引入BC库有两个主要的版本线bcprov-jdk15on和bcprov-jdk18on数字代表其设计兼容的JDK大版本。对于Spring Boot 2.x/3.x项目通常使用JDK 8或以上选择bcprov-jdk18on即可。建议使用较新的稳定版本以获得更好的性能和安全性更新。在Maven项目中只需在pom.xml中添加以下依赖dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk18on/artifactId version1.78/version !-- 请检查并使用最新稳定版本 -- /dependency对于Gradle项目在build.gradle的dependencies块中添加implementation org.bouncycastle:bcprov-jdk18on:1.78引入依赖只是第一步关键是如何让JVM在运行时识别并使用BC作为安全提供者。3. 项目初始化与BouncyCastle配置创建一个新的Spring Boot项目这里以Spring Boot 3.x为例我们接下来要解决的核心问题是如何确保BC提供者在整个应用生命周期内被正确加载和注册。3.1 静态注册 vs. 动态注册BC提供者的注册方式有两种静态注册修改JRE的安全配置文件$JAVA_HOME/jre/lib/security/java.security在security.provider列表中添加一行。这种方式影响全局不推荐在应用部署中使用因为它依赖于特定的服务器环境。动态注册在应用程序启动时通过代码将BC提供者添加到JVM的安全提供者列表中。这是Spring Boot项目中的推荐做法做到了应用级别的自包含。3.2 通过配置类实现动态注册我们创建一个配置类使用PostConstruct确保在Bean初始化完成后立即注册提供者。package com.yourproject.config; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.springframework.context.annotation.Configuration; import javax.annotation.PostConstruct; import java.security.Security; Configuration public class CryptoConfig { /** * 在Spring容器初始化完成后动态注册BouncyCastle提供者。 * 使用PostConstruct确保此方法在Bean初始化后执行。 */ PostConstruct public void init() { // 检查是否已经注册避免重复注册 if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) null) { Security.addProvider(new BouncyCastleProvider()); System.out.println(BouncyCastle Provider 注册成功。); } } }关键点解析BouncyCastleProvider.PROVIDER_NAME的值通常是BC。通过Security.getProvider(BC)判断可以防止在热部署或某些特定场景下重复注册虽然重复注册一般不会报错但保持代码整洁是好的习惯。PostConstruct注解是Java EE/Jakarta EE的规范Spring支持它。它标记的方法会在依赖注入完成后执行非常适合做初始化工作。你也可以使用CommandLineRunner或ApplicationRunner接口但PostConstruct更轻量、更直接。注册成功后当你使用Cipher.getInstance(SM2),KeyPairGenerator.getInstance(EC, BC)等代码时JVM就知道去向BC这个提供者请求算法实现。3.3 验证配置是否生效编写一个简单的测试Controller或单元测试来验证BC提供者是否可用。package com.yourproject.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.security.Security; RestController public class TestController { GetMapping(/test/bc) public String testBouncyCastle() { StringBuilder sb new StringBuilder(); sb.append(当前已注册的安全提供者br); java.security.Provider[] providers Security.getProviders(); for (java.security.Provider p : providers) { sb.append(p.getName()).append( (Version ).append(p.getVersionStr()).append()br); // 可以进一步查看该提供者支持的服务 // p.getServices().forEach(service - sb.append( - ).append(service.getAlgorithm()).append(br)); } return sb.toString(); } }启动应用访问/test/bc你应该能在输出的列表中看到BC或BouncyCastle字样这证明提供者注册成功了。4. SM2算法核心原理与Java实现在开始写代码前花几分钟理解SM2的核心能帮你更好地理解后续API的调用和可能遇到的异常。4.1 SM2算法简述SM2是一种基于椭圆曲线密码学ECC的公钥密码算法。它包含多个功能部分我们主要用到数字签名算法用于验明数据真伪和完整性类似ECDSA。密钥交换协议用于双方协商共享密钥本项目未涉及。公钥加密算法用于加密数据只有对应的私钥才能解密。国密SM2标准指定的椭圆曲线参数是固定的称为sm2p256v1这不同于国际上通用的NIST P-256等曲线。BC库已经内置了这条曲线的定义。4.2 SM2密钥对生成生成SM2密钥对是第一步。由于SM2使用特定的椭圆曲线我们需要在生成时指明。package com.yourproject.util; import org.bouncycastle.jce.ECNamedCurveTable; import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; import java.security.*; import java.security.spec.ECGenParameterSpec; public class Sm2KeyGenerator { /** * 生成SM2密钥对 * return 生成的密钥对 * throws NoSuchProviderException 如果BC提供者未注册 * throws NoSuchAlgorithmException 如果算法不存在 * throws InvalidAlgorithmParameterException 如果参数无效 */ public static KeyPair generateKeyPair() throws NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException { // 方法一使用BC特有的方式推荐更明确 // ECNamedCurveParameterSpec sm2Spec ECNamedCurveTable.getParameterSpec(sm2p256v1); // KeyPairGenerator kpg KeyPairGenerator.getInstance(EC, BC); // kpg.initialize(sm2Spec, new SecureRandom()); // 方法二使用标准的EC算法名并指定曲线参数更通用 KeyPairGenerator kpg KeyPairGenerator.getInstance(EC, BC); ECGenParameterSpec sm2Spec new ECGenParameterSpec(sm2p256v1); // 国密标准曲线名称 kpg.initialize(sm2Spec, new SecureRandom()); // 使用强随机数源 return kpg.generateKeyPair(); } /** * 将公钥和私钥转换为Base64编码的字符串便于存储和传输 */ public static void main(String[] args) throws Exception { KeyPair keyPair generateKeyPair(); PublicKey publicKey keyPair.getPublic(); PrivateKey privateKey keyPair.getPrivate(); // 通常使用X.509格式编码公钥PKCS#8格式编码私钥 String publicKeyBase64 java.util.Base64.getEncoder().encodeToString(publicKey.getEncoded()); String privateKeyBase64 java.util.Base64.getEncoder().encodeToString(privateKey.getEncoded()); System.out.println(公钥(Base64): publicKeyBase64); System.out.println(私钥(Base64): privateKeyBase64); System.out.println(公钥算法: publicKey.getAlgorithm()); System.out.println(公钥格式: publicKey.getFormat()); // 通常是 X.509 System.out.println(私钥格式: privateKey.getFormat()); // 通常是 PKCS#8 } }实操要点与避坑指南曲线名称必须使用sm2p256v1。如果你误用了prime256v1NIST P-256生成的密钥对虽然也是ECC密钥但不符合国密SM2标准与其他SM2实现互通时会失败。随机数源务必使用SecureRandom。在Linux环境下默认会使用/dev/urandom这是安全的。避免使用new Random()。密钥格式publicKey.getEncoded()得到的是DER编码的X.509格式字节流privateKey.getEncoded()得到的是DER编码的PKCS#8格式字节流。Base64编码后就是常见的PEM格式去掉头尾和换行后的内容。这是密钥持久化存入数据库或配置文件和网络传输的基础。异常处理NoSuchProviderException通常意味着BC提供者没有成功注册回头检查第3节的配置。InvalidAlgorithmParameterException意味着曲线名称不对。4.3 SM2公钥加密与私钥解密实现SM2加密解密过程比RSA稍复杂因为它涉及到一个称为C1C2C3或C1C3C2的编码顺序问题这是国密标准与国际标准如ECIES的一个差异点。BC库通过SM2Engine类封装了这些细节。package com.yourproject.util; import org.bouncycastle.crypto.engines.SM2Engine; import org.bouncycastle.crypto.params.ECPrivateKeyParameters; import org.bouncycastle.crypto.params.ECPublicKeyParameters; import org.bouncycastle.crypto.util.PublicKeyFactory; import org.bouncycastle.crypto.util.PrivateKeyFactory; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; import java.security.*; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; public class Sm2CryptoUtil { private static final String ALGORITHM SM2; // BC库中SM2加密模式 Mode.C1C3C2 是国密标准格式 private static final SM2Engine.Mode ENGINE_MODE SM2Engine.Mode.C1C3C2; /** * 从Base64字符串还原公钥对象 */ public static PublicKey restorePublicKey(String publicKeyBase64) throws Exception { byte[] keyBytes java.util.Base64.getDecoder().decode(publicKeyBase64); X509EncodedKeySpec keySpec new X509EncodedKeySpec(keyBytes); KeyFactory keyFactory KeyFactory.getInstance(EC, BC); // 注意指定Provider为BC return keyFactory.generatePublic(keySpec); } /** * 从Base64字符串还原私钥对象 */ public static PrivateKey restorePrivateKey(String privateKeyBase64) throws Exception { byte[] keyBytes java.util.Base64.getDecoder().decode(privateKeyBase64); PKCS8EncodedKeySpec keySpec new PKCS8EncodedKeySpec(keyBytes); KeyFactory keyFactory KeyFactory.getInstance(EC, BC); // 注意指定Provider为BC return keyFactory.generatePrivate(keySpec); } /** * SM2 公钥加密 * param publicKey 公钥对象 * param data 明文数据 * return 密文字节数组 */ public static byte[] encrypt(PublicKey publicKey, byte[] data) throws Exception { // 将JCE公钥转换为BC库内部的参数格式 ECPublicKeyParameters ecPublicKeyParameters PublicKeyFactory.createKey(publicKey.getEncoded()); SM2Engine engine new SM2Engine(ENGINE_MODE); engine.init(true, ecPublicKeyParameters); // true 表示加密模式 return engine.processBlock(data, 0, data.length); } /** * SM2 私钥解密 * param privateKey 私钥对象 * param cipherData 密文数据 * return 明文字节数组 */ public static byte[] decrypt(PrivateKey privateKey, byte[] cipherData) throws Exception { // 将JCE私钥转换为BC库内部的参数格式 ECPrivateKeyParameters ecPrivateKeyParameters PrivateKeyFactory.createKey(privateKey.getEncoded()); SM2Engine engine new SM2Engine(ENGINE_MODE); engine.init(false, ecPrivateKeyParameters); // false 表示解密模式 return engine.processBlock(cipherData, 0, cipherData.length); } // 提供Base64字符串入参的便捷方法 public static String encrypt(String publicKeyBase64, String plainText) throws Exception { PublicKey publicKey restorePublicKey(publicKeyBase64); byte[] cipherBytes encrypt(publicKey, plainText.getBytes(java.nio.charset.StandardCharsets.UTF_8)); return java.util.Base64.getEncoder().encodeToString(cipherBytes); } public static String decrypt(String privateKeyBase64, String cipherTextBase64) throws Exception { PrivateKey privateKey restorePrivateKey(privateKeyBase64); byte[] cipherBytes java.util.Base64.getDecoder().decode(cipherTextBase64); byte[] plainBytes decrypt(privateKey, cipherBytes); return new String(plainBytes, java.nio.charset.StandardCharsets.UTF_8); } }核心细节与经验SM2Engine.Mode这是最容易出错的地方。国密标准规定的密文结构是C1C3C2其中C1是椭圆曲线点C3是SM3摘要C2是实际加密数据。而BC库为了兼容也支持国际通用的C1C2C3格式。在与中国其他遵循国密标准的系统如很多硬件加密机、其他语言实现的SM2库交互时必须使用Mode.C1C3C2。如果模式不匹配解密一定会失败。我建议在工具类中将其定义为常量并在整个项目中统一使用。密钥转换PublicKeyFactory.createKey和PrivateKeyFactory.createKey是BC库提供的工具方法用于将标准的、经过编码的密钥字节流转换为其内部密码学操作所需的参数对象。这是调用SM2Engine的前提。数据编码加密输入和解密输出都是字节数组(byte[])。在实际业务中明文通常是字符串密文需要存储或传输因此配合UTF-8字符集和Base64编解码是标准做法。注意加解密过程中字符集要一致。异常处理解密过程可能抛出InvalidCipherTextException等异常可能的原因有私钥不匹配、密文被篡改、加密模式(Mode)错误、密文Base64解码失败等。4.4 SM2私钥签名与公钥验签实现签名验签用于确保数据的完整性和不可否认性。SM2的签名算法本身也和国际上的ECDSA有所不同包含对用户ID的哈希处理即“SM2withSM3”。package com.yourproject.util; import org.bouncycastle.asn1.gm.GMObjectIdentifiers; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; import java.security.*; public class Sm2SignatureUtil { // 国密SM2推荐的默认用户ID用于参与签名生成。通常使用这个值即可。 private static final String DEFAULT_USER_ID 1234567812345678; /** * SM2 私钥签名 * param privateKey 私钥 * param data 待签名数据 * param userId 用户标识参与签名计算。可为null将使用默认值。 * return 签名字节数组ASN.1 DER编码格式 */ public static byte[] sign(PrivateKey privateKey, byte[] data, String userId) throws Exception { // 获取BC库提供的特定算法名称 Signature signature Signature.getInstance(GMObjectIdentifiers.sm2sign_with_sm3.toString(), BC); // 如果需要指定用户ID需要通过BC特有的Key类型来设置参数 if (privateKey instanceof BCECPrivateKey) { // BC库的Key实现允许我们传递更多参数 // 这里简化处理通常使用默认ID即可。更复杂的ID设置需要用到ECNamedCurveSpec等。 } signature.initSign(privateKey); if (userId ! null) { // 实际中用户ID会与原文一起参与SM3哈希但JCE API隐藏了细节。 // BC库在获取Signature实例时内部已经处理了“SM2withSM3”的流程。 // 对于大多数API直接调用update即可。 } signature.update(data); return signature.sign(); // 返回的签名是ASN.1 DER编码的 (r, s) 序列 } /** * SM2 公钥验签 * param publicKey 公钥 * param data 原始数据 * param signatureBytes 签名字节数组 * param userId 用户标识需与签名时一致。 * return 验签是否通过 */ public static boolean verify(PublicKey publicKey, byte[] data, byte[] signatureBytes, String userId) throws Exception { Signature signature Signature.getInstance(GMObjectIdentifiers.sm2sign_with_sm3.toString(), BC); signature.initVerify(publicKey); signature.update(data); return signature.verify(signatureBytes); } // 提供便捷方法使用默认用户ID public static byte[] sign(PrivateKey privateKey, byte[] data) throws Exception { return sign(privateKey, data, DEFAULT_USER_ID); } public static boolean verify(PublicKey publicKey, byte[] data, byte[] signatureBytes) throws Exception { return verify(publicKey, data, signatureBytes, DEFAULT_USER_ID); } // 提供Base64字符串入参的便捷方法 public static String signToBase64(String privateKeyBase64, String plainText) throws Exception { PrivateKey privateKey Sm2CryptoUtil.restorePrivateKey(privateKeyBase64); byte[] signature sign(privateKey, plainText.getBytes(java.nio.charset.StandardCharsets.UTF_8)); return java.util.Base64.getEncoder().encodeToString(signature); } public static boolean verifyFromBase64(String publicKeyBase64, String plainText, String signatureBase64) throws Exception { PublicKey publicKey Sm2CryptoUtil.restorePublicKey(publicKeyBase64); byte[] signatureBytes java.util.Base64.getDecoder().decode(signatureBase64); return verify(publicKey, plainText.getBytes(java.nio.charset.StandardCharsets.UTF_8), signatureBytes); } }签名验签的特别注意事项算法标识符GMObjectIdentifiers.sm2sign_with_sm3.toString()是BC库定义的国密SM2签名算法OID对象标识符它对应的是“SM2withSM3”算法。不要使用SHA256withECDSA之类的通用算法名那会是完全不同的签名流程。签名输出格式signature.sign()返回的字节数组不是简单的(r, s)拼接而是ASN.1 DER编码的序列。这种格式是标准的便于在不同系统间交换。验签时也需要传入同样格式的签名。用户IDUserIdSM2签名标准要求一个用户标识符参与哈希运算通常使用默认的123456781234567816字节。除非对接方有特殊要求否则使用默认值即可。BC库的JCE Provider在内部处理了这个细节我们通过特定的算法名调用即可。性能考虑签名验签操作比对称加密慢对于大量数据应对数据本身先做哈希如SM3然后对哈希值进行签名这是标准做法。但“SM2withSM3”这个算法标识已经隐含了用SM3做哈希的步骤所以我们直接对原始数据调用update即可。5. 在Spring Boot服务中封装与应用现在我们已经有了核心的工具类接下来将其封装成Spring Boot服务以便在Controller、Service中方便地调用。5.1 创建配置属性类为了灵活管理密钥比如区分不同环境的密钥、不同用途的密钥对我们将其配置在application.yml中。# application.yml sm2: # 是否启用SM2功能方便开关 enabled: true # 平台自身的密钥对用于解密别人发来的数据或验证别人的签名 platform: private-key: 你的Base64编码私钥字符串 public-key: 你的Base64编码公钥字符串 # 合作方的公钥用于加密发给对方的数据或验证对方的签名 partner: public-key: 合作方的Base64编码公钥字符串对应的配置属性类package com.yourproject.config.properties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; Component ConfigurationProperties(prefix sm2) public class Sm2Properties { private boolean enabled; private KeyPairConfig platform; private KeyPairConfig partner; // getters and setters public static class KeyPairConfig { private String privateKey; private String publicKey; // getters and setters } }5.2 创建SM2服务类这个服务类注入配置属性并对外提供简洁的加解密、签名验签API。package com.yourproject.service; import com.yourproject.config.properties.Sm2Properties; import com.yourproject.util.Sm2CryptoUtil; import com.yourproject.util.Sm2SignatureUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import java.security.PrivateKey; import java.security.PublicKey; Service Slf4j public class Sm2Service { Autowired private Sm2Properties sm2Properties; private PublicKey platformPublicKey; private PrivateKey platformPrivateKey; private PublicKey partnerPublicKey; PostConstruct public void init() throws Exception { if (!sm2Properties.isEnabled()) { log.warn(SM2功能未启用请在配置中设置sm2.enabledtrue); return; } // 加载平台自身密钥 if (sm2Properties.getPlatform() ! null) { String pubKeyStr sm2Properties.getPlatform().getPublicKey(); String priKeyStr sm2Properties.getPlatform().getPrivateKey(); if (pubKeyStr ! null !pubKeyStr.trim().isEmpty()) { platformPublicKey Sm2CryptoUtil.restorePublicKey(pubKeyStr); } if (priKeyStr ! null !priKeyStr.trim().isEmpty()) { platformPrivateKey Sm2CryptoUtil.restorePrivateKey(priKeyStr); } } // 加载合作方公钥 if (sm2Properties.getPartner() ! null) { String pubKeyStr sm2Properties.getPartner().getPublicKey(); if (pubKeyStr ! null !pubKeyStr.trim().isEmpty()) { partnerPublicKey Sm2CryptoUtil.restorePublicKey(pubKeyStr); } } log.info(SM2服务初始化完成。); } /** * 使用合作方公钥加密数据发送给合作方 */ public String encryptForPartner(String plainText) throws Exception { if (partnerPublicKey null) { throw new IllegalStateException(合作方公钥未配置无法加密。); } return Sm2CryptoUtil.encrypt(partnerPublicKey, plainText.getBytes()); } /** * 使用平台私钥解密数据接收来自合作方的密文 */ public String decryptFromPartner(String cipherTextBase64) throws Exception { if (platformPrivateKey null) { throw new IllegalStateException(平台私钥未配置无法解密。); } return Sm2CryptoUtil.decrypt(platformPrivateKey, cipherTextBase64); } /** * 使用平台私钥对数据签名 */ public String signWithPlatform(String data) throws Exception { if (platformPrivateKey null) { throw new IllegalStateException(平台私钥未配置无法签名。); } return Sm2SignatureUtil.signToBase64( Sm2CryptoUtil.getPrivateKeyBase64(platformPrivateKey), data); } /** * 使用合作方公钥验证签名验证合作方发来的数据 */ public boolean verifyFromPartner(String data, String signatureBase64) throws Exception { if (partnerPublicKey null) { throw new IllegalStateException(合作方公钥未配置无法验签。); } return Sm2SignatureUtil.verifyFromBase64( Sm2CryptoUtil.getPublicKeyBase64(partnerPublicKey), data, signatureBase64); } // 更多便捷方法... public PublicKey getPlatformPublicKey() { return platformPublicKey; } // ... 其他getter }5.3 在REST API中应用最后我们创建一个Controller来演示如何使用这些服务。package com.yourproject.controller; import com.yourproject.service.Sm2Service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; RestController RequestMapping(/api/sm2) public class Sm2DemoController { Autowired private Sm2Service sm2Service; /** * 加密演示 */ PostMapping(/encrypt) public ApiResponse encrypt(RequestBody EncryptRequest request) { try { String cipherText sm2Service.encryptForPartner(request.getPlainText()); return ApiResponse.success(cipherText); } catch (Exception e) { return ApiResponse.error(加密失败: e.getMessage()); } } /** * 解密演示 */ PostMapping(/decrypt) public ApiResponse decrypt(RequestBody DecryptRequest request) { try { String plainText sm2Service.decryptFromPartner(request.getCipherText()); return ApiResponse.success(plainText); } catch (Exception e) { return ApiResponse.error(解密失败: e.getMessage()); } } /** * 签名演示 */ PostMapping(/sign) public ApiResponse sign(RequestBody SignRequest request) { try { String signature sm2Service.signWithPlatform(request.getData()); return ApiResponse.success(signature); } catch (Exception e) { return ApiResponse.error(签名失败: e.getMessage()); } } /** * 验签演示 */ PostMapping(/verify) public ApiResponse verify(RequestBody VerifyRequest request) { try { boolean isValid sm2Service.verifyFromPartner(request.getData(), request.getSignature()); return ApiResponse.success(isValid ? 验签成功 : 验签失败, isValid); } catch (Exception e) { return ApiResponse.error(验签过程异常: e.getMessage()); } } // 内部请求对象定义 static class EncryptRequest { private String plainText; /* getter/setter */ } static class DecryptRequest { private String cipherText; /* getter/setter */ } static class SignRequest { private String data; /* getter/setter */ } static class VerifyRequest { private String data; private String signature; /* getter/setter */ } static class ApiResponse { /* 标准的API响应封装 */ } }至此一个完整的、可在Spring Boot项目中直接使用的SM2国密算法集成方案就完成了。你可以通过调用这些API轻松实现与合作伙伴系统的国密算法数据安全交互。6. 实战中遇到的典型问题与排查技巧在实际开发和联调过程中我遇到了不少坑。这里把最常见的问题和解决方法列出来希望能帮你快速定位。6.1 常见异常与原因分析异常信息可能原因排查步骤java.security.NoSuchProviderException: BCBouncyCastle提供者未成功注册。1. 检查CryptoConfig类是否被Spring扫描到有Configuration注解。2. 检查Security.addProvider是否被执行查看启动日志。3. 检查依赖冲突是否有其他库引入了旧版本BC并覆盖了注册。java.security.InvalidKeyException密钥格式错误、密钥类型不匹配或密钥已损坏。1. 确认用于还原密钥的Base64字符串是否正确、完整没有多余空格或换行。2. 确认公钥用X509EncodedKeySpec私钥用PKCS8EncodedKeySpec。3. 确认KeyFactory.getInstance(“EC”, “BC”)中指定了Provider为”BC”。org.bouncycastle.crypto.InvalidCipherTextException加密/解密失败的最常见异常。1.首要检查加密模式(Mode)加密和解密使用的SM2Engine.Mode必须一致强烈建议统一用C1C3C2。2. 公私钥不配对。3. 密文在传输或存储过程中被损坏或Base64编解码出错。4. 使用了错误的曲线参数生成的密钥。签名验证始终返回false签名或验签过程有误。1. 确认签名和验签使用的是相同的用户ID默认即可。2. 确认签名数据(data)在签名和验签时完全一致包括编码、空格等。3. 确认使用的是GMObjectIdentifiers.sm2sign_with_sm3算法而不是其他ECDSA算法。4. 检查公钥是否正确是对应签名私钥的公钥。与其他系统如C、Python互通失败双方实现标准不一致。1.密文格式确认对方使用的是C1C3C2还是C1C2C3格式。2.签名格式确认对方输出的签名是ASN.1 DER编码还是简单的(r,s)拼接。3.椭圆曲线参数确认双方都使用sm2p256v1曲线。4.用户ID确认签名验签使用的用户ID是否一致。6.2 密钥管理的最佳实践建议不要硬编码在代码中务必像示例一样将Base64编码的密钥放在配置文件如application-{profile}.yml或配置中心里。生产环境的私钥更应使用Vault等密钥管理服务。区分环境开发、测试、生产环境使用不同的密钥对。定期轮换制定密钥轮换策略虽然SM2密钥长度256位目前很安全但定期更换是良好的安全习惯。备份私钥私钥一旦丢失由其加密的数据将无法解密。务必安全备份。使用硬件安全模块HSM对于金融级等高安全要求场景应考虑使用HSM来生成和存储私钥密码运算也在硬件内完成Java代码只调用接口。BC库也支持通过PKCS#11接口与HSM交互。6.3 性能优化小贴士对象复用SM2Engine和Signature对象在单次操作后可以重置(init)并重复使用避免频繁创建对象带来的开销。但在高并发下需考虑线程安全性通常每个线程使用独立实例或采用池化技术。数据量较大时SM2作为非对称加密不适合加密大量数据如超过几十KB的文件。标准做法是用SM2加密一个随机生成的对称密钥如SM4密钥然后用SM4去加密实际的大数据。即“SM2加密对称密钥对称密钥加密数据”的混合加密体系。签名验签对于很长的数据直接签名效率尚可因为“SM2withSM3”内部已经做了哈希。但如果数据源本身就是哈希值如文件哈希可以对哈希值直接进行签名操作需使用支持直接签哈希的底层APIJCE的Signature类通常设计为接收原始数据。集成过程就像拼图把BouncyCastle这个强大的密码学工具箱嵌入到Spring Boot的标准安全框架里再按照国密SM2的规则去调用它。最深的体会就是“细节决定成败”一个Mode参数的差异就足以让整个通信链路瘫痪。所以在正式与外部系统联调前务必先用自己的代码进行完整的加密-解密、签名-验签闭环测试。把密钥、密文、签名值都打印出来甚至用在线工具确保来源可靠做交叉验证能提前发现大部分兼容性问题。这套代码已经在几个需要国密支持的项目里稳定运行了如果你在集成时遇到其他怪问题很可能就是某个细微的“标准”或“约定”没对上耐心对照文档和日志总能解决的。