SpringBoot算法實(shí)現(xiàn)數(shù)據(jù)加密傳輸
本文是混合加密:前端 SM2 + SM4,后端 Spring Boot + Hutool 解密的完整示例。
方案的邏輯是:
- 前端隨機(jī)生成一個(gè) SM4 key
- 用 SM4 加密整個(gè)業(yè)務(wù) JSON
- 用后端提供的 SM2 公鑰 加密這個(gè) SM4 key
- 后端先用 SM2 私鑰 解出 SM4 key
- 再用 SM4 解出業(yè)務(wù) JSON
Hutool 官方文檔明確支持 SM2 / SM3 / SM4,并給出了 SmUtil.sm2(...)、SmUtil.sm4(...) 以及 encryptHex / decryptStr 這類用法;同時(shí)文檔說(shuō)明國(guó)密算法需要引入 Bouncy Castle 依賴。sm-crypto 系列前端庫(kù)也支持 SM2 / SM3 / SM4。
方案統(tǒng)一用:
- 前端公鑰:SM2 原始公鑰 Hex,
04 + X + Y - SM2 密文:Hex
- SM4 密文:Hex
- SM4 key:16 字節(jié)字符串
- SM2 模式:
C1C3C2
一、前后端協(xié)議
前端原始數(shù)據(jù):
{
"username": "admin",
"password": "123456",
"timestamp": 1710000000000
}前端最終提交給后端:
{
"key": "SM2加密后的SM4密鑰(hex)",
"data": "SM4加密后的業(yè)務(wù)JSON(hex)"
}二、后端 Spring Boot 代碼
Maven 依賴
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.29</version>
</dependency>
<!-- Bouncy Castle,Hutool 國(guó)密依賴 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>1.83</version>
</dependency>
</dependencies>Hutool 的國(guó)密文檔明確寫(xiě)了 SM2/SM3/SM4 依賴 Bouncy Castle;Hutool 加密模塊文檔也說(shuō)明其封裝入口之一就是 SmUtil。
啟動(dòng)類
package com.example.demo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.security.Security;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
Security.addProvider(new BouncyCastleProvider());
SpringApplication.run(DemoApplication.class, args);
}
}
密鑰工具類
這個(gè)類負(fù)責(zé):
- 生成 SM2 密鑰對(duì)
- 導(dǎo)出前端可用的原始公鑰 Hex
- 導(dǎo)出后端解密用的原始私鑰 Hex
Sm2KeyUtil.java
package com.example.demo.crypto;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.SM2;
import org.bouncycastle.jce.interfaces.BCECPrivateKey;
import org.bouncycastle.jce.interfaces.BCECPublicKey;
import org.bouncycastle.math.ec.ECPoint;
public class Sm2KeyUtil {
private Sm2KeyUtil() {
}
public static SM2 generateSm2() {
return SmUtil.sm2();
}
/**
* 前端 sm-crypto 可直接使用的公鑰:
* 04 + X(64位hex) + Y(64位hex)
*/
public static String getPublicKeyHexForFrontend(SM2 sm2) {
BCECPublicKey publicKey = (BCECPublicKey) sm2.getPublicKey();
ECPoint point = publicKey.getQ();
String x = leftPad64(point.getAffineXCoord().toBigInteger().toString(16));
String y = leftPad64(point.getAffineYCoord().toBigInteger().toString(16));
return "04" + x + y;
}
/**
* 后端原始私鑰 hex,64位
*/
public static String getPrivateKeyHexRaw(SM2 sm2) {
BCECPrivateKey privateKey = (BCECPrivateKey) sm2.getPrivateKey();
return leftPad64(privateKey.getD().toString(16));
}
/**
* 按原始私鑰重建 SM2 對(duì)象
*/
public static SM2 buildSm2ByPrivateKeyHex(String privateKeyHex) {
return SmUtil.sm2(privateKeyHex, null);
}
private static String leftPad64(String hex) {
if (hex == null) {
return null;
}
if (hex.length() >= 64) {
return hex;
}
return "0".repeat(64 - hex.length()) + hex;
}
}
Hutool 官方文檔明確區(qū)分了 SM2 密鑰的幾種格式:私鑰可為 D 值、PKCS#8、PKCS#1,公鑰可為 Q 值、X.509、PKCS#1,并說(shuō)明新版本構(gòu)造方法對(duì)這些格式做了兼容。文檔還給出了用私鑰 D 值和公鑰 Q 值構(gòu)建/驗(yàn)簽的示例。
密鑰持有類
演示用啟動(dòng)時(shí)生成。生產(chǎn)環(huán)境要固定保存,不要每次重啟都換。
Sm2KeyHolder.java
package com.example.demo.crypto;
import cn.hutool.crypto.asymmetric.SM2;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;
@Component
public class Sm2KeyHolder {
private String publicKeyHexForFrontend;
private String privateKeyHexRaw;
private SM2 sm2;
@PostConstruct
public void init() {
this.sm2 = Sm2KeyUtil.generateSm2();
this.publicKeyHexForFrontend = Sm2KeyUtil.getPublicKeyHexForFrontend(sm2);
this.privateKeyHexRaw = Sm2KeyUtil.getPrivateKeyHexRaw(sm2);
System.out.println("=== SM2密鑰初始化 ===");
System.out.println("前端公鑰Hex: " + publicKeyHexForFrontend);
System.out.println("后端私鑰Hex: " + privateKeyHexRaw);
}
public String getPublicKeyHexForFrontend() {
return publicKeyHexForFrontend;
}
public String getPrivateKeyHexRaw() {
return privateKeyHexRaw;
}
public SM2 getSm2() {
return sm2;
}
}
請(qǐng)求 DTO
EncryptedLoginRequest.java
package com.example.demo.dto;
public class EncryptedLoginRequest {
/**
* SM2加密后的SM4 key(hex)
*/
private String key;
/**
* SM4加密后的業(yè)務(wù)數(shù)據(jù)(hex)
*/
private String data;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}
LoginPlainRequest.java
package com.example.demo.dto;
public class LoginPlainRequest {
private String username;
private String password;
private Long timestamp;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Long getTimestamp() {
return timestamp;
}
public void setTimestamp(Long timestamp) {
this.timestamp = timestamp;
}
}
解密服務(wù)
HybridCryptoService.java
package com.example.demo.service;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import cn.hutool.crypto.symmetric.SM4;
import com.example.demo.crypto.Sm2KeyHolder;
import com.example.demo.crypto.Sm2KeyUtil;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
@Service
public class HybridCryptoService {
private final Sm2KeyHolder keyHolder;
public HybridCryptoService(Sm2KeyHolder keyHolder) {
this.keyHolder = keyHolder;
}
/**
* 用后端私鑰解密前端傳來(lái)的 SM4 key
*/
public String decryptSm4Key(String encryptedSm4KeyHex) {
SM2 sm2 = Sm2KeyUtil.buildSm2ByPrivateKeyHex(keyHolder.getPrivateKeyHexRaw());
byte[] keyBytes = sm2.decryptFromBcd(encryptedSm4KeyHex, KeyType.PrivateKey);
return StrUtil.utf8Str(keyBytes);
}
/**
* 用 SM4 key 解密業(yè)務(wù)數(shù)據(jù)
*/
public String decryptBusinessData(String sm4Key, String encryptedDataHex) {
SM4 sm4 = SmUtil.sm4(sm4Key.getBytes(StandardCharsets.UTF_8));
return sm4.decryptStr(encryptedDataHex, StandardCharsets.UTF_8);
}
}
Hutool 官方文檔給出了 SmUtil.sm4(key)、encryptHex(...)、decryptStr(...) 的 SM4 用法,也給出了 sm2.decryptFromBcd(..., KeyType.PrivateKey) 的 SM2 私鑰解密示例。
控制器
LoginController.java
package com.example.demo.controller;
import cn.hutool.json.JSONUtil;
import com.example.demo.crypto.Sm2KeyHolder;
import com.example.demo.dto.EncryptedLoginRequest;
import com.example.demo.dto.LoginPlainRequest;
import com.example.demo.service.HybridCryptoService;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api")
public class LoginController {
private final Sm2KeyHolder keyHolder;
private final HybridCryptoService hybridCryptoService;
public LoginController(Sm2KeyHolder keyHolder, HybridCryptoService hybridCryptoService) {
this.keyHolder = keyHolder;
this.hybridCryptoService = hybridCryptoService;
}
/**
* 提供前端可直接使用的 SM2 原始公鑰
*/
@GetMapping("/public-key")
public Map<String, Object> getPublicKey() {
Map<String, Object> result = new HashMap<>();
result.put("code", 0);
result.put("publicKey", keyHolder.getPublicKeyHexForFrontend());
return result;
}
/**
* 混合加密登錄接口
*/
@PostMapping("/login")
public Map<String, Object> login(@RequestBody EncryptedLoginRequest request) {
Map<String, Object> result = new HashMap<>();
try {
// 1. 解密 SM4 key
String sm4Key = hybridCryptoService.decryptSm4Key(request.getKey());
// 2. 解密業(yè)務(wù) JSON
String plainJson = hybridCryptoService.decryptBusinessData(sm4Key, request.getData());
// 3. 轉(zhuǎn)換為明文請(qǐng)求對(duì)象
LoginPlainRequest loginRequest = JSONUtil.toBean(plainJson, LoginPlainRequest.class);
// 4. 演示校驗(yàn)
if ("admin".equals(loginRequest.getUsername())
&& "123456".equals(loginRequest.getPassword())) {
result.put("code", 0);
result.put("message", "登錄成功");
} else {
result.put("code", 401);
result.put("message", "用戶名或密碼錯(cuò)誤");
}
// 生產(chǎn)環(huán)境不要打印明文
// System.out.println("解密后JSON: " + plainJson);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "解密失敗: " + e.getMessage());
}
return result;
}
}
三、前端代碼
這里是通用 JS,Vue / React / 原生都能直接使用。
安裝依賴
使用 sm-crypto,也可以用更新一點(diǎn)的 sm-crypto-v2。npm 上顯示 sm-crypto-v2 近期仍有更新,并明確支持 SM2/SM3/SM4。下面示例先按 sm-crypto 風(fēng)格來(lái)寫(xiě)。
npm install sm-crypto
混合加密工具
hybrid-login.js
import { sm2, sm4 } from "sm-crypto";
/**
* 生成 16 字節(jié) SM4 key
* 這里用 16 個(gè) ASCII 字符,后端按 UTF-8 字節(jié)拿到就是 16 字節(jié)
*/
function randomSm4Key(length = 16) {
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789";
let result = "";
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
* 獲取后端提供的 SM2 公鑰(原始hex,04開(kāi)頭)
*/
export async function getPublicKey() {
const resp = await fetch("/api/public-key");
const json = await resp.json();
return json.publicKey;
}
/**
* 混合加密:
* 1. 隨機(jī)生成 SM4 key
* 2. 用 SM4 加密整個(gè)業(yè)務(wù) JSON
* 3. 用 SM2 公鑰加密 SM4 key
*/
export async function encryptLoginPayload(username, password) {
const publicKey = await getPublicKey();
// 1. 隨機(jī) SM4 key
const sm4Key = randomSm4Key(16);
// 2. 原始業(yè)務(wù) JSON
const payload = JSON.stringify({
username,
password,
timestamp: Date.now(),
});
// 3. SM4 加密業(yè)務(wù) JSON(輸出 hex)
const encryptedData = sm4.encrypt(payload, sm4Key);
// 4. SM2 加密 SM4 key(cipherMode=1 表示 C1C3C2)
const cipherMode = 1;
const encryptedKey = sm2.doEncrypt(sm4Key, publicKey, cipherMode);
return {
key: encryptedKey,
data: encryptedData,
};
}
/**
* 提交登錄
*/
export async function login(username, password) {
const body = await encryptLoginPayload(username, password);
const resp = await fetch("/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
return await resp.json();
}
sm-crypto/同類包支持 SM2、SM4;Hutool 文檔則說(shuō)明 SM4 可以使用自定義 key,并通過(guò) encryptHex/decryptStr 處理字符串?dāng)?shù)據(jù)。
頁(yè)面調(diào)用示例
import { login } from "./hybrid-login";
async function submitLogin() {
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
const result = await login(username, password);
console.log(result);
}
四、完整交互過(guò)程
前端獲取公鑰
請(qǐng)求:
GET /api/public-key
響應(yīng):
{
"code": 0,
"publicKey": "04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}這個(gè)公鑰是給前端 sm2.doEncrypt(...) 直接用的原始 SM2 公鑰。
前端組裝明文 JSON
{
"username": "admin",
"password": "123456",
"timestamp": 1710000000000
}前端生成隨機(jī) SM4 key
例如:
A8cD3eF7hJ2kL9mN
前端加密
data = sm4.encrypt(payload, sm4Key)key = sm2.doEncrypt(sm4Key, publicKey, 1)
最終請(qǐng)求體:
{
"key": "SM2加密后的SM4密鑰(hex)",
"data": "SM4加密后的業(yè)務(wù)JSON(hex)"
}后端解密
- 用 SM2 私鑰解出
sm4Key - 用 SM4 key 解出
plainJson - 解析出
username/password/timestamp
五、為什么這樣更合理
“為什么不直接用 SM2”。這里混合加密的優(yōu)勢(shì)就是:
- SM2 負(fù)責(zé)保護(hù)一個(gè)很小的隨機(jī)密鑰
- SM4 負(fù)責(zé)高效加密真正的業(yè)務(wù)數(shù)據(jù)
Hutool 文檔本身也把 SM2 歸為非對(duì)稱加密,把 SM4 歸為對(duì)稱加密;這兩類算法在工程上本來(lái)就常常配合使用。
六、最容易踩的坑
1. 前端公鑰格式錯(cuò)
不能把 getPublicKeyBase64() 直接給前端。前端要的是 04 + X + Y 的原始公鑰,不是 X.509/ASN.1 編碼的公鑰。Hutool 文檔明確區(qū)分了公鑰的 Q 值 和 X.509 兩種不同格式。
2. SM2 模式不一致
前端這里固定:
const cipherMode = 1;
聯(lián)調(diào)時(shí)就按 C1C3C2 統(tǒng)一,不要混。
3. SM4 key 長(zhǎng)度不對(duì)
Hutool 文檔中自定義 SM4 key 的示例是 128 位,也就是 16 字節(jié)。這里前端隨機(jī)生成 16 個(gè) ASCII 字符,后端按 UTF-8 讀取后恰好是 16 字節(jié)。
4. 后端每次重啟重新生成密鑰
演示可以這樣。生產(chǎn)不行。
生產(chǎn)環(huán)境要把私鑰固定存起來(lái),不然前端今天拿到的公鑰和明天后端的私鑰就不是一對(duì)了。
5. 仍然必須用 HTTPS
這套字段級(jí)加密不能替代 TLS。Hutool 只解決加解密實(shí)現(xiàn),不負(fù)責(zé)傳輸層安全。
七、生產(chǎn)版建議
可以先用上面代碼跑通,之后再補(bǔ)這幾項(xiàng):
- 固定私鑰:放配置中心 / KMS / HSM
- 時(shí)間戳校驗(yàn):比如 5 分鐘內(nèi)有效
- nonce 防重放
- 簽名校驗(yàn):在混合加密外再加簽,防篡改
- 不要打印明文 JSON / 密碼
- 全站 HTTPS
八、最小可驗(yàn)證步驟
先啟動(dòng)后端。
第一步,調(diào)用:
GET /api/public-key
確認(rèn)返回的 publicKey 是 04 開(kāi)頭的長(zhǎng) hex 字符串。
第二步,前端執(zhí)行:
const body = await encryptLoginPayload("admin", "123456");
console.log(body);應(yīng)該能看到:
{
"key": "一串SM2 hex密文",
"data": "一串SM4 hex密文"
}第三步,調(diào)用:
login("admin", "123456")應(yīng)該返回:
{
"code": 0,
"message": "登錄成功"
}以上就是SpringBoot算法實(shí)現(xiàn)數(shù)據(jù)加密傳輸?shù)脑敿?xì)內(nèi)容,更多關(guān)于SpringBoot數(shù)據(jù)加密傳輸?shù)馁Y料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- SpringBoot+MyBatis實(shí)現(xiàn)數(shù)據(jù)庫(kù)字段加密全過(guò)程
- 使用SpringBoot+MyBatis實(shí)現(xiàn)數(shù)據(jù)庫(kù)字段級(jí)加密
- SpringBoot對(duì)數(shù)據(jù)庫(kù)用戶名和密碼進(jìn)行加密的兩種方案
- SpringBoot整合jasypt實(shí)現(xiàn)重要數(shù)據(jù)加密
- SpringBoot進(jìn)行數(shù)據(jù)加密和解密的詳細(xì)指南
- SpringBoot項(xiàng)目配置數(shù)據(jù)庫(kù)密碼加密相關(guān)代碼
相關(guān)文章
spring-boot react如何一步一步實(shí)現(xiàn)增刪改查
這篇文章主要介紹了spring-boot react如何一步一步實(shí)現(xiàn)增刪改查,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-11-11
Mybatis往Mapper.xml文件中傳遞多個(gè)參數(shù)問(wèn)題
這篇文章主要介紹了Mybatis往Mapper.xml文件中傳遞多個(gè)參數(shù)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05
30個(gè)最常見(jiàn)算法題及其Java實(shí)現(xiàn)和詳細(xì)解析
在技術(shù)筆試中,算法題主要考察對(duì)基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)(數(shù)組、鏈表、樹(shù)、棧 / 隊(duì)列、圖)的掌握,以及經(jīng)典算法思想(動(dòng)態(tài)規(guī)劃、貪心、查找排序、回溯)的應(yīng)用,這篇文章主要介紹了30個(gè)最常見(jiàn)算法題及其Java實(shí)現(xiàn)和詳細(xì)解析的相關(guān)資料,需要的朋友可以參考下2026-01-01
JAVA復(fù)制數(shù)組和重置數(shù)組大小操作
這篇文章主要介紹了JAVA復(fù)制數(shù)組和重置數(shù)組大小操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09
Java橋接模式實(shí)例詳解【簡(jiǎn)單版與升級(jí)版】
這篇文章主要介紹了Java橋接模式,結(jié)合實(shí)例形式分析了java橋接模式簡(jiǎn)單版與升級(jí)版兩種實(shí)現(xiàn)技巧,需要的朋友可以參考下2019-07-07
Intellij Idea修改代碼方法參數(shù)自動(dòng)提示快捷鍵的操作
這篇文章主要介紹了Intellij Idea修改代碼方法參數(shù)自動(dòng)提示快捷鍵的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-01-01
Spring Boot Web 靜態(tài)文件緩存處理的方法
本篇文章主要介紹了Spring Boot Web 靜態(tài)文件緩存處理的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-02-02

