1. 项目概述:SM2/SM4国密算法在Java接口对接中的应用
国密算法作为我国自主研发的密码标准体系,正在金融、政务、物联网等领域快速替代国际通用算法。最近在对接某政务平台接口时,对方明确要求使用SM2签名验签和SM4加密解密方案。这种组合既能保证数据传输的不可否认性(SM2),又能确保数据内容的机密性(SM4),是当前最主流的国密应用方案之一。
SM2基于椭圆曲线密码学(ECC),相比RSA具有密钥短、安全性高的特点。而SM4作为分组密码算法,其128位密钥强度与AES相当,但算法设计更适合硬件实现。在Java生态中,虽然JDK尚未内置国密支持,但通过BouncyCastle等第三方库可以完整实现相关功能。本文将分享我在实际项目中从零搭建国密加解密体系的完整过程。
2. 环境准备与依赖配置
2.1 开发环境要求
- JDK 1.8+(推荐JDK11,某些国密库对高版本支持更好)
- Maven/Gradle构建工具
- BouncyCastle Provider(bcprov-jdk15on)
- 可选:Hutool-crypto(封装了常用国密操作)
2.2 关键依赖引入
xml复制<!-- BouncyCastle 核心库 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<!-- Hutool工具包(可选) -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>
注意:必须确保BouncyCastle版本在1.60以上,早期版本对SM4支持不完善。我曾因使用1.58版本导致SM4/CBC模式出现异常填充错误。
2.3 安全提供者注册
在应用启动时需静态注册BouncyCastle提供者:
java复制static {
Security.addProvider(new BouncyCastleProvider());
}
或者在每次操作时动态指定:
java复制Security.insertProviderAt(new BouncyCastleProvider(), 1);
3. SM2签名验签实现详解
3.1 密钥对生成
SM2标准推荐的椭圆曲线参数为sm2p256v1,密钥长度为256位:
java复制// 使用Hutool简化生成(实际项目建议预生成后存储)
KeyPair pair = SecureUtil.generateKeyPair("SM2");
byte[] privateKey = pair.getPrivate().getEncoded();
byte[] publicKey = pair.getPublic().getEncoded();
手动生成方式:
java复制ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1");
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", "BC");
kpg.initialize(sm2Spec);
KeyPair keyPair = kpg.generateKeyPair();
3.2 签名生成流程
SM2签名需要指定ID参数(默认"1234567812345678"),实际项目应与对接方约定:
java复制public static byte[] sign(byte[] privateKey, byte[] data) throws Exception {
// 转换私钥
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKey);
KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
PrivateKey priKey = keyFactory.generatePrivate(keySpec);
// 创建签名实例
Signature signature = Signature.getInstance("SM3withSM2", "BC");
signature.initSign(priKey);
// 设置ID(重要!)
SM2ParameterSpec paramSpec = new SM2ParameterSpec("1234567812345678".getBytes());
signature.setParameter(paramSpec);
signature.update(data);
return signature.sign();
}
3.3 验签实现
java复制public static boolean verify(byte[] publicKey, byte[] data, byte[] sign) throws Exception {
// 转换公钥
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKey);
KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
PublicKey pubKey = keyFactory.generatePublic(keySpec);
// 验签实例
Signature signature = Signature.getInstance("SM3withSM2", "BC");
signature.initVerify(pubKey);
// 必须与签名时使用相同ID
SM2ParameterSpec paramSpec = new SM2ParameterSpec("1234567812345678".getBytes());
signature.setParameter(paramSpec);
signature.update(data);
return signature.verify(sign);
}
踩坑记录:某次对接时因双方ID参数不一致导致验签始终失败。建议将ID作为可配置参数,而非硬编码。
4. SM4加密解密实现
4.1 密钥生成
SM4支持128/192/256位密钥,但国标规定使用128位:
java复制// 随机生成密钥(16字节=128位)
byte[] key = new byte[16];
new SecureRandom().nextBytes(key);
// 实际项目中建议使用KeyGenerator
KeyGenerator kg = KeyGenerator.getInstance("SM4", "BC");
kg.init(128);
SecretKey secretKey = kg.generateKey();
4.2 ECB模式加密
java复制public static byte[] encryptSM4_ECB(byte[] key, byte[] data) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(key, "SM4");
Cipher cipher = Cipher.getInstance("SM4/ECB/PKCS5Padding", "BC");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
return cipher.doFinal(data);
}
4.3 CBC模式加密(推荐)
java复制public static byte[] encryptSM4_CBC(byte[] key, byte[] iv, byte[] data) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(key, "SM4");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS5Padding", "BC");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
return cipher.doFinal(data);
}
4.4 解密实现
java复制public static byte[] decryptSM4_CBC(byte[] key, byte[] iv, byte[] cipherText) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(key, "SM4");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS5Padding", "BC");
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
return cipher.doFinal(cipherText);
}
重要提示:CBC模式必须保证每次加密使用不同的IV值,否则会降低安全性。我曾见过有项目固定IV导致的安全事故。
5. 接口对接实战案例
5.1 典型请求报文结构
json复制{
"header": {
"appId": "TEST_APP",
"timestamp": "20240520120000"
},
"body": "U2FsdGVkX1+3C7JQ7V5z...", // SM4加密后的业务数据
"sign": "MEUCIQD5Z0W3w4wZ..." // SM2签名值
}
5.2 完整处理流程
java复制// 客户端构建
public String buildRequest(String appId, String bodyJson) throws Exception {
// 1. SM4加密body
byte[] sm4Key = loadSM4Key(); // 从安全存储获取
byte[] iv = generateRandomIV(); // 16字节随机IV
byte[] encryptedBody = encryptSM4_CBC(sm4Key, iv, bodyJson.getBytes());
// 2. 构建签名原文
String signText = appId + "|" + System.currentTimeMillis() + "|" + Base64.encode(iv);
byte[] sign = signSM2(loadPrivateKey(), signText.getBytes());
// 3. 组装报文
RequestDTO request = new RequestDTO();
request.setHeader(new Header(appId, System.currentTimeMillis()));
request.setBody(Base64.encode(encryptedBody));
request.setSign(Base64.encode(sign));
request.setIv(Base64.encode(iv));
return JSON.toJSONString(request);
}
// 服务端验证
public boolean verifyRequest(RequestDTO request) throws Exception {
// 1. 验签
String signText = request.getHeader().getAppId() + "|"
+ request.getHeader().getTimestamp() + "|"
+ request.getIv();
boolean signValid = verifySM2(loadPublicKey(),
signText.getBytes(),
Base64.decode(request.getSign()));
// 2. 解密body
byte[] decrypted = decryptSM4_CBC(
loadSM4Key(),
Base64.decode(request.getIv()),
Base64.decode(request.getBody())
);
return signValid && (decrypted != null);
}
6. 性能优化与安全实践
6.1 密钥管理方案
- 硬件加密机:生产环境推荐使用HSM设备
- 密钥分级:
- 一级密钥:根密钥(HSM保护)
- 二级密钥:业务密钥(根密钥加密存储)
- 会话密钥:每次请求动态生成
6.2 缓存优化
java复制// 使用ConcurrentHashMap缓存Cipher实例(线程安全)
private static final Map<String, Cipher> cipherCache = new ConcurrentHashMap<>();
public static Cipher getSM4Cipher(int mode, byte[] key, byte[] iv) throws Exception {
String cacheKey = "SM4_" + mode + "_" + Hex.encode(key) + "_" + Hex.encode(iv);
return cipherCache.computeIfAbsent(cacheKey, k -> {
try {
Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS5Padding", "BC");
cipher.init(mode, new SecretKeySpec(key, "SM4"), new IvParameterSpec(iv));
return cipher;
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
6.3 常见问题排查
-
SM2验签失败:
- 检查双方ID参数是否一致
- 确认公钥是否匹配私钥
- 验证签名原文是否完全相同(包括空格等不可见字符)
-
SM4解密异常:
- 确认加密模式(ECB/CBC)一致
- 检查IV值是否匹配加密时使用的值
- 验证密钥是否正确(曾遇到因密钥误用Base64解码导致的错误)
-
性能瓶颈:
- 大量数据加密时建议分块处理(每1MB为一个块)
- 考虑使用Native方法加速(如通过JNI调用GMSSL)
7. 测试验证方案
7.1 单元测试用例
java复制@Test
public void testSM2SignVerify() throws Exception {
String originalText = "测试SM2签名验签";
KeyPair pair = SecureUtil.generateKeyPair("SM2");
byte[] sign = SM2Util.sign(pair.getPrivate().getEncoded(),
originalText.getBytes());
boolean verified = SM2Util.verify(pair.getPublic().getEncoded(),
originalText.getBytes(),
sign);
assertTrue(verified);
}
@Test
public void testSM4EncryptDecrypt() throws Exception {
String originalText = "测试SM4加密解密";
byte[] key = new byte[16];
new SecureRandom().nextBytes(key);
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
byte[] encrypted = SM4Util.encryptSM4_CBC(key, iv,
originalText.getBytes());
byte[] decrypted = SM4Util.decryptSM4_CBC(key, iv, encrypted);
assertEquals(originalText, new String(decrypted));
}
7.2 性能压测数据
使用JMeter测试(单机4核8G):
| 操作类型 | 吞吐量(ops/s) | 平均耗时(ms) |
|---|---|---|
| SM2签名 | 1,200 | 0.83 |
| SM2验签 | 1,500 | 0.67 |
| SM4加密 | 8,000 | 0.125 |
| SM4解密 | 8,500 | 0.118 |
8. 扩展应用场景
8.1 与HTTPS的结合使用
虽然国密算法可以独立使用,但与HTTPS结合能提供更全面的保护:
java复制// 自定义SSLContext(需国密证书支持)
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, trustManagers, secureRandom);
// 在Spring Boot中配置
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCustomizer() {
return factory -> factory.addConnectorCustomizers(connector -> {
connector.setProperty("sslEnabledProtocols", "TLSv1.3");
connector.setProperty("SSLImplementationName",
"org.apache.tomcat.util.net.openssl.OpenSSLImplementation");
connector.setProperty("sslProtocol", "TLS");
connector.setProperty("ciphers",
"ECDHE-SM2-WITH-SMS4-SM3");
});
}
8.2 物联网设备安全通信
典型物联网安全方案:
- 设备预置SM2公私钥对
- 云端存储设备公钥
- 通信流程:
- 设备生成随机SM4会话密钥
- 用SM2加密会话密钥传给云端
- 后续通信使用SM4加密业务数据
8.3 金融行业应用
在支付系统中的典型实现:
java复制public class PaymentSecurityService {
// 商户端签名
public SignedRequest merchantSign(PaymentRequest request) {
byte[] sign = SM2.sign(merchantPrivateKey, request.getDigest());
return new SignedRequest(request, sign);
}
// 银行端验证
public boolean bankVerify(SignedRequest request) {
return SM2.verify(bankPublicKey,
request.getOriginalDigest(),
request.getSignature());
}
// 敏感数据加密存储
public String encryptSensitiveData(String plainText) {
return Base64.encode(SM4.encrypt(
storageKey,
plainText.getBytes()));
}
}
9. 开发工具推荐
9.1 在线验证工具
- SM2签名验证:可使用OpenSSL国密版验证Java生成的签名
- SM4加解密验证:https://the-x.cn/cryptography/Sm4.aspx
9.2 调试技巧
java复制// 调试输出密钥信息
public static void printKeyInfo(Key key) {
System.out.println("Algorithm: " + key.getAlgorithm());
System.out.println("Format: " + key.getFormat());
if (key instanceof ECKey) {
ECParameterSpec spec = ((ECKey)key).getParams();
System.out.println("Curve: " + spec.getCurve());
}
}
9.3 代码生成工具
使用Hutool快速生成测试用例:
java复制// 生成SM2测试密钥对
KeyPair pair = SecureUtil.generateKeyPair("SM2");
FileUtil.writeBytes(pair.getPrivate().getEncoded(), "private.key");
FileUtil.writeBytes(pair.getPublic().getEncoded(), "public.key");
// 生成SM4测试密钥
byte[] key = SecureUtil.generateKey("SM4").getEncoded();
FileUtil.writeBytes(key, "sm4.key");
10. 安全加固建议
-
密钥存储安全:
- 禁止硬编码密钥
- 使用KMS或HSM管理根密钥
- 配置文件中的密钥必须加密
-
防重放攻击:
- 在签名原文中加入timestamp
- 服务端校验时间窗口(如±5分钟)
-
日志脱敏:
java复制// 自动脱敏敏感字段 @Override public String toString() { return "PaymentRequest{" + "amount=" + amount + ", cardNo='" + StringUtil.hide(cardNo, 4, 4) + '\'' + '}'; } -
算法升级预案:
- 设计可插拔的密码组件
- 预留国密算法版本号标识
- 示例:
java复制public interface CryptoService { byte[] encrypt(AlgorithmVersion version, byte[] data); byte[] decrypt(AlgorithmVersion version, byte[] data); }
在实际项目中,我们通过这套国密方案成功对接了多个政府机构和金融机构的接口。最深刻的体会是:密码算法的正确实现只是基础,密钥管理、安全传输、防篡改设计等配套措施同样重要。特别是在分布式环境中,需要特别注意密钥的同步和安全存储问题。
