vue + springboot 實(shí)現(xiàn)國(guó)密SM2加密
一、國(guó)密SM2加密算法簡(jiǎn)介
SM2是中國(guó)國(guó)家密碼管理局發(fā)布的橢圓曲線公鑰密碼算法標(biāo)準(zhǔn),用于替代傳統(tǒng)的RSA算法。
1.1 核心特點(diǎn)
- 高安全性:基于橢圓曲線離散對(duì)數(shù)問(wèn)題(ECDLP),目前無(wú)有效破解算法
- 密鑰長(zhǎng)度短:256位密鑰提供相當(dāng)于RSA 2048位的安全強(qiáng)度
- 性能優(yōu)異:計(jì)算速度比RSA快,尤其在簽名驗(yàn)證方面
- 國(guó)密標(biāo)準(zhǔn):符合中國(guó)國(guó)家密碼管理局的加密標(biāo)準(zhǔn),適用于國(guó)內(nèi)敏感信息處理
1.2 基本原理
SM2使用公鑰密碼體制,通過(guò)橢圓曲線上的點(diǎn)運(yùn)算實(shí)現(xiàn)加密解密:
- 密鑰對(duì):公鑰(公開(kāi))和私鑰(保密)組成,公鑰由私鑰通過(guò)橢圓曲線點(diǎn)乘法生成
- 加密:使用接收方公鑰加密數(shù)據(jù),生成包含橢圓曲線點(diǎn)和加密數(shù)據(jù)的密文
- 解密:使用接收方私鑰解密數(shù)據(jù),恢復(fù)原始明文
1.3 應(yīng)用場(chǎng)景
- 敏感數(shù)據(jù)加密傳輸與存儲(chǔ)
- 數(shù)字簽名與身份認(rèn)證
- 安全密鑰交換
- 系統(tǒng)登錄與訪問(wèn)控制
二、基于BouncyCastle實(shí)現(xiàn)SM2完整代碼
2.1 項(xiàng)目架構(gòu)
本次實(shí)現(xiàn)采用前后端分離架構(gòu):
- 后端:Spring Boot + BouncyCastle
- 前端:Vue 3 + Pinia + sm-crypto-v2
2.2 后端核心實(shí)現(xiàn)(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();
// 靜態(tài)初始化BouncyCastleProvider
static {
if (Security.getProvider(PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
/**
* 生成SM2密鑰對(duì)
* @return KeyPair對(duì)象,包含公鑰和私鑰
*/
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密鑰對(duì)失敗: {}", e.getMessage(), e);
throw new RuntimeException("生成SM2密鑰對(duì)失敗", e);
}
}
/**
* 獲取十六進(jìn)制格式的公鑰
* @param publicKey 公鑰對(duì)象
* @return 十六進(jìn)制格式的公鑰字符串
*/
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 私鑰對(duì)象
* @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("密文數(shù)據(jù)不能為空");
}
if (privateKeyBase64 == null || privateKeyBase64.isEmpty()) {
throw new IllegalArgumentException("私鑰不能為空");
}
try {
logger.info("=== 開(kāi)始SM2解密流程 ===");
logger.info("密文原始字符串長(zhǎng)度: {} 字符", 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("私鑰長(zhǎng)度: {} 字符", privateKeyBase64.length());
// 使用Bouncy Castle實(shí)現(xiàn)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);
// 檢查密文格式并修復(fù)
if (encryptedData.length > 0 && encryptedData[0] != 0x04) {
logger.warn("檢測(cè)到密文沒(méi)有0x04前綴,這可能是sm-crypto庫(kù)的加密輸出格式問(wèn)題");
if (encryptedData.length >= 97) {
// 分離C1、C3、C2組件(假設(shè)是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("成功修復(fù)密文格式:");
logger.info(" 修復(fù)前長(zhǎng)度: {} 字節(jié)", encryptedData.length - 1);
logger.info(" 修復(fù)后長(zhǎng)度: {} 字節(jié)", encryptedData.length);
logger.info(" 修復(fù)后第一個(gè)字節(jié): 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解密成功,密文長(zhǎng)度: {},原文長(zhǎng)度: {}",
encryptedText.length(), decryptedText.length());
logger.info("=== SM2解密流程結(jié)束(成功)===");
return decryptedText;
} catch (InvalidCipherTextException e) {
logger.error("SM2解密失敗 - 無(wú)效密文: {}", e.getMessage(), e);
throw new RuntimeException("SM2解密失敗 - 無(wú)效密文", e);
} catch (Exception e) {
logger.error("SM2解密失敗: {}", e.getMessage(), e);
throw new RuntimeException("SM2解密失敗", e);
}
}
}
2.3 前端核心實(shí)現(xiàn)(sm2Store.js)
import { defineStore } from 'pinia'
import { sm2 } from 'sm-crypto-v2'
export const useSm2Store = defineStore('sm2', {
state: () => ({
publicKey: '',
privateKey: '', // 實(shí)際項(xiàng)目中私鑰不應(yīng)從前端獲取
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('公鑰長(zhǎng)度:', this.publicKey.length)
} catch (error) {
this.error = `獲取公鑰失敗: ${error.message}`
console.error(this.error)
} finally {
this.isLoading = false
}
},
/**
* 加密數(shù)據(jù)
*/
encryptData() {
if (!this.plainText) {
this.error = '請(qǐng)輸入要加密的明文'
return
}
if (!this.publicKey) {
this.error = '請(qǐng)先獲取公鑰'
return
}
this.isLoading = true
this.error = ''
this.encryptedData = ''
try {
console.log('開(kāi)始加密,明文:', this.plainText)
console.log('公鑰:', this.publicKey)
// 使用sm-crypto-v2庫(kù)進(jìn)行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
}
},
/**
* 本地解密數(shù)據(jù)(不再調(diào)用后端接口)
*/
decryptData() {
if (!this.encryptedData) {
this.error = '請(qǐng)先加密數(shù)據(jù)'
return
}
if (!this.privateKey) {
this.error = '解密需要私鑰'
return
}
this.isLoading = true
this.error = ''
this.decryptedData = ''
try {
console.log('開(kāi)始本地解密,密文:', this.encryptedData)
console.log('私鑰:', this.privateKey)
// 使用sm-crypto-v2庫(kù)進(jìn)行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實(shí)現(xiàn)(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 {
// 全局密鑰對(duì)(實(shí)際項(xiàng)目中應(yīng)使用更安全的密鑰管理方式)
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);
}
/**
* 獲取十六進(jìn)制格式的公鑰
* @return 十六進(jìn)制格式的公鑰字符串
*/
@GetMapping("/public-key-hex")
public String getPublicKeyHex() {
return publicKeyHex;
}
}
三、項(xiàng)目依賴配置
3.1 后端依賴(pom.xml)
<dependencies>
<!-- Spring Boot 核心依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Bouncy Castle 加密庫(kù) -->
<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"
}
}
四、常見(jiàn)問(wèn)題分析與解決方案
4.1 "Invalid point encoding"錯(cuò)誤
問(wèn)題現(xiàn)象: 解密時(shí)出現(xiàn)"Invalid point encoding"錯(cuò)誤。
原因: SM2公鑰或密文缺少0x04前綴(橢圓曲線非壓縮點(diǎn)標(biāo)識(shí))。
解決方案:
- 確保公鑰使用非壓縮格式,包含0x04前綴
- 驗(yàn)證密文格式正確(這是重中之重),必要時(shí)自動(dòng)修復(fù)(如代碼中實(shí)現(xiàn)的密文修復(fù)邏輯)
4.2 "Cannot read properties of null (reading ‘multiply’)"錯(cuò)誤
問(wèn)題現(xiàn)象: 前端加密時(shí)出現(xiàn)此錯(cuò)誤。
原因: 公鑰格式不正確或?yàn)榭铡?/p>
解決方案:
- 確保公鑰是有效的十六進(jìn)制格式,長(zhǎng)度為130字符(包含0x04前綴)
- 添加公鑰格式驗(yàn)證邏輯
4.3 加密模式不匹配問(wèn)題
問(wèn)題現(xiàn)象: 密文解密失敗。
原因: 加密和解密使用了不同的密文格式(C1C3C2 vs C1C2C3)。
解決方案:
- 前后端統(tǒng)一使用C1C3C2格式(新國(guó)密標(biāo)準(zhǔn))
- 在sm-crypto-v2中通過(guò)mode參數(shù)指定格式
到此這篇關(guān)于vue + springboot 實(shí)現(xiàn)國(guó)密SM2加密的文章就介紹到這了,更多相關(guān)vue springboot國(guó)密SM2加密內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用vue-router切換頁(yè)面時(shí)實(shí)現(xiàn)設(shè)置過(guò)渡動(dòng)畫(huà)
今天小編就為大家分享一篇使用vue-router切換頁(yè)面時(shí)實(shí)現(xiàn)設(shè)置過(guò)渡動(dòng)畫(huà)。具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-10-10
Vue純前端如何實(shí)現(xiàn)導(dǎo)出簡(jiǎn)單Excel表格的功能
這篇文章主要介紹了如何在Vue項(xiàng)目中使用vue-json-excel插件實(shí)現(xiàn)Excel表格的導(dǎo)出功能,包括安裝依賴、引入插件、使用組件、設(shè)置表頭和數(shù)據(jù)、處理空數(shù)據(jù)情況、源代碼修改以解決常見(jiàn)問(wèn)題,需要的朋友可以參考下2025-01-01
Vue3組件不發(fā)生變化如何監(jiān)聽(tīng)pinia中數(shù)據(jù)變化
這篇文章主要給大家介紹了關(guān)于Vue3組件不發(fā)生變化如何監(jiān)聽(tīng)pinia中數(shù)據(jù)變化的相關(guān)資料,pinia是Vue的存儲(chǔ)庫(kù),它允許您跨組件/頁(yè)面共享狀態(tài),文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-11-11
Vue自定義指令結(jié)合阿里云OSS優(yōu)化圖片的實(shí)現(xiàn)方法
這篇文章主要介紹了Vue自定義指令結(jié)合阿里云OSS優(yōu)化圖片的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11
vue項(xiàng)目中使用天地圖的簡(jiǎn)單代碼示例
這篇文章主要給大家介紹了關(guān)于vue項(xiàng)目中使用天地圖的相關(guān)資料,在Vue.js項(xiàng)目中使用天地圖,首先需要申請(qǐng)apikey并在index.html中引入天地圖的js文件,然后創(chuàng)建一個(gè)div元素作為地圖容器,并通過(guò)CSS設(shè)置其樣式,需要的朋友可以參考下2024-11-11
解決Vue數(shù)據(jù)更新了但頁(yè)面沒(méi)有更新的問(wèn)題
在vue項(xiàng)目中,有些我們會(huì)遇到修改完數(shù)據(jù),但是視圖卻沒(méi)有更新的情況,具體的場(chǎng)景不一樣,解決問(wèn)題的方法也不一樣,在網(wǎng)上看了很多文章,在此總結(jié)匯總一下,需要的朋友可以參考下2023-08-08
詳解Vue3如何優(yōu)雅的監(jiān)聽(tīng)localStorage變化
最近在研究框架,也仔細(xì)用了Vue3一些功能,所以本文就來(lái)和大家聊聊Vue3如何實(shí)現(xiàn)優(yōu)雅的監(jiān)聽(tīng)localStorage的變化,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-06-06
Vue+java實(shí)現(xiàn)時(shí)間段的搜索示例
本文主要介紹了Vue+java實(shí)現(xiàn)時(shí)間段的搜索示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06
vue項(xiàng)目實(shí)現(xiàn)點(diǎn)擊目標(biāo)區(qū)域之外可關(guān)閉(隱藏)目標(biāo)區(qū)域
這篇文章主要介紹了vue項(xiàng)目實(shí)現(xiàn)點(diǎn)擊目標(biāo)區(qū)域之外可關(guān)閉(隱藏)目標(biāo)區(qū)域,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03

