Java 使用国密算法实现数据加密传输

本文是混合加密:前端 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


一、前后端协议

前端原始数据:

json 复制代码
{
  "username": "admin",
  "password": "123456",
  "timestamp": 1710000000000
}

前端最终提交给后端:

json 复制代码
{
  "key": "SM2加密后的SM4密钥(hex)",
  "data": "SM4加密后的业务JSON(hex)"
}

二、后端 Spring Boot 代码

1)Maven 依赖

xml 复制代码
<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)启动类

java 复制代码
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

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

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

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

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

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

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)

bash 复制代码
npm install sm-crypto

2)混合加密工具

hybrid-login.js

javascript 复制代码
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)页面调用示例

javascript 复制代码
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)前端获取公钥

请求:

http 复制代码
GET /api/public-key

响应:

json 复制代码
{
  "code": 0,
  "publicKey": "04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}

这个公钥是给前端 sm2.doEncrypt(...) 直接用的原始 SM2 公钥。


2)前端组装明文 JSON

json 复制代码
{
  "username": "admin",
  "password": "123456",
  "timestamp": 1710000000000
}

3)前端生成随机 SM4 key

例如:

text 复制代码
A8cD3eF7hJ2kL9mN

4)前端加密

  • data = sm4.encrypt(payload, sm4Key)

  • key = sm2.doEncrypt(sm4Key, publicKey, 1)

最终请求体:

json 复制代码
{
  "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 模式不一致

前端这里固定:

javascript 复制代码
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


八、最小可验证步骤

先启动后端。

第一步,调用:

http 复制代码
GET /api/public-key

确认返回的 publicKey04 开头的长 hex 字符串。

第二步,前端执行:

javascript 复制代码
const body = await encryptLoginPayload("admin", "123456");
console.log(body);

应该能看到:

json 复制代码
{
  "key": "一串SM2 hex密文",
  "data": "一串SM4 hex密文"
}

第三步,调用:

javascript 复制代码
login("admin", "123456")

应该返回:

json 复制代码
{
  "code": 0,
  "message": "登录成功"
}
相关推荐
我命由我123452 小时前
Android Gradle - Gradle 自定义插件(Build Script 自定义插件、buildSrc 自定义插件、独立项目自定义插件)
android·java·java-ee·kotlin·android studio·android-studio·android runtime
Riu_Peter2 小时前
【技术】Maven 配置 settings.xml 轮询下载
xml·java·maven
Rust语言中文社区2 小时前
【Rust日报】用 Rust 重写的 Turso 是一个更好的 SQLite 吗?
开发语言·数据库·后端·rust·sqlite
十六年开源服务商2 小时前
2026年WordPress网站地图完整指南
java·前端·javascript
Edward111111113 小时前
3月17枚举
java·开发语言
凡。。。2963 小时前
阿里云产品说明
java
蓝天守卫者联盟13 小时前
2026乙酸乙酯回收设备厂家选型与技术实践
java·jvm·python·算法
于先生吖3 小时前
教育数字化转型 JAVA 国际版答题练习系统完整开发教程
java·开发语言