金融、政务类的系统对数据安全要求越来越高,等保测评和密评也逐渐把国密算法列成了硬性指标。SM2、SM3、SM4是国家密码管理局发布的商用密码标准,分别在非对称加密、摘要、对称加密三个领域对标RSA、SHA-256和AES。这篇文章就来聊聊怎么在一个已有的Spring Boot项目里把这些算法落地,包括依赖配置、工具类封装、接口层加解密实战,以及混合加密的方案设计。

一、先弄清楚三种算法各自的分工
很多刚接触国密的开发者容易把SM2、SM3、SM4混为一谈,实际上三者定位完全不同,选错了算法类型,安全方案就站不住脚。
SM2是基于椭圆曲线(ECC)的非对称加密算法,可以理解为国密版的RSA,但密钥更短、速度更快。256位的SM2密钥安全强度约等于3072位的RSA密钥。它主要用在两个场景:一是公钥加密、私钥解密,适合传输短小敏感数据;二是数字签名,用于验证数据完整性和身份。
SM3是密码散列算法,对标MD5和SHA-256,输出固定256位的摘要,常用于密码存储、数据完整性校验和签名前的原文摘要计算。它本身不可逆,也没有密钥的概念。
SM4是分组对称加密算法,对标AES,密钥长度128位,分组长度也是128位。它适合对大批量数据做加解密,性能上和AES接近。典型的用法是SM4加密报文体,SM2加密SM4的密钥,两者配合构成混合加密。
二、引入依赖并封装工具类
Java原生JDK并不支持国密算法,需要借助Bouncy Castle作为安全提供者。这里推荐直接使用hutool-crypto,它对Bouncy Castle做了一层封装,API非常简洁,同时也支持BC库的底层调用方式。
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.77</version>
</dependency>注意Bouncy Castle的版本要和JDK版本匹配,JDK8用bcprov-jdk15on,JDK17及以上用bcprov-jdk18on,否则注册Provider时会抛出异常。
接着封装一个国密工具类,把SM2、SM3、SM4的常用操作都收拢进来,业务代码只面向这个门面类编程,后面更换底层实现也不影响业务层:
import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import cn.hutool.crypto.symmetric.SM4;
public class SmCryptoUtil {
// SM4加密,key为16字节hex字符串或字节数组
public static String sm4Encrypt(String plainText, byte[] key) {
SM4 sm4 = new SM4(key);
return sm4.encryptHex(plainText);
}
public static String sm4Decrypt(String cipherHex, byte[] key) {
SM4 sm4 = new SM4(key);
return sm4.decryptStr(cipherHex);
}
// SM2公钥加密,前端持公钥加密,后端私钥解密
public static String sm2Encrypt(String plainText, String publicKey) {
SM2 sm2 = SmUtil.sm2(publicKey, null);
return sm2.encryptHex(plainText, KeyType.PublicKey);
}
public static String sm2Decrypt(String cipherHex, String privateKey) {
SM2 sm2 = SmUtil.sm2(null, privateKey);
return sm2.decryptStr(cipherHex, KeyType.PrivateKey);
}
// SM3摘要,常用于密码散列
public static String sm3Digest(String plainText) {
return SmUtil.sm3(plainText);
}
// SM2签名与验签
public static String sm2Sign(String data, String privateKey) {
SM2 sm2 = SmUtil.sm2(null, privateKey);
return sm2.signHex(data);
}
public static boolean sm2Verify(String data, String sign, String publicKey) {
SM2 sm2 = SmUtil.sm2(publicKey, null);
return sm2.verifyHex(data, sign);
}
}密钥管理上有个细节要注意:SM4的密钥必须是16字节,SM2的密钥对一般用SmUtil.sm2()生成一次后固化到配置中心,不要每次请求都重新生成,否则解密方拿不到对应密钥。密钥建议通过环境变量或配置中心注入,严禁硬编码在代码里,这一点在密评时是重点检查项。
三、在接口层落地加解密
工具类封装好之后,接下来解决接口层面的统一加解密。最优雅的方式是自定义注解加RequestBodyAdvice和ResponseBodyAdvice,在参数进入Controller之前完成解密,在响应返回之前完成加密,业务代码完全无感。
// 1. 自定义注解,标记需要加解密的接口
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Encrypt {
}
// 2. 请求解密:前端用SM4加密请求体,密钥通过SM2保护传输
@ControllerAdvice
public class DecryptRequestAdvice implements RequestBodyAdvice {
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage input, MethodParameter parameter,
Type targetType, Class converterType) throws IOException {
String body = IoUtil.read(input.getBody(), StandardCharsets.UTF_8);
// 解析出加密的业务数据和加密后的SM4密钥
JSONObject json = JSONUtil.parseObj(body);
String sm4KeyHex = SmCryptoUtil.sm2Decrypt(
json.getStr("key"), SecurityKeys.SM2_PRIVATE_KEY);
String plain = SmCryptoUtil.sm4Decrypt(
json.getStr("data"), HexUtil.decodeHex(sm4KeyHex));
InputStream is = new ByteArrayInputStream(
plain.getBytes(StandardCharsets.UTF_8));
return new HttpInputMessage() {
@Override public InputStream getBody() { return is; }
@Override public HttpHeaders getHeaders() { return input.getHeaders(); }
};
}
@Override public boolean supports(MethodParameter m, Type t, Class<?> c) {
return m.hasMethodAnnotation(Encrypt.class)
|| m.getContainingClass().isAnnotationPresent(Encrypt.class);
}
// 其他方法省略
}前端侧需要一个对应的加密流程:先用后端下发的SM2公钥加密一个随机生成的SM4密钥,再用这个SM4密钥加密真正的业务报文,把两者拼成JSON提交。这就是典型的信封加密(混合加密)思路:非对称算法解决密钥分发问题,对称算法负责大数据量的加解密,兼顾安全性和性能。
登录密码这类场景则不需要加密,用SM3摘要加盐即可。可以在用户表存sm3(salt + password),盐值每个用户独立生成,校验时重新计算摘要比对。相比SM2加密存储,摘要方案不可逆且无需管理解密密钥,更适合密码这种只需要验证、不需要还原的数据。
四、几个容易踩的坑
第一个坑是SM2密文格式不兼容。国密标准C1C2C3和新的C1C3C2两种排列顺序都有实现,Java端hutool默认输出C1C3C2,而部分前端js库(如sm-crypto)默认是C1C2C3,联调时解密失败大概率是这个问题。可以在创建SM2对象时指定SM2Engine.Mode.C1C2C3来对齐。
第二个坑是密钥hex字符串直接当字节用。前端传来的密钥如果是64位hex字符串,实际只有32字节,直接getBytes()当密钥会报错,必须先HexUtil.decodeHex()转成字节数组,这也是联调阶段最常见的异常来源。
第三个坑是SM4的ECB模式。默认的ECB模式对相同明文块产生相同密文,容易被分组重放攻击。建议显式指定CBC或GCM模式,并配合随机IV,每次加密生成新的初始向量随密文一起传输。安全要求高的系统,密评时ECB模式基本过不了关。
整体来看,Spring Boot接入国密算法的门槛并不高,核心工作在于选对算法组合、封装好工具层、处理好前后端密文格式对齐这三件事。把混合加密方案和注解式的统一加解密搭好之后,后续新增接口只需要打一个注解就能享受国密保护,维护成本非常低。
Spring Boot国密算法SM4加密修改时间:2026-09-05 23:38:51