本文是混合加密:前端 SM2 + SM4,后端 Spring Boot + Hutool 解密的完整示例。
方案的逻辑是:
-
前端随机生成一个 SM4 key
-
用 SM4 加密整个业务 JSON
-
用后端提供的 SM2 公钥 加密这个 SM4 key
-
后端先用 SM2 私钥 解出 SM4 key
-
再用 SM4 解出业务 JSON
Hutool 官方文档明确支持 SM2 / SM3 / SM4,并给出了 SmUtil.sm2(...)、SmUtil.sm4(...) 以及 encryptHex / decryptStr 这类用法;同时文档说明国密算法需要引入 Bouncy Castle 依赖。sm-crypto 系列前端库也支持 SM2 / SM3 / SM4。(Hutool)
方案统一用:
-
前端公钥:SM2 原始公钥 Hex,
04 + X + Y -
SM2 密文:Hex
-
SM4 密文:Hex
-
SM4 key:16 字节字符串
-
SM2 模式:
C1C3C2
一、前后端协议
前端原始数据:
{
"username": "admin",
"password": "123456",
"timestamp": 1710000000000
}
前端最终提交给后端:
{
"key": "SM2加密后的SM4密钥(hex)",
"data": "SM4加密后的业务JSON(hex)"
}
二、后端 Spring Boot 代码
1)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 国密依赖 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>1.83</version>
</dependency>
</dependencies>
Hutool 的国密文档明确写了 SM2/SM3/SM4 依赖 Bouncy Castle;Hutool 加密模块文档也说明其封装入口之一就是 SmUtil。(Hutool)
2)启动类
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);
}
}
3)密钥工具类
这个类负责:
-
生成 SM2 密钥对
-
导出前端可用的原始公钥 Hex
-
导出后端解密用的原始私钥 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 对象
*/
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 官方文档明确区分了 SM2 密钥的几种格式:私钥可为 D 值、PKCS#8、PKCS#1,公钥可为 Q 值、X.509、PKCS#1,并说明新版本构造方法对这些格式做了兼容。文档还给出了用私钥 D 值和公钥 Q 值构建/验签的示例。(Hutool)
4)密钥持有类
演示用启动时生成。生产环境要固定保存,不要每次重启都换。
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;
}
}
5)请求 DTO
EncryptedLoginRequest.java
package com.example.demo.dto;
public class EncryptedLoginRequest {
/**
* SM2加密后的SM4 key(hex)
*/
private String key;
/**
* SM4加密后的业务数据(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;
}
}
6)解密服务
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;
}
/**
* 用后端私钥解密前端传来的 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 解密业务数据
*/
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 私钥解密示例。(Hutool)
7)控制器
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. 解密业务 JSON
String plainJson = hybridCryptoService.decryptBusinessData(sm4Key, request.getData());
// 3. 转换为明文请求对象
LoginPlainRequest loginRequest = JSONUtil.toBean(plainJson, LoginPlainRequest.class);
// 4. 演示校验
if ("admin".equals(loginRequest.getUsername())
&& "123456".equals(loginRequest.getPassword())) {
result.put("code", 0);
result.put("message", "登录成功");
} else {
result.put("code", 401);
result.put("message", "用户名或密码错误");
}
// 生产环境不要打印明文
// System.out.println("解密后JSON: " + plainJson);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "解密失败: " + e.getMessage());
}
return result;
}
}
三、前端代码
这里是通用 JS,Vue / React / 原生都能直接使用。
1)安装依赖
使用 sm-crypto,也可以用更新一点的 sm-crypto-v2。npm 上显示 sm-crypto-v2 近期仍有更新,并明确支持 SM2/SM3/SM4。下面示例先按 sm-crypto 风格来写。(NPM)
npm install sm-crypto
2)混合加密工具
hybrid-login.js
import { sm2, sm4 } from "sm-crypto";
/**
* 生成 16 字节 SM4 key
* 这里用 16 个 ASCII 字符,后端按 UTF-8 字节拿到就是 16 字节
*/
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开头)
*/
export async function getPublicKey() {
const resp = await fetch("/api/public-key");
const json = await resp.json();
return json.publicKey;
}
/**
* 混合加密:
* 1. 随机生成 SM4 key
* 2. 用 SM4 加密整个业务 JSON
* 3. 用 SM2 公钥加密 SM4 key
*/
export async function encryptLoginPayload(username, password) {
const publicKey = await getPublicKey();
// 1. 随机 SM4 key
const sm4Key = randomSm4Key(16);
// 2. 原始业务 JSON
const payload = JSON.stringify({
username,
password,
timestamp: Date.now(),
});
// 3. SM4 加密业务 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 文档则说明 SM4 可以使用自定义 key,并通过 encryptHex/decryptStr 处理字符串数据。(NPM)
3)页面调用示例
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);
}
四、完整交互过程
1)前端获取公钥
请求:
GET /api/public-key
响应:
{
"code": 0,
"publicKey": "04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
这个公钥是给前端 sm2.doEncrypt(...) 直接用的原始 SM2 公钥。
2)前端组装明文 JSON
{
"username": "admin",
"password": "123456",
"timestamp": 1710000000000
}
3)前端生成随机 SM4 key
例如:
A8cD3eF7hJ2kL9mN
4)前端加密
-
data = sm4.encrypt(payload, sm4Key) -
key = sm2.doEncrypt(sm4Key, publicKey, 1)
最终请求体:
{
"key": "SM2加密后的SM4密钥(hex)",
"data": "SM4加密后的业务JSON(hex)"
}
5)后端解密
-
用 SM2 私钥解出
sm4Key -
用 SM4 key 解出
plainJson -
解析出
username/password/timestamp
五、为什么这样更合理
“为什么不直接用 SM2”。这里混合加密的优势就是:
-
SM2 负责保护一个很小的随机密钥
-
SM4 负责高效加密真正的业务数据
Hutool 文档本身也把 SM2 归为非对称加密,把 SM4 归为对称加密;这两类算法在工程上本来就常常配合使用。(Hutool)
六、最容易踩的坑
1. 前端公钥格式错
不能把 getPublicKeyBase64() 直接给前端。
前端要的是 04 + X + Y 的原始公钥,不是 X.509/ASN.1 编码的公钥。Hutool 文档明确区分了公钥的 Q 值 和 X.509 两种不同格式。(Hutool)
2. SM2 模式不一致
前端这里固定:
const cipherMode = 1;
联调时就按 C1C3C2 统一,不要混。
3. SM4 key 长度不对
Hutool 文档中自定义 SM4 key 的示例是 128 位,也就是 16 字节。这里前端随机生成 16 个 ASCII 字符,后端按 UTF-8 读取后恰好是 16 字节。(Hutool)
4. 后端每次重启重新生成密钥
演示可以这样。生产不行。
生产环境要把私钥固定存起来,不然前端今天拿到的公钥和明天后端的私钥就不是一对了。
5. 仍然必须用 HTTPS
这套字段级加密不能替代 TLS。Hutool 只解决加解密实现,不负责传输层安全。(Hutool)
七、生产版建议
可以先用上面代码跑通,之后再补这几项:
-
固定私钥:放配置中心 / KMS / HSM
-
时间戳校验:比如 5 分钟内有效
-
nonce 防重放
-
签名校验:在混合加密外再加签,防篡改
-
不要打印明文 JSON / 密码
-
全站 HTTPS
八、最小可验证步骤
先启动后端。
第一步,调用:
GET /api/public-key
确认返回的 publicKey 是 04 开头的长 hex 字符串。
第二步,前端执行:
const body = await encryptLoginPayload("admin", "123456");
console.log(body);
应该能看到:
{
"key": "一串SM2 hex密文",
"data": "一串SM4 hex密文"
}
第三步,调用:
login("admin", "123456")
应该返回:
{
"code": 0,
"message": "登录成功"
}