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": "登录成功"
}
相关推荐
CaffeinePro3 分钟前
Pydantic深度使用:数据校验、枚举、ORM映射
后端·fastapi
Chenyiax34 分钟前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH35 分钟前
Koa和Express的区别
后端
MariaH40 分钟前
Koa框架的使用
后端
luckdewei2 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某3 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy3 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom3 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github
唐青枫7 小时前
Java JDBC 实战指南:从 Connection 到事务和连接池
java
用户1474853079748 小时前
CodeX使用Skill生成游戏美术和音乐资源,一分钟入门
后端