> 技术文档 > Spring Boot中使用Bouncy Castle实现SM2国密算法(与前端JS加密交互)_spring国密 sm2

Spring Boot中使用Bouncy Castle实现SM2国密算法(与前端JS加密交互)_spring国密 sm2


Spring Boot中使用Bouncy Castle实现SM2国密算法(与前端JS加密交互)

    • 一、环境准备
    • 二、核心实现
    • 三、前后端交互流程
    • 四、关键问题解决方案
    • 五、常见问题排查
    • 六、最佳实践建议

在这里插入图片描述

在现代Web应用中,数据安全传输至关重要。SM2作为我国自主设计的非对称加密算法,在安全性、效率和合规性方面具有显著优势。本文将详细介绍如何在Spring Boot中集成SM2算法,实现与前端JS的无缝加密交互。

一、环境准备

技术栈:

  • Java 1.8
  • Spring Boot 2.1.18
  • Bouncy Castle 1.68+
  • 前端:sm-crypto或类似库

Maven核心依赖:

<dependencies> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.68</version> </dependency></dependencies>

二、核心实现

  1. 密钥生成服务
@RestController@RequestMapping(\"/sm2\")public class SM2Controller { @GetMapping(\"/keypair\") public Map<String, String> generateKeyPair() throws Exception { KeyPair keyPair = SM2CryptoUtil.generateKeyPair(); ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic(); ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate(); String publicKeyHex = Hex.toHexString(publicKey.getQ().getEncoded(false)); String privateKeyHex = privateKey.getD().toString(16); // 标准化私钥格式(64字符) privateKeyHex = String.format(\"%64s\", privateKeyHex).replace(\' \', \'0\'); return Map.of( \"publicKey\", publicKeyHex, // 130字符带04前缀 \"privateKey\", privateKeyHex // 64字符 ); }}
  1. SM2解密服务
import org.bouncycastle.asn1.gm.GMNamedCurves;import org.bouncycastle.asn1.x9.X9ECParameters;import org.bouncycastle.crypto.digests.SM3Digest;import org.bouncycastle.crypto.params.ECDomainParameters;import org.bouncycastle.jce.ECNamedCurveTable;import org.bouncycastle.jce.provider.BouncyCastleProvider;import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;import org.bouncycastle.math.ec.ECPoint;import org.bouncycastle.util.BigIntegers;import org.bouncycastle.util.encoders.Hex;import java.math.BigInteger;import java.security.KeyPair;import java.security.KeyPairGenerator;import java.security.SecureRandom;import java.security.Security;import java.util.Arrays;/** * @author cmamg * @title: Base64Util * @projectName * @description: TODO * @date 2025/7/29 */public class SM2CryptoUtil { // 加密模式常量 public static final int C1C2C3 = 0; public static final int C1C3C2 = 1; // 椭圆曲线参数 private static final X9ECParameters EC_PARAMS; private static final ECDomainParameters DOMAIN_PARAMS; private static final BigInteger CURVE_ORDER; static { Security.addProvider(new BouncyCastleProvider()); EC_PARAMS = GMNamedCurves.getByName(\"sm2p256v1\"); DOMAIN_PARAMS = new ECDomainParameters( EC_PARAMS.getCurve(), EC_PARAMS.getG(), EC_PARAMS.getN(), EC_PARAMS.getH()); CURVE_ORDER = EC_PARAMS.getN(); } /** * 生成SM2密钥对 */ public static KeyPair generateKeyPair() throws Exception { ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec(\"sm2p256v1\"); KeyPairGenerator kpg = KeyPairGenerator.getInstance(\"EC\", \"BC\"); kpg.initialize(spec, new SecureRandom()); return kpg.generateKeyPair(); } /** * 获取压缩公钥十六进制字符串 */ public static String getCompressedPublicKey(ECPoint publicKey) { byte[] compressed = publicKey.getEncoded(true); return Hex.toHexString(compressed); } /** * 获取未压缩公钥十六进制字符串(不带04前缀) */ public static String getUncompressedPublicKey(ECPoint publicKey) { byte[] uncompressed = publicKey.getEncoded(false); // 去掉开头的04标识 return Hex.toHexString(uncompressed); } /** * 从十六进制字符串解析公钥 */ public static ECPoint parsePublicKey(String publicKeyHex) { // 添加04前缀表示未压缩格式 byte[] pubKeyBytes = Hex.decode( publicKeyHex); return DOMAIN_PARAMS.getCurve().decodePoint(pubKeyBytes); } private static BigInteger parsePrivateKey(String privateKeyHex) { if (privateKeyHex == null || privateKeyHex.length() != 64) { throw new IllegalArgumentException(\"私钥必须是64字符十六进制字符串\"); } try { BigInteger privateKey = new BigInteger(privateKeyHex, 16); // 验证私钥范围 [1, n-1] if (privateKey.signum() <= 0 || privateKey.compareTo(CURVE_ORDER) >= 0) { throw new IllegalArgumentException(\"私钥超出有效范围\"); } return privateKey; } catch (NumberFormatException e) { throw new IllegalArgumentException(\"无效的私钥格式\", e); } } public static String decryptStr(String ciphertextHex, String privateKeyHex) throws Exception { return new String(decrypt(ciphertextHex,privateKeyHex, 1), \"UTF-8\"); } /** * SM2解密 */ public static byte[] decrypt(String ciphertextHex, String privateKeyHex, int cipherMode) throws Exception { // 1. 验证并解析私钥 BigInteger privateKey = parsePrivateKey(privateKeyHex); // 2. 解析密文 byte[] ciphertext = Hex.decode(ciphertextHex); // 验证最小长度 = C1(64) + C3(32) = 96字节 if (ciphertext.length < 96) { throw new IllegalArgumentException(\"密文太短\"); } // 3. 拆分密文 byte[] c1 = Arrays.copyOfRange(ciphertext, 0, 64); // 64字节 byte[] c3; byte[] c2; if (cipherMode == C1C2C3) { // C1C2C3模式: C1(64) + C2 + C3(32) c3 = Arrays.copyOfRange(ciphertext, ciphertext.length - 32, ciphertext.length); c2 = Arrays.copyOfRange(ciphertext, 64, ciphertext.length - 32); } else { // C1C3C2模式: C1(64) + C3(32) + C2 c3 = Arrays.copyOfRange(ciphertext, 64, 96); c2 = Arrays.copyOfRange(ciphertext, 96, ciphertext.length); } // 4. 重建C1点 byte[] c1Full = new byte[65]; // 04 + 64字节 c1Full[0] = 0x04; // 添加未压缩标识 System.arraycopy(c1, 0, c1Full, 1, 64); ECPoint c1Point; try { c1Point = DOMAIN_PARAMS.getCurve().decodePoint(c1Full); } catch (Exception e) { throw new IllegalArgumentException(\"无效的C1点\", e); } // 5. 计算共享点 (x2, y2) = privateKey * C1 ECPoint s = c1Point.multiply(privateKey).normalize(); // 验证点是否在曲线上 if (!s.isValid()) { throw new SecurityException(\"计算出的点不在曲线上\"); } byte[] x2 = BigIntegers.asUnsignedByteArray(32, s.getXCoord().toBigInteger()); byte[] y2 = BigIntegers.asUnsignedByteArray(32, s.getYCoord().toBigInteger()); // 6. KDF生成密钥流 byte[] z = new byte[x2.length + y2.length]; System.arraycopy(x2, 0, z, 0, x2.length); System.arraycopy(y2, 0, z, x2.length, y2.length); byte[] t = kdf(z, c2.length); // 7. 异或解密 byte[] msg = new byte[c2.length]; for (int i = 0; i < c2.length; i++) { msg[i] = (byte) (c2[i] ^ t[i]); } // 8. 验证C3 byte[] u = new byte[x2.length + msg.length + y2.length]; System.arraycopy(x2, 0, u, 0, x2.length); System.arraycopy(msg, 0, u, x2.length, msg.length); System.arraycopy(y2, 0, u, x2.length + msg.length, y2.length); byte[] calculatedC3 = sm3(u); if (!Arrays.equals(c3, calculatedC3)) { throw new SecurityException(\"C3验证失败: 数据可能被篡改或密钥错误\"); } return msg; } /** * KDF密钥派生函数 */ private static byte[] kdf(byte[] z, int keylen) { int ct = 1; int offset = 0; byte[] result = new byte[keylen]; SM3Digest digest = new SM3Digest(); while (offset < keylen) { // 准备计数器字节 byte[] ctBytes = new byte[]{ (byte) (ct >>> 24), (byte) (ct >>> 16), (byte) (ct >>> 8), (byte) ct }; // 计算SM3哈希 digest.update(z, 0, z.length); digest.update(ctBytes, 0, 4); byte[] hash = new byte[digest.getDigestSize()]; digest.doFinal(hash, 0); // 填充结果 int copyLen = Math.min(keylen - offset, hash.length); System.arraycopy(hash, 0, result, offset, copyLen); offset += copyLen; ct++; digest.reset(); } return result; } /** * SM3哈希计算 */ private static byte[] sm3(byte[] input) { SM3Digest digest = new SM3Digest(); digest.update(input, 0, input.length); byte[] hash = new byte[digest.getDigestSize()]; digest.doFinal(hash, 0); return hash; }}
  1. 前端加密示例
import { sm2 } from \'sm-crypto\';// 使用后端生成的公钥(130字符带04前缀)const publicKey = \'04d4de...\'; function encryptMessage(message) { // 使用C1C3C2模式加密 const ciphertext = sm2.doEncrypt( message, publicKey, 1 // cipherMode=1 表示C1C3C2 ); return ciphertext; // 十六进制字符串}// 调用示例const encrypted = encryptMessage(\'敏感数据123\');

三、前后端交互流程

密钥获取:

GET /sm2/keypairResponse: { \"publicKey\": \"04...\", \"privateKey\": \"a1b2...\" }

前端加密:

const ciphertext = sm2.doEncrypt(data, publicKey, 1);

后端解密:

POST /sm2/decrypt{ \"ciphertext\": \"a1b2c3...\", \"privateKey\": \"a1b2...\", \"mode\": 1}

四、关键问题解决方案

  1. 公钥格式一致性
    前端要求公钥带04前缀(未压缩格式),后端需确保:
public String getPublicKeyHex(ECPoint publicKey) { return Hex.toHexString(publicKey.getEncoded(false)); // 带04前缀}
  1. 私钥范围验证
    防止Scalar not in interval错误:
private static final BigInteger CURVE_ORDER = EC_PARAMS.getN();if (privateKey.signum() <= 0 || privateKey.compareTo(CURVE_ORDER) >= 0) { throw new IllegalArgumentException(\"无效私钥范围\");}
  1. C1点重建
    前端密文中的C1点不带04前缀,后端需重建:
byte[] c1Full = new byte[65];c1Full[0] = 0x04; // 添加前缀System.arraycopy(c1, 0, c1Full, 1, 64);

五、常见问题排查

错误现象 可能原因 解决方案
Scalar not in interval 私钥格式错误或越界 验证私钥长度64字符,值在[1, n-1]范围内
C3验证失败 密钥错误或数据篡改 检查公私钥配对,重试加密流程
无效的C1点 密文格式错误 确认使用C1C3C2模式,检查密文长度
解密乱码 编码不一致 统一使用UTF-8编码

六、最佳实践建议

密钥管理:

前端不存储私钥

后端使用HSM或KMS管理私钥

定期轮换密钥

性能优化:

// 重用SM3Digest实例private static final ThreadLocal<SM3Digest> sm3Cache = ThreadLocal.withInitial(SM3Digest::new);

安全增强:

// 防止时序攻击if (!MessageDigest.isEqual(c3, calculatedC3)) { throw new SecurityException(\"C3验证失败\");}

七、总结
本文实现了Spring Boot中完整的SM2算法集成方案,重点解决了:

密钥生成与格式标准化

与前端JS的加密交互

解密过程中的异常处理

通过此方案,开发者可以快速构建符合国密标准的安全应用,确保数据传输的机密性和完整性。在实际业务中,建议结合HTTPS等传输层安全措施,构建纵深防御体系。

股市资讯