vue + springboot 实现国密SM2加密
一、国密SM2加密算法简介
SM2是中国国家密码管理局发布的椭圆曲线公钥密码算法标准,用于替代传统的RSA算法。
1.1 核心特点
- 高安全性:基于椭圆曲线离散对数问题(ECDLP),目前无有效破解算法
- 密钥长度短:256位密钥提供相当于RSA 2048位的安全强度
- 性能优异:计算速度比RSA快,尤其在签名验证方面
- 国密标准:符合中国国家密码管理局的加密标准,适用于国内敏感信息处理
1.2 基本原理
SM2使用公钥密码体制,通过椭圆曲线上的点运算实现加密解密:
- 密钥对:公钥(公开)和私钥(保密)组成,公钥由私钥通过椭圆曲线点乘法生成
- 加密:使用接收方公钥加密数据,生成包含椭圆曲线点和加密数据的密文
- 解密:使用接收方私钥解密数据,恢复原始明文
1.3 应用场景
- 敏感数据加密传输与存储
- 数字签名与身份认证
- 安全密钥交换
- 系统登录与访问控制
二、基于BouncyCastle实现SM2完整代码
2.1 项目架构
本次实现采用前后端分离架构:
- 后端:Spring Boot + BouncyCastle
- 前端:Vue 3 + Pinia + sm-crypto-v2
2.2 后端核心实现(SmCryptoUtil.java)
package com.example.sm2demo.util;
import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.params.*;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64.Decoder;
import java.util.Base64.Encoder;
public class SmCryptoUtil {
private static final Logger logger = LoggerFactory.getLogger(SmCryptoUtil.class);
private static final String ALGORITHM_NAME = "SM2";
private static final String PROVIDER_NAME = BouncyCastleProvider.PROVIDER_NAME;
private static final String CURVE_NAME = "sm2p256v1";
private static final Encoder base64Encoder = java.util.Base64.getEncoder();
private static final Decoder base64Decoder = java.util.Base64.getDecoder();
// 静态初始化BouncyCastleProvider
static {
if (Security.getProvider(PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
/**
* 生成SM2密钥对
* @return KeyPair对象,包含公钥和私钥
*/
public static KeyPair generateKeyPair() {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM_NAME, PROVIDER_NAME);
ECGenParameterSpec ecGenParameterSpec = new ECGenParameterSpec(CURVE_NAME);
keyPairGenerator.initialize(ecGenParameterSpec);
return keyPairGenerator.generateKeyPair();
} catch (Exception e) {
logger.error("生成SM2密钥对失败: {}", e.getMessage(), e);
throw new RuntimeException("生成SM2密钥对失败", e);
}
}
/**
* 获取十六进制格式的公钥
* @param publicKey 公钥对象
* @return 十六进制格式的公钥字符串
*/
public static String getPublicKeyHex(PublicKey publicKey) {
BCECPublicKey bcecPublicKey = (BCECPublicKey) publicKey;
ECPoint ecPoint = bcecPublicKey.getQ();
byte[] encoded = ecPoint.getEncoded(false); // false表示不压缩格式,包含0x04前缀
return Hex.toHexString(encoded);
}
/**
* 获取Base64格式的私钥
* @param privateKey 私钥对象
* @return Base64格式的私钥字符串
*/
public static String getPrivateKeyBase64(PrivateKey privateKey) {
return base64Encoder.encodeToString(privateKey.getEncoded());
}
/**
* SM2解密方法
* @param encryptedText 密文字符串
* @param privateKeyBase64 Base64格式的私钥
* @return 解密后的明文字符串
*/
public static String sm2Decrypt(String encryptedText, String privateKeyBase64) {
if (encryptedText == null || encryptedText.isEmpty()) {
throw new IllegalArgumentException("密文数据不能为空");
}
if (privateKeyBase64 == null || privateKeyBase64.isEmpty()) {
throw new IllegalArgumentException("私钥不能为空");
}
try {
logger.info("=== 开始SM2解密流程 ===");
logger.info("密文原始字符串长度: {} 字符", encryptedText.length());
logger.info("密文前50字符: {}", encryptedText.substring(0, Math.min(50, encryptedText.length())));
logger.info("密文后50字符: {}", encryptedText.substring(Math.max(0, encryptedText.length() - 50)));
logger.info("私钥长度: {} 字符", privateKeyBase64.length());
// 使用Bouncy Castle实现SM2解密
byte[] encryptedData = Hex.decode(encryptedText);
byte[] privateKeyData = base64Decoder.decode(privateKeyBase64);
X9ECParameters ecParameters = GMNamedCurves.getByName(CURVE_NAME);
ECDomainParameters domainParameters = new ECDomainParameters(
ecParameters.getCurve(),
ecParameters.getG(),
ecParameters.getN(),
ecParameters.getH(),
ecParameters.getSeed());
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyData);
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_NAME, PROVIDER_NAME);
BCECPrivateKey bcecPrivateKey = (BCECPrivateKey) keyFactory.generatePrivate(pkcs8EncodedKeySpec);
BigInteger privateKeyD = bcecPrivateKey.getD();
ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(privateKeyD, domainParameters);
SM2Engine sm2Engine = new SM2Engine(new SM3Digest());
sm2Engine.init(false, privateKeyParameters);
// 检查密文格式并修复
if (encryptedData.length > 0 && encryptedData[0] != 0x04) {
logger.warn("检测到密文没有0x04前缀,这可能是sm-crypto库的加密输出格式问题");
if (encryptedData.length >= 97) {
// 分离C1、C3、C2组件(假设是C1C3C2格式)
byte[] c1WithoutPrefix = Arrays.copyOfRange(encryptedData, 0, 64);
byte[] c3 = Arrays.copyOfRange(encryptedData, 64, 96);
byte[] c2 = Arrays.copyOfRange(encryptedData, 96, encryptedData.length);
// 为C1添加0x04前缀
byte[] c1WithPrefix = new byte[65];
c1WithPrefix[0] = 0x04;
System.arraycopy(c1WithoutPrefix, 0, c1WithPrefix, 1, 64);
// 重新组装密文
byte[] fixedEncryptedData = new byte[65 + 32 + c2.length];
System.arraycopy(c1WithPrefix, 0, fixedEncryptedData, 0, 65);
System.arraycopy(c3, 0, fixedEncryptedData, 65, 32);
System.arraycopy(c2, 0, fixedEncryptedData, 97, c2.length);
encryptedData = fixedEncryptedData;
logger.info("成功修复密文格式:");
logger.info(" 修复前长度: {} 字节", encryptedData.length - 1);
logger.info(" 修复后长度: {} 字节", encryptedData.length);
logger.info(" 修复后第一个字节: 0x{}", Integer.toHexString(encryptedData[0]));
}
}
byte[] decryptedData = sm2Engine.processBlock(encryptedData, 0, encryptedData.length);
String decryptedText = new String(decryptedData, StandardCharsets.UTF_8);
logger.info("Bouncy Castle解密成功,密文长度: {},原文长度: {}",
encryptedText.length(), decryptedText.length());
logger.info("=== SM2解密流程结束(成功)===");
return decryptedText;
} catch (InvalidCipherTextException e) {
logger.error("SM2解密失败 - 无效密文: {}", e.getMessage(), e);
throw new RuntimeException("SM2解密失败 - 无效密文", e);
} catch (Exception e) {
logger.error("SM2解密失败: {}", e.getMessage(), e);
throw new RuntimeException("SM2解密失败", e);
}
}
}
2.3 前端核心实现(sm2Store.js)
import { defineStore } from 'pinia'
import { sm2 } from 'sm-crypto-v2'
export const useSm2Store = defineStore('sm2', {
state: () => ({
publicKey: '',
privateKey: '', // 实际项目中私钥不应从前端获取
encryptedData: '',
decryptedData: '',
plainText: '',
isLoading: false,
error: ''
}),
actions: {
/**
* 获取SM2公钥
*/
async fetchPublicKey() {
this.isLoading = true
this.error = ''
try {
const response = await fetch('http://localhost:8080/public-key-hex')
if (!response.ok) {
throw new Error('获取公钥失败')
}
this.publicKey = await response.text()
console.log('获取公钥成功:', this.publicKey)
console.log('公钥长度:', this.publicKey.length)
} catch (error) {
this.error = `获取公钥失败: ${error.message}`
console.error(this.error)
} finally {
this.isLoading = false
}
},
/**
* 加密数据
*/
encryptData() {
if (!this.plainText) {
this.error = '请输入要加密的明文'
return
}
if (!this.publicKey) {
this.error = '请先获取公钥'
return
}
this.isLoading = true
this.error = ''
this.encryptedData = ''
try {
console.log('开始加密,明文:', this.plainText)
console.log('公钥:', this.publicKey)
// 使用sm-crypto-v2库进行SM2加密,返回C1C3C2格式
const encryptedText = sm2.encrypt(this.plainText, this.publicKey, {
mode: 1 // 1表示C1C3C2格式
})
this.encryptedData = encryptedText
console.log('加密成功:', this.encryptedData)
} catch (error) {
this.error = `加密失败: ${error.message}`
console.error(this.error)
} finally {
this.isLoading = false
}
},
/**
* 本地解密数据(不再调用后端接口)
*/
decryptData() {
if (!this.encryptedData) {
this.error = '请先加密数据'
return
}
if (!this.privateKey) {
this.error = '解密需要私钥'
return
}
this.isLoading = true
this.error = ''
this.decryptedData = ''
try {
console.log('开始本地解密,密文:', this.encryptedData)
console.log('私钥:', this.privateKey)
// 使用sm-crypto-v2库进行SM2本地解密
const decryptedText = sm2.decrypt(this.encryptedData, this.privateKey, {
mode: 1 // 1表示C1C3C2格式
})
this.decryptedData = decryptedText
console.log('本地解密成功:', this.decryptedData)
} catch (error) {
this.error = `解密失败: ${error.message}`
console.error(this.error)
} finally {
this.isLoading = false
}
}
}
})
2.4 后端API实现(Sm2Controller.java)
package com.example.sm2demo.controller;
import com.example.sm2demo.util.SmCryptoUtil;
import org.springframework.web.bind.annotation.*;
import java.security.KeyPair;
import java.security.PublicKey;
@RestController
@RequestMapping("/")
public class Sm2Controller {
// 全局密钥对(实际项目中应使用更安全的密钥管理方式)
private static final KeyPair keyPair;
private static final PublicKey publicKey;
private static final String publicKeyHex;
static {
keyPair = SmCryptoUtil.generateKeyPair();
publicKey = keyPair.getPublic();
publicKeyHex = SmCryptoUtil.getPublicKeyHex(publicKey);
}
/**
* 获取十六进制格式的公钥
* @return 十六进制格式的公钥字符串
*/
@GetMapping("/public-key-hex")
public String getPublicKeyHex() {
return publicKeyHex;
}
}
三、项目依赖配置
3.1 后端依赖(pom.xml)
<dependencies>
<!-- Spring Boot 核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Bouncy Castle 加密库 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.83</version>
</dependency>
<!-- 日志依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
</dependencies>
3.2 前端依赖(package.json)
{
"name": "sm2-frontend",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.3.4",
"pinia": "^2.1.6",
"sm-crypto-v2": "^1.15.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.3.4",
"vite": "^4.4.9"
}
}
四、常见问题分析与解决方案
4.1 "Invalid point encoding"错误
问题现象: 解密时出现"Invalid point encoding"错误。
原因: SM2公钥或密文缺少0x04前缀(椭圆曲线非压缩点标识)。
解决方案:
- 确保公钥使用非压缩格式,包含0x04前缀
- 验证密文格式正确(这是重中之重),必要时自动修复(如代码中实现的密文修复逻辑)
4.2 "Cannot read properties of null (reading ‘multiply’)"错误
问题现象: 前端加密时出现此错误。
原因: 公钥格式不正确或为空。
解决方案:
- 确保公钥是有效的十六进制格式,长度为130字符(包含0x04前缀)
- 添加公钥格式验证逻辑
4.3 加密模式不匹配问题
问题现象: 密文解密失败。
原因: 加密和解密使用了不同的密文格式(C1C3C2 vs C1C2C3)。
解决方案:
- 前后端统一使用C1C3C2格式(新国密标准)
- 在sm-crypto-v2中通过mode参数指定格式
到此这篇关于vue + springboot 实现国密SM2加密的文章就介绍到这了,更多相关vue springboot国密SM2加密内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
详解Vue+Element的动态表单,动态表格(后端发送配置,前端动态生成)
这篇文章主要介绍了Vue+Element动态表单动态表格,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧2019-04-04
vue项目使用electron-builder库打包成桌面程序的过程
这篇文章主要介绍了vue项目使用electron-builder库打包成桌面程序的过程,本文给大家介绍如何使用electron-builder这个库结合实例代码给大家讲解的非常详细,感兴趣的朋友一起看看吧2024-02-02
vue3.0+vue-router+element-plus初实践
这篇文章主要介绍了vue3.0+vue-router+element-plus初实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧2020-12-12


最新评论