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

0 阅读8分钟

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

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

第二步,前端执行:

const body = await encryptLoginPayload("admin", "123456");
console.log(body);

应该能看到:

{
  "key": "一串SM2 hex密文",
  "data": "一串SM4 hex密文"
}

第三步,调用:

login("admin", "123456")

应该返回:

{
  "code": 0,
  "message": "登录成功"
}