Spring Boot集成Bouncy Castle实现SM2国密算法:前后端加密交互完整指南

📅 2026/7/1 15:12:58
Spring Boot集成Bouncy Castle实现SM2国密算法:前后端加密交互完整指南
1. 项目概述与核心价值最近在做一个对数据安全要求比较高的项目涉及到前后端敏感数据的加密传输。甲方明确要求必须使用国密算法特别是非对称加密部分点名要用SM2。这其实挺常见的现在金融、政务、物联网这些领域国密算法的落地已经是硬性标准了。我一开始想用Java实现SM2找个现成的国密算法库不就行了结果一上手就发现事情没那么简单。JDK自带的加密体系里并没有原生支持SM2。网上倒是有一些开源实现比如hutool里的SmUtil用起来确实方便但当你需要和前端比如Vue、React的JavaScript加密库进行交互时兼容性问题就冒出来了。前端常用的sm-crypto库其内部实现和某些Java库的默认参数、编码格式可能存在微妙的差异导致前端加密的数据后端解不开或者反过来。这种“联调”阶段的坑往往最耗时间。经过一番调研和踩坑我最终选择了Bouncy Castle这个老牌、强大的加密提供者Provider来在Spring Boot中实现SM2。Bouncy Castle简称BC是一个提供了大量加密算法实现的Java库它对国密算法的支持相对成熟和标准。更重要的是通过仔细配置BC的SM2实现并规范前后端的密钥格式、加密模式、编码方式可以确保与前端sm-crypto等JS库无缝对接。这个方案不仅解决了当前需求其构建的加密工具类和交互规范也成了团队后续项目的标准组件。下面我就把从环境集成、密钥生成、加解密实现到前后端联调的完整过程以及我踩过的那些“坑”详细分享一下。2. 技术选型与环境搭建2.1 为什么是Bouncy Castle在Java生态里做加密JCAJava Cryptography Architecture是基石。但JCA更像一个框架具体的算法实现如AES、RSA由Provider提供。Oracle JDK默认的Provider不支持国密。这时候就需要引入第三方ProviderBouncy Castle就是其中最权威、最广泛使用的一个。选择BC的主要原因有三个算法支持全面且标准BC对SM2、SM3、SM4等国密算法的实现遵循了国家密码管理局的标准规范。这对于确保与其他标准实现如前端sm-crypto、硬件加密机的互操作性至关重要。成熟稳定社区活跃BC是一个经历了长时间考验的开源项目被广泛应用于生产环境。这意味着其代码质量、安全性和遇到问题时的解决方案都更有保障。灵活的集成方式我们可以将BC作为JCA的一个Provider动态注册到JVM中这样就能使用标准的KeyPairGenerator、Cipher等JCA接口来操作SM2学习成本和代码迁移成本都更低。相比之下一些其他国产开源Jar包可能更“轻便”但可能在算法实现的严格标准性、长期维护性上存在风险。对于企业级应用尤其是涉及合规要求的场景BC是更稳妥的选择。2.2 Spring Boot项目集成Bouncy Castle集成BC主要分为两步引入依赖和注册Provider。这里我推荐使用Bouncy Castle的bcprov-jdk15on和bcpkix-jdk15on。后者包含了处理证书和CRL等公钥基础设施相关的功能虽然SM2基础加解密不一定需要但为了功能的完整性和未来扩展建议一并引入。Maven依赖配置dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk15on/artifactId version1.70/version !-- 请检查并使用最新稳定版 -- /dependency dependency groupIdorg.bouncycastle/groupId artifactIdbcpkix-jdk15on/artifactId version1.70/version /dependency关键一步注册Provider仅仅引入依赖JVM并不会自动使用BC。我们需要在应用启动时将BC的Provider注册到JCA中。一个可靠的方式是使用PostConstruct在一个配置类里完成。import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.springframework.context.annotation.Configuration; import javax.annotation.PostConstruct; import java.security.Security; Configuration public class CryptoConfig { PostConstruct public void init() { // 防止重复注册 if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) null) { Security.addProvider(new BouncyCastleProvider()); System.out.println(BouncyCastle Provider 注册成功。); } } }注意注册Provider的代码要确保在任何加密操作之前执行。将其放在PostConstruct中由Spring容器在Bean初始化后调用是一个简单有效的方法。另外一定要检查是否已注册避免重复注册导致意外问题。验证注册是否成功你可以写一个简单的测试接口或单元测试打印当前所有ProviderTest public void testProvider() { Provider[] providers Security.getProviders(); for (Provider p : providers) { System.out.println(p.getName()); } }如果输出中包含BC或BouncyCastle就说明注册成功了。3. SM2密钥对生成与管理3.1 使用BC生成SM2密钥对SM2算法基于椭圆曲线密码学ECC其密钥对包括一个私钥PrivateKey和一个公钥PublicKey。生成密钥对时需要指定一条标准的椭圆曲线参数。国密SM2标准推荐使用sm2p256v1这条曲线其OID为1.2.156.10197.1.301。以下是生成SM2密钥对的工具方法import lombok.extern.slf4j.Slf4j; import org.bouncycastle.asn1.gm.GMNamedCurves; import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; import org.bouncycastle.jce.spec.ECParameterSpec; import org.springframework.stereotype.Component; import java.security.*; import java.security.spec.ECGenParameterSpec; import java.util.Base64; Slf4j Component public class Sm2KeyGenerator { /** * 生成SM2密钥对 * return 包含公钥和私钥的KeyPair对象 * throws NoSuchAlgorithmException * throws InvalidAlgorithmParameterException */ public KeyPair generateSm2KeyPair() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { // 1. 获取SM2椭圆曲线参数 final X9ECParameters sm2EcParameters GMNamedCurves.getByName(sm2p256v1); final ECParameterSpec sm2Spec new ECParameterSpec( sm2EcParameters.getCurve(), sm2EcParameters.getG(), sm2EcParameters.getN(), sm2EcParameters.getH() ); // 2. 使用标准JCA接口指定算法为EC并设置Provider为BC KeyPairGenerator keyPairGenerator KeyPairGenerator.getInstance(EC, BouncyCastleProvider.PROVIDER_NAME); keyPairGenerator.initialize(sm2Spec, new SecureRandom()); // 使用强随机数种子 // 3. 生成密钥对 KeyPair keyPair keyPairGenerator.generateKeyPair(); log.info(SM2密钥对生成成功。); return keyPair; } /** * 将公钥对象转换为Base64编码的字符串便于存储或传输 * param publicKey 公钥对象 * return Base64字符串 */ public String getPublicKeyBase64(PublicKey publicKey) { BCECPublicKey bcecPublicKey (BCECPublicKey) publicKey; // 获取Q值椭圆曲线上的点的编码形式 byte[] encoded bcecPublicKey.getQ().getEncoded(false); // false表示不压缩 return Base64.getEncoder().encodeToString(encoded); } /** * 将私钥对象转换为Base64编码的字符串需妥善保管 * param privateKey 私钥对象 * return Base64字符串 */ public String getPrivateKeyBase64(PrivateKey privateKey) { BCECPrivateKey bcecPrivateKey (BCECPrivateKey) privateKey; // 获取私钥大整数D的字节数组 byte[] encoded bcecPrivateKey.getD().toByteArray(); // 注意私钥D的字节数组长度可能不固定可能需要处理前导零 return Base64.getEncoder().encodeToString(encoded); } }关键点解析曲线参数GMNamedCurves.getByName(sm2p256v1)是BC库中定义的国密标准曲线这是与前端sm-crypto互操作的基础。切勿使用其他非标曲线。算法名称KeyPairGenerator.getInstance(EC, BouncyCastleProvider.PROVIDER_NAME)。这里算法名是EC椭圆曲线通用名但通过BC Provider和指定的sm2Spec参数实际生成的就是SM2密钥。直接使用SM2可能在某些版本中不被识别。公钥格式SM2公钥本质是椭圆曲线上的一个点(Q)。getEncoded(false)获取的是非压缩格式的字节表示04 || X || Y这是最通用、兼容性最好的格式。前端sm-crypto通常也接受这种格式的Base64或Hex字符串。私钥格式私钥是一个大整数(D)。直接将其转换为字节数组并Base64编码。这里需要注意BigInteger.toByteArray()可能包含符号位导致数组长度不定但在SM2上下文下通常直接使用即可。更严谨的做法是将其填充或转换为固定长度的字节数组如32字节。3.2 密钥的存储与安全实践生成的密钥对需要妥善管理公钥可以公开发布用于加密或验签。可以存储在配置文件、数据库或通过接口动态下发。私钥必须绝对保密。绝不能硬编码在代码中或提交到版本库。推荐方案将私钥的Base64字符串存储在环境变量、云厂商的密钥管理服务如AWS KMS,阿里云KMS或专用的硬件安全模块HSM中。应用启动时从这些安全位置读取。次选方案对于安全性要求稍低的内部系统可以使用经过加密的配置文件并在启动时注入密码解密。示例从环境变量读取私钥并还原为PrivateKey对象public PrivateKey loadPrivateKeyFromEnv() throws Exception { String base64PrivateKey System.getenv(SM2_PRIVATE_KEY); byte[] privateKeyBytes Base64.getDecoder().decode(base64PrivateKey); BigInteger d new BigInteger(1, privateKeyBytes); // 使用1确保正数 // 获取曲线参数 X9ECParameters sm2EcParameters GMNamedCurves.getByName(sm2p256v1); ECParameterSpec sm2Spec new ECParameterSpec(...); // 同生成代码 // 构建私钥规格 ECPrivateKeySpec privateKeySpec new ECPrivateKeySpec(d, sm2Spec); // 使用BC的KeyFactory生成私钥对象 KeyFactory keyFactory KeyFactory.getInstance(EC, BouncyCastleProvider.PROVIDER_NAME); return keyFactory.generatePrivate(privateKeySpec); }4. 后端SM2加解密核心实现4.1 加密实现详解SM2加密不是简单的“公钥加密明文”它本质上是一种“集成加密方案”内部包含了密钥派生和对称加密等步骤。BC的Cipher类封装了这些细节我们只需要按标准方式调用。import org.bouncycastle.jce.provider.BouncyCastleProvider; import javax.crypto.Cipher; import java.security.*; import java.util.Base64; Component public class Sm2CryptoService { private static final String ALGORITHM SM2; private static final String PROVIDER BouncyCastleProvider.PROVIDER_NAME; /** * SM2公钥加密 * param publicKeyBase64 Base64编码的公钥字符串 * param plainText 明文 * return Base64编码的密文 */ public String encrypt(String publicKeyBase64, String plainText) throws Exception { // 1. 将Base64公钥字符串还原为PublicKey对象 PublicKey publicKey restorePublicKey(publicKeyBase64); // 2. 获取Cipher实例指定算法和Provider Cipher cipher Cipher.getInstance(ALGORITHM, PROVIDER); // 3. 初始化为加密模式 cipher.init(Cipher.ENCRYPT_MODE, publicKey); // 4. 执行加密明文需先转为字节 byte[] cipherTextBytes cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); // 5. 将密文字节数组转换为Base64字符串返回 return Base64.getEncoder().encodeToString(cipherTextBytes); } /** * 从Base64字符串还原SM2公钥对象 * 这是与前端交互的关键格式必须匹配 */ private PublicKey restorePublicKey(String publicKeyBase64) throws Exception { byte[] publicKeyBytes Base64.getDecoder().decode(publicKeyBase64); // 假设公钥格式为未压缩的04||X||Y共65字节 if (publicKeyBytes.length ! 65 || publicKeyBytes[0] ! 0x04) { throw new IllegalArgumentException(无效的SM2公钥格式预期为65字节未压缩格式(04开头)); } X9ECParameters sm2EcParameters GMNamedCurves.getByName(sm2p256v1); ECCurve curve sm2EcParameters.getCurve(); // 从字节数组中解析出X, Y坐标 BigInteger x new BigInteger(1, Arrays.copyOfRange(publicKeyBytes, 1, 33)); // 第1-32字节是X BigInteger y new BigInteger(1, Arrays.copyOfRange(publicKeyBytes, 33, 65)); // 第33-64字节是Y ECPoint ecPoint curve.createPoint(x, y); ECParameterSpec sm2Spec new ECParameterSpec(...); // 同前 ECPublicKeySpec pubKeySpec new ECPublicKeySpec(ecPoint, sm2Spec); KeyFactory keyFactory KeyFactory.getInstance(EC, PROVIDER); return keyFactory.generatePublic(pubKeySpec); } }加密过程注意事项公钥格式restorePublicKey方法假设前端传过来的公钥是65字节、以0x04开头的未压缩格式。这是sm-crypto库getPublicKey(hex)或getPublicKey(base64)默认输出的格式。务必与前端同学确认公钥格式这是联调成功的第一道关卡。字符编码明文getBytes()必须指定字符集如UTF-8避免在不同环境下因默认编码不同导致加密结果不一致。异常处理加密操作可能抛出BadPaddingException、IllegalBlockSizeException等在生产代码中需要进行妥善的异常处理和日志记录但不应将具体的加密错误细节暴露给前端。4.2 解密实现详解解密是加密的逆过程使用私钥进行。public class Sm2CryptoService { // ... 承接上文 /** * SM2私钥解密 * param privateKeyBase64 Base64编码的私钥字符串从安全处获取 * param cipherTextBase64 Base64编码的密文 * return 解密后的明文 */ public String decrypt(String privateKeyBase64, String cipherTextBase64) throws Exception { // 1. 还原私钥对象 PrivateKey privateKey restorePrivateKey(privateKeyBase64); // 2. 获取Cipher实例 Cipher cipher Cipher.getInstance(ALGORITHM, PROVIDER); // 3. 初始化为解密模式 cipher.init(Cipher.DECRYPT_MODE, privateKey); // 4. 执行解密 byte[] cipherTextBytes Base64.getDecoder().decode(cipherTextBase64); byte[] plainTextBytes cipher.doFinal(cipherTextBytes); // 5. 将明文字节数组按UTF-8编码转为字符串 return new String(plainTextBytes, StandardCharsets.UTF_8); } /** * 从Base64字符串还原SM2私钥对象 */ private PrivateKey restorePrivateKey(String privateKeyBase64) throws Exception { byte[] privateKeyBytes Base64.getDecoder().decode(privateKeyBase64); BigInteger d new BigInteger(1, privateKeyBytes); // 使用1确保为正数 X9ECParameters sm2EcParameters GMNamedCurves.getByName(sm2p256v1); ECParameterSpec sm2Spec new ECParameterSpec(...); // 同前 ECPrivateKeySpec privateKeySpec new ECPrivateKeySpec(d, sm2Spec); KeyFactory keyFactory KeyFactory.getInstance(EC, PROVIDER); return keyFactory.generatePrivate(privateKeySpec); } }解密过程关键点私钥安全privateKeyBase64参数应从安全存储如环境变量中获取而不是由前端传递。后端解密接口通常需要严格的权限控制。密文格式确保接收到的cipherTextBase64是后端encrypt方法生成的标准Base64字符串或与前端约定好的格式。SM2加密后的密文是ASN.1 DER编码的复杂结构BC的Cipher类会处理这个结构的解析。错误处理解密失败最常见的原因是密钥不匹配用错了公钥/私钥对或密文被篡改。返回给前端的错误信息应足够模糊如“解密失败”避免信息泄露。5. 前端JS加密与交互规范5.1 前端库选型与初始化前端我们选择sm-crypto这是一个纯JavaScript实现的国密算法库支持SM2、SM3、SM4在Node.js和浏览器环境都能运行且API设计友好。安装npm install sm-crypto --save # 或 yarn add sm-crypto初始化与密钥生成在前端我们通常不会生成密钥对而是使用后端下发的公钥进行加密。但为了测试和演示这里也展示一下前端的密钥生成。import { sm2 } from sm-crypto; // 1. 生成密钥对通常由后端生成前端仅用于测试 const keypair sm2.generateKeyPairHex(); const publicKey keypair.publicKey; // 04开头的16进制公钥字符串 const privateKey keypair.privateKey; // 16进制私钥字符串 console.log(公钥:, publicKey); console.log(私钥:, privateKey); // 注意前端不应保存或使用私钥 // 2. 后端下发的公钥假设是Base64格式需要转换 // 假设从接口获取的公钥Base64字符串为 backendPublicKeyBase64 import { Base64 } from js-base64; // 可能需要引入Base64库 const backendPublicKeyBase64 MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEL...; // 示例 // 将Base64解码为字节再转为16进制sm-crypto的encrypt方法通常接受16进制公钥 const backendPublicKeyHex Buffer.from(backendPublicKeyBase64, base64).toString(hex); // 或者如果后端直接提供16进制公钥字符串则更简单重要提示在实际生产环境中前端绝对不应该生成或持有私钥。私钥的生成、保管和使用必须完全在后端或硬件设备的安全环境中进行。前端只负责用公钥加密数据。5.2 前端加密与数据发送前端加密的核心是使用sm-crypto的sm2.doEncrypt方法。这里有一个至关重要的细节加密模式。sm-crypto默认使用C1C3C2的ASN.1编码格式而BC库默认可能使用C1C2C3或其他格式。如果格式不匹配后端解密必定失败。经过实测确保前后端一致的配置如下// 前端加密函数 function encryptData(plainText, publicKeyHex) { // 关键参数cipherMode 0 表示输出为C1C3C2顺序的ASN.1 DER编码密文 // 这个模式与后端Bouncy Castle的默认解密期望格式兼容 const cipherMode 0; const encryptedDataHex sm2.doEncrypt(plainText, publicKeyHex, cipherMode); // 将16进制密文转换为Base64便于在JSON中传输 const encryptedDataBase64 Buffer.from(encryptedDataHex, hex).toString(base64); return encryptedDataBase64; } // 使用示例 const dataToEncrypt JSON.stringify({ userId: 12345, timestamp: Date.now() }); const encryptedBase64 encryptData(dataToEncrypt, backendPublicKeyHex); // 将加密后的数据作为请求体发送 fetch(/api/secure-endpoint, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ cipherText: encryptedBase64 }) }) .then(response response.json()) .then(data console.log(响应:, data));核心要点cipherMode 0这是与Java BC库兼容的关键。sm-crypto的doEncrypt方法第二个参数是公钥第三个参数是模式。模式0代表C1C3C2的ASN.1 DER编码这是目前与BC默认解密器兼容性最好的选择。务必与后端确认并使用此模式。编码转换sm-crypto加密输出的是16进制字符串而网络传输中Base64更常用体积更小。使用Buffer或js-base64库进行转换。明文格式通常我们会将需要加密的数据如一个JSON对象先序列化成字符串再加密。确保前后端对明文字符串的编码UTF-8有共识。5.3 后端接收与解密处理后端提供一个RESTful接口来接收前端加密的数据。import org.springframework.web.bind.annotation.*; RestController RequestMapping(/api) Slf4j public class CryptoController { Autowired private Sm2CryptoService sm2CryptoService; // 假设私钥已从安全位置加载到这个变量中 Value(${sm2.private-key-base64}) private String serverPrivateKeyBase64; PostMapping(/secure-endpoint) public ResponseEntityMapString, Object handleEncryptedRequest(RequestBody EncryptedRequest request) { MapString, Object response new HashMap(); try { // 1. 获取前端传来的Base64密文 String cipherTextBase64 request.getCipherText(); // 2. 使用服务端私钥解密 String decryptedText sm2CryptoService.decrypt(serverPrivateKeyBase64, cipherTextBase64); // 3. 将解密后的字符串解析为业务对象 MyBusinessDTO businessData objectMapper.readValue(decryptedText, MyBusinessDTO.class); log.info(解密成功业务数据: {}, businessData); // 4. 处理业务逻辑... // ... // 5. 构造响应如需返回加密数据则用前端公钥加密 response.put(success, true); response.put(data, 处理成功); return ResponseEntity.ok(response); } catch (Exception e) { log.error(请求处理失败解密或业务逻辑错误, e); // 注意不要将具体的异常信息如解密失败详情返回给前端以防信息泄露 response.put(success, false); response.put(message, 请求处理失败); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); } } // 用于接收加密请求的DTO Data public static class EncryptedRequest { private String cipherText; } }6. 联调实战、常见问题与排查技巧6.1 联调核对清单前后端联调SM2加密时请按以下清单逐一核对可以解决90%的问题检查项后端 (Java Bouncy Castle)前端 (JavaScript sm-crypto)必须一致椭圆曲线sm2p256v1(OID: 1.2.156.10197.1.301)默认即为国密SM2曲线✅公钥格式65字节未压缩04X私钥格式大整数D的字节数组Base64存储仅用于后端前端不持有-加密模式Cipher默认模式 (通常对应C1C3C2 ASN.1)sm2.doEncrypt(msg, key, 0)第三个参数必须为0✅数据编码明文/密文转换使用UTF-8明文使用TextEncoder或确保UTF-8密文Hex/Base64转换✅传输格式接收/发送Base64字符串发送加密后的Base64字符串✅6.2 典型错误与解决方案问题1后端解密失败抛出InvalidCipherTextException或类似异常。可能原因A公钥不匹配。前端用于加密的公钥与后端解密使用的私钥不是一对。排查让后端打印用于解密的私钥对应的公钥Base64与前端用于加密的公钥进行比对。确保完全一致。可能原因B加密模式不匹配。前端sm-crypto加密时未指定模式或模式错误。解决前端加密必须使用sm2.doEncrypt(plainText, publicKeyHex, 0)。第三个参数0是联调成功的黄金法则。可能原因C密文在传输过程中被篡改或编码错误。排查后端收到密文Base64字符串后先尝试Base64解码看是否能正常解码为字节数组。同时对比前端发送的Base64字符串和后端接收到的字符串是否完全相同注意URL编码问题如果放在URL中可能需要encode/decode。问题2后端能解密但解密出的明文是乱码。可能原因前后端字符编码不一致。解决后端在加密时将字符串转为字节数组时显式指定plainText.getBytes(StandardCharsets.UTF_8)。解密后用new String(plainBytes, StandardCharsets.UTF_8)。前端确保加密前的字符串是UTF-8编码。问题3前端加密时公钥格式错误。现象sm-crypto报错提示无效的公钥。排查检查后端提供给前端的公钥字符串。如果是Base64前端需要先将其解码为16进制。确保公钥字符串以04开头如果是16进制且长度正确130个16进制字符对应65字节。问题4性能问题加密大量数据慢。原因SM2作为非对称加密不适合加密大数据量。通常用于加密对称加密的密钥如AES密钥或者加密非常短的数据如票据、签名。最佳实践采用“混合加密”体系。前端随机生成一个AES密钥key。使用这个AES密钥通过SM4或AES算法加密实际的大数据data。使用后端的SM2公钥加密上一步生成的AES密钥encryptedKey。将{ cipherData: encryptedData, encryptedKey: encryptedKey }发送给后端。后端先用SM2私钥解密出AES密钥再用该密钥解密数据。6.3 进阶技巧签名与验签除了加密SM2还用于数字签名。流程类似但目的不同签名是为了验证数据的完整性和来源真实性。后端签名public String sign(PrivateKey privateKey, String data) throws Exception { Signature signature Signature.getInstance(SM3withSM2, BouncyCastleProvider.PROVIDER_NAME); signature.initSign(privateKey); signature.update(data.getBytes(StandardCharsets.UTF_8)); byte[] signBytes signature.sign(); return Base64.getEncoder().encodeToString(signBytes); }前端验签import { sm2 } from sm-crypto; const publicKeyHex ...; // 后端公钥 const originalData 待验签数据; const signBase64 ...; // 后端传来的签名 const signHex Buffer.from(signBase64, base64).toString(hex); const verifyResult sm2.doVerifySignature(originalData, signHex, publicKeyHex); console.log(验签结果:, verifyResult); // true or false签名验签注意事项签名算法名是SM3withSM2表示使用SM3做摘要SM2做签名。签名的结果也是ASN.1 DER编码的直接Base64传输即可。前端sm-crypto的doVerifySignature方法默认支持这种格式的签名。7. 项目集成与生产级考量7.1 配置化与Bean管理在实际Spring Boot项目中我们不应在每次加解密时都去读取环境变量或重建密钥对象。最佳实践是使用ConfigurationProperties或Value将密钥注入并在应用启动时初始化好Sm2CryptoServiceBean。Configuration Slf4j public class Sm2AutoConfiguration { Bean ConditionalOnMissingBean public Sm2CryptoService sm2CryptoService( Value(${sm2.private-key-base64:#{null}}) String privateKeyBase64, Value(${sm2.public-key-base64:#{null}}) String publicKeyBase64) throws Exception { Sm2CryptoService service new Sm2CryptoService(); if (privateKeyBase64 ! null !privateKeyBase64.trim().isEmpty()) { // 初始化服务端私钥用于解密和签名 PrivateKey privateKey service.restorePrivateKey(privateKeyBase64); service.setServerPrivateKey(privateKey); log.info(SM2服务端私钥已加载。); } if (publicKeyBase64 ! null !publicKeyBase64.trim().isEmpty()) { // 初始化服务端公钥用于验签如果前端需要验签的话 PublicKey publicKey service.restorePublicKey(publicKeyBase64); service.setServerPublicKey(publicKey); log.info(SM2服务端公钥已加载。); } else { // 或者可以在这里生成一对新的密钥对 // KeyPair keyPair new Sm2KeyGenerator().generateSm2KeyPair(); // service.setServerKeyPair(keyPair); // log.warn(未配置SM2公钥已自动生成新密钥对。请妥善保管私钥。); } return service; } }然后在application.yml中配置sm2: # 从安全环境变量中读取不要明文写在配置文件中 private-key-base64: ${SM2_SERVER_PRIVATE_KEY} # 公钥可以配置也可以由私钥推导或动态生成 public-key-base64: ${SM2_SERVER_PUBLIC_KEY}7.2 封装为Starter或通用模块如果公司内有多个项目需要使用SM2强烈建议将上述Sm2CryptoService、Sm2KeyGenerator、配置类以及相关的工具类如密钥格式转换打包成一个独立的Spring Boot Starter或者一个通用的Jar包。这样可以统一实现确保所有项目使用相同、正确的BC配置和交互规范。降低接入成本其他项目只需引入依赖简单配置即可使用。便于升级和维护算法库版本升级、安全补丁应用只需修改一个地方。7.3 监控与日志在生产环境中加解密操作应该被妥善监控和记录。日志记录加解密操作的摘要信息如操作类型、数据ID但绝对不要记录明文、密文、密钥等敏感信息到日志文件。监控监控加解密接口的调用频率、耗时和错误率。异常的解密失败请求突然增多可能是遭受攻击的迹象。性能非对称加密是CPU密集型操作。在高并发场景下需要关注服务器的CPU使用率必要时考虑使用连接池化技术或硬件加速卡。7.4 密钥轮换与多版本支持为了安全密钥需要定期轮换。在设计中需要考虑密钥版本化每个密钥对有一个版本号如v1,v2。加密时可以在密文或请求头中附带使用的公钥版本号。后端多密钥支持后端根据版本号从密钥库中加载对应的私钥进行解密。这样旧版本密钥在轮换后仍能解密历史数据新数据则用新密钥加密。前端动态获取公钥前端不应硬编码公钥而应该从一个安全的接口动态获取当前活跃的公钥及其版本号。这为密钥轮换提供了可能。实现这套方案后你的Spring Boot应用就具备了符合国密标准、且能与前端安全交互的SM2加解密能力。整个过程的核心在于对Bouncy Castle库的正确使用以及对前后端数据格式、编码、模式的严格约定。联调阶段耐心按照核对清单排查就能顺利打通。这套方案已经在我们多个对数据安全有严格要求的项目中稳定运行希望能为你提供可靠的参考。