本文是混合加密:前端 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。
方案统一用:
- 前端公钥: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 代码
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。
启动类
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);
}
}
密钥工具类
这个类负责:
- 生成 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 值构建/验签的示例。
密钥持有类
演示用启动时生成。生产环境要固定保存,不要每次重启都换。
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;
}
}
请求 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;
}
}
解密服务
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 私钥解密示例。
控制器
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 / 原生都能直接使用。
安装依赖
使用 sm-crypto,也可以用更新一点的 sm-crypto-v2。npm 上显示 sm-crypto-v2 近期仍有更新,并明确支持 sm2/sm3/sm4。下面示例先按 sm-crypto 风格来写。
npm install sm-crypto
混合加密工具
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 处理字符串数据。
页面调用示例
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);
}
四、完整交互过程
前端获取公钥
请求:
get /api/public-key
响应:
{
"code": 0,
"publickey": "04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}这个公钥是给前端 sm2.doencrypt(...) 直接用的原始 sm2 公钥。
前端组装明文 json
{
"username": "admin",
"password": "123456",
"timestamp": 1710000000000
}前端生成随机 sm4 key
例如:
a8cd3ef7hj2kl9mn
前端加密
data = sm4.encrypt(payload, sm4key)key = sm2.doencrypt(sm4key, publickey, 1)
最终请求体:
{
"key": "sm2加密后的sm4密钥(hex)",
"data": "sm4加密后的业务json(hex)"
}后端解密
- 用 sm2 私钥解出
sm4key - 用 sm4 key 解出
plainjson - 解析出
username/password/timestamp
五、为什么这样更合理
“为什么不直接用 sm2”。这里混合加密的优势就是:
- sm2 负责保护一个很小的随机密钥
- sm4 负责高效加密真正的业务数据
hutool 文档本身也把 sm2 归为非对称加密,把 sm4 归为对称加密;这两类算法在工程上本来就常常配合使用。
六、最容易踩的坑
1. 前端公钥格式错
不能把 getpublickeybase64() 直接给前端。前端要的是 04 + x + y 的原始公钥,不是 x.509/asn.1 编码的公钥。hutool 文档明确区分了公钥的 q 值 和 x.509 两种不同格式。
2. sm2 模式不一致
前端这里固定:
const ciphermode = 1;
联调时就按 c1c3c2 统一,不要混。
3. sm4 key 长度不对
hutool 文档中自定义 sm4 key 的示例是 128 位,也就是 16 字节。这里前端随机生成 16 个 ascii 字符,后端按 utf-8 读取后恰好是 16 字节。
4. 后端每次重启重新生成密钥
演示可以这样。生产不行。
生产环境要把私钥固定存起来,不然前端今天拿到的公钥和明天后端的私钥就不是一对了。
5. 仍然必须用 https
这套字段级加密不能替代 tls。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": "登录成功"
}以上就是springboot算法实现数据加密传输的详细内容,更多关于springboot数据加密传输的资料请关注代码网其它相关文章!
发表评论