API接口加密认证
采用 SM2非对称加密 + SM4对称加密 + SM3摘要签名 的国密算法组合,确保API调用的安全性。
SM2数字签名,用于验证数据上传对象是否合法,是否是在服务端备案的客户端。
SM4对称加密业务数据:将客户端上传的业务数据进行加密,防止业务数据明文暴露出去。
SM3消息摘要签名:对业务数据进行hash计算,生成摘要签名,防止业务数据被人篡改。
流程
客户端与服务端各自生成一对SM2公私密钥对。
服务端保留服务端的SM2私钥(
serverPrivateKey),公开服务端的SM2公钥(serverPublicKey)。客户端保留客户端的SM2私钥(
clientPrivateKey),公开客户端的SM2公钥(clientPublicKey)。客户端上传数据到服务端--客户端操作
生成SM4对称加密的秘钥。(得到SM4对称秘钥
encryptedKey)使用SM4对称加密需要上传的业务数据。(得到加密业务数据
encryptedData,和SM4的随机值iv)使用服务端的SM2公钥加密客户端生成的SM4秘钥
encryptedKey。(得到encryptedSm4Key)使用SM3算法计算需要上传的业务数据摘要。(得到
requestDigest)获取当前时间的时间戳。(得到
timestamp)使用SM2签名,对(clientId客户端在服务端备案的标识 + ":" + timestamp + ":" + requestDigest)的组合数据,使用客户端私钥
clientPrivateKey进行签名。(得到signature)组装数据
{ "clientId": "客户端在服务端备案的标识", "timestamp": "时间戳", "encryptedData": "SM4加密业务数据后的结果", "iv": "SM4加密业务数据时的随机数", "encryptedKey": "被加密后的SM4秘钥 encryptedSm4Key", "digest": "SM3计算出的业务数据摘要requestDigest", "signature": "SM2签名signature" }发送请求到服务端。
客户端上传数据到服务端--服务端操作
- 校验时间戳是否超过30秒,超过30秒拒绝接收。
- 判断
clientId是否有在服务端备案,没有备案拒绝接收请求。- 服务端根据
clientId获取客户端在服务端备案的客户端公钥clientPublicKey。- 验证签名: 组合请求参数里的数据(clientId + ":" + timestamp + ":" + digest),然后结合
signature和clientPublicKey,使用SM2进行验签。如果验签不通过,则说明签名有问题,服务端接收到的这份数据不是客户端原本上传的,摘要、时间戳、客户端标识、签名有可能被人篡改了,拒绝接收请求。- 使用服务端的SM2私钥
serverPrivateKey,通过SM2算法解密encryptedKey,获取到客户端上传的SM4的秘钥decryptedSm4Key。- 使用SM4秘钥
decryptedSm4Key、iv、encryptedData, 通过SM4算法解密出业务数据decryptedData。- 验证业务数据的完整性,验证数据是否有被篡改。通过SM3算法,计算业务数据
decryptedData的Hash结果,然后和客户端上传的digest进行比较,如果数据不一致,则说明数据被篡改,直接拒绝接收请求。- 到此结束,完成了客户端身份验证,完成了数据时间有效性验证,完成了数据安全未变更验证。接下来也可正常进行业务数据的处理。
案例
1. 秘钥配置类
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 国密安全配置
*/
@Data
@Component
@ConfigurationProperties(prefix = "sm")
public class SMProperties {
/**
* 国密配置
* Map<客户端标识clientId, 客户端SM2公钥>
*/
private Map<String, String> client;
/**
* 国密配置
* 服务端SM2公钥
*/
private String serverPublicKey;
/**
* 国密配置
* 服务端SM2私钥
*/
private String serverPrivateKey;
}在 application.properties 的配置案例
sm.client.71aafd0a75704b1881507fbb6e83eccd=MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEJ9ugTx95K+nD5z2hG3ignMGF2cOr7cuheP7mU0OdTeV8Og1NLAv+O+V7QcC59n+47QL2rPMn6hSocpDfLpkfsA==
sm.server-public-key=MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEtjV5EVXKwowoqUfCjtTDgIIjyKBCmNu3w5YP28s3N83IgXUmzz+6eXxMcc7+nLwJfQUk5zyH2EoMHapk1+N9pw==
sm.server-private-key=MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgWscAvwwQOs3FrU265vq0KZHcgBS3PA0jKe4IGNBgujigCgYIKoEcz1UBgi2hRANCAAS2NXkRVcrCjCipR8KO1MOAgiPIoEKY27fDlg/byzc3zciBdSbPP7p5fExxzv6cvAl9BSTnPIfYSgwdqmTX432n2. 请求入参公共类
安全认证基础参数类
import com.alibaba.fastjson.JSONObject;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
/**
* 安全认证基础参数
*/
@Data
public class BaseParams implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 客户端标识
*/
@NotBlank(message = "客户端标识不能为空")
private String clientId;
/**
* 随机值
*/
@NotBlank(message = "随机值不能为空")
private String iv;
/**
* 业务加密数据
*/
@NotBlank(message = "业务数据不能为空")
private String encryptedData;
/**
* 对称密钥
*/
@NotBlank(message = "对称密钥不能为空")
private String encryptedKey;
/**
* 业务数据摘要
*/
@NotBlank(message = "业务数据摘要不能为空")
private String digest;
/**
* 签名
*/
@NotBlank(message = "签名不能为空")
private String signature;
/**
* 时间戳
*/
@NotNull(message = "时间戳不能为空")
private Long timestamp;
@Override
public String toString() {
return JSONObject.toJSONString(this);
}
}入参公共类
import com.alibaba.fastjson.JSONObject;
import lombok.Data;
import javax.validation.Valid;
import java.io.Serializable;
/**
* 请求参数
*/
@Data
public class SmParamsDto<T> extends BaseParams implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 业务数据
*/
@Valid
private T data;
@Override
public String toString() {
String str = super.toString();
JSONObject jsonObject = JSONObject.parseObject(str);
jsonObject.put("data", data);
return jsonObject.toJSONString();
}
}3. SM算法工具类
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.util.Base64;
/**
* 国密算法工具类
*/
public class SMUtil {
static {
Security.addProvider(new BouncyCastleProvider());
}
private static final String PROVIDER = "BC";
private static final String SM2_CURVE_NAME = "sm2p256v1";
private static final String SM4_ALGORITHM = "SM4";
private static final String SM4_MODE = "SM4/CBC/PKCS5Padding";
private static final int SM4_KEY_SIZE = 128;
private static final int SM4_IV_LENGTH = 16;
// ==================== SM2 非对称加密 ====================
/**
* 生成SM2密钥对
*/
public static KeyPair generateSM2KeyPair() throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC", PROVIDER);
ECGenParameterSpec sm2Spec = new ECGenParameterSpec(SM2_CURVE_NAME);
keyPairGenerator.initialize(sm2Spec);
return keyPairGenerator.generateKeyPair();
}
/**
* SM2加密
*/
public static String sm2Encrypt(byte[] data, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance("SM2", PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encrypted = cipher.doFinal(data);
return Base64.getEncoder().encodeToString(encrypted);
}
/**
* SM2解密
*/
public static byte[] sm2Decrypt(String encryptedData, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance("SM2", PROVIDER);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] data = Base64.getDecoder().decode(encryptedData);
return cipher.doFinal(data);
}
/**
* SM2签名
*/
public static String sm2Sign(byte[] data, PrivateKey privateKey) throws Exception {
Signature signature = Signature.getInstance("SM3withSM2", PROVIDER);
signature.initSign(privateKey);
signature.update(data);
byte[] sign = signature.sign();
return Base64.getEncoder().encodeToString(sign);
}
/**
* SM2验签
*/
public static boolean sm2Verify(byte[] data, String sign, PublicKey publicKey) throws Exception {
Signature signature = Signature.getInstance("SM3withSM2", PROVIDER);
signature.initVerify(publicKey);
signature.update(data);
return signature.verify(Base64.getDecoder().decode(sign));
}
// ==================== SM3 摘要算法 ====================
/**
* SM3哈希计算
*/
public static String sm3Hash(byte[] data) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SM3", PROVIDER);
byte[] hash = digest.digest(data);
return Base64.getEncoder().encodeToString(hash);
}
/**
* SM3哈希计算(十六进制输出)
*/
public static String sm3HashHex(byte[] data) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SM3", PROVIDER);
byte[] hash = digest.digest(data);
return bytesToHex(hash);
}
// ==================== SM4 对称加密 ====================
/**
* 生成SM4密钥
*/
public static SecretKey generateSM4Key() throws Exception {
KeyGenerator keyGenerator = KeyGenerator.getInstance(SM4_ALGORITHM, PROVIDER);
keyGenerator.init(SM4_KEY_SIZE);
return keyGenerator.generateKey();
}
/**
* SM4加密
*/
public static SM4EncryptResult sm4Encrypt(byte[] data, SecretKey key) throws Exception {
byte[] iv = new byte[SM4_IV_LENGTH];
SecureRandom random = new SecureRandom();
random.nextBytes(iv);
Cipher cipher = Cipher.getInstance(SM4_MODE, PROVIDER);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
byte[] encrypted = cipher.doFinal(data);
return new SM4EncryptResult(encrypted, iv);
}
/**
* SM4解密
*/
public static byte[] sm4Decrypt(byte[] encryptedData, byte[] iv, SecretKey key) throws Exception {
Cipher cipher = Cipher.getInstance(SM4_MODE, PROVIDER);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
return cipher.doFinal(encryptedData);
}
// ==================== 辅助方法 ====================
private static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
/**
* 密钥序列化
*/
public static String keyToString(Key key) {
return Base64.getEncoder().encodeToString(key.getEncoded());
}
/**
* 从字符串恢复SM4密钥
*/
public static SecretKey sm4KeyFromString(String keyStr) {
byte[] keyBytes = Base64.getDecoder().decode(keyStr);
return new SecretKeySpec(keyBytes, SM4_ALGORITHM);
}
/**
* SM4加密结果封装类
*/
public static class SM4EncryptResult {
private byte[] encryptedData;
private byte[] iv;
public SM4EncryptResult(byte[] encryptedData, byte[] iv) {
this.encryptedData = encryptedData;
this.iv = iv;
}
public byte[] getEncryptedData() {
return encryptedData;
}
public byte[] getIv() {
return iv;
}
}
}4. 接口加密校验注解
import java.lang.annotation.*;
/**
* 国密安全加密认证
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SMSecurity {
/**
* 参数是集合
*
* @return true:集合 false:非集合
*/
boolean isList() default false;
/**
* 参数类型
*
* @return 参数类型
*/
Class<?> type() default String.class;
}5. 请求参数校验与解密切面类
import com.alibaba.fastjson.JSONObject;
import net.sf.json.JSONArray;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.lang.reflect.Field;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.*;
import java.util.stream.Collectors;
/**
* SMSecurity国密注解增强
*
* @author 3hgh
* @date 2025/10/15 10:59
*/
@Aspect
@Component
public class SMSecurityAspect {
private final SMProperties smProperties;
public SMSecurityAspect(SMProperties smProperties) {
this.smProperties = smProperties;
}
@Pointcut(value = "@annotation(security)")
public void pointcut(SMSecurity security) {
}
@Around(value = "pointcut(security)")
public Object around(ProceedingJoinPoint joinPoint, SMSecurity security) throws Throwable {
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) {
throw new RuntimeException("参数不能为空");
}
String paramsStr = args[0].toString();
BaseParams baseParams = JSONObject.parseObject(paramsStr, BaseParams.class);
if (baseParams == null) {
throw new BusinessException("参数不能为空");
}
// 校验时间戳距离当前是否大于30秒
if (System.currentTimeMillis() - baseParams.getTimestamp() > 30 * 1000) {
throw new BusinessException("数据存在风险,拒绝接收");
}
// 判断是否有给这个客户端配置密钥
if (!smProperties.getClient().containsKey(baseParams.getClientId())) {
throw new BusinessException(baseParams.getClientId() + "客户端未备案,不可访问接口");
}
Security.addProvider(new BouncyCastleProvider());
KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
// 服务端私钥
String serverPrivateKeyStr = smProperties.getServerPrivateKey();
byte[] serverPrivateKeyBytes = Base64.getDecoder().decode(serverPrivateKeyStr); // 解码Base64私钥
PrivateKey serverPrivateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(serverPrivateKeyBytes)); // 创建SM2私钥对象
// 客户端公钥
String clientPublicKeyStr = smProperties.getClient().get(baseParams.getClientId());
byte[] clientPublicKeyBytes = Base64.getDecoder().decode(clientPublicKeyStr); // 解码Base64公钥
PublicKey clientPublicKey = keyFactory.generatePublic(new X509EncodedKeySpec(clientPublicKeyBytes)); // 创建SM2公钥对象
// 验证签名
String signatureValidStr = baseParams.getClientId() + ":" + baseParams.getTimestamp() + ":" + baseParams.getDigest();
boolean signValid = SMUtil.sm2Verify(signatureValidStr.getBytes("UTF-8"), baseParams.getSignature(), clientPublicKey);
if (!signValid) {
// 数据签名验证失败
throw new BusinessException("数据存在风险,拒绝接收");
}
// 解密SM4密钥(用服务端SM2私钥,因为客户端是用服务端的SM2公钥进行的加密)
byte[] decryptedKeyBytes = SMUtil.sm2Decrypt(baseParams.getEncryptedKey(), serverPrivateKey);
SecretKey decryptedSm4Key = new SecretKeySpec(decryptedKeyBytes, "SM4");
// 解密业务数据
byte[] decryptedData = SMUtil.sm4Decrypt(
Base64.getDecoder().decode(baseParams.getEncryptedData()),
Base64.getDecoder().decode(baseParams.getIv()),
decryptedSm4Key
);
// 验证数据完整性
String decryptedDigest = SMUtil.sm3HashHex(decryptedData);
boolean digestValid = decryptedDigest.equals(baseParams.getDigest());
if (!digestValid) {
// 数据被篡改
throw new BusinessException("数据存在风险,拒绝接收");
}
// 将解密后的数据作为参数传递给目标方法
Field field = joinPoint.getArgs()[0].getClass().getDeclaredField("data");
field.setAccessible(true);
String params = JSONObject.parseObject(decryptedData, String.class);
Class<?> type = security.type();
if (security.isList()) {
field.set(joinPoint.getArgs()[0], JSONObject.parseArray(params, type));
} else {
field.set(joinPoint.getArgs()[0], JSONObject.parseObject(params, type));
}
// 重新触发Spring验证
// 获取 Validator 实例
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
for (Object arg : args) {
if (arg != null && hasValidationAnnotations(arg)) {
Set<ConstraintViolation<Object>> violations = validator.validate(arg);
if (!violations.isEmpty()) {
String errorMsg = violations.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining("; "));
throw new BusinessException(errorMsg);
}
}
}
return joinPoint.proceed();
}
private boolean hasValidationAnnotations(Object obj) {
// 检查对象是否有验证注解
return Arrays.stream(obj.getClass().getDeclaredFields())
.anyMatch(field -> field.getAnnotations().length > 0);
}
}6. 服务端数据接收接口
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 数据上传接口
*/
@RestController
@RequestMapping("/demo")
public class DemoController {
private final DemoService demoService;
public DemoController(DemoService demoService) {
this.demoService = demoService;
}
/**
* 数据上传接口
*/
@SMSecurity(type = DemoDto.class)
@PostMapping("/upload")
public Result upload(@RequestBody SmParamsDto<DemoDto> dto) {
String id = demoService.save(dto);
return Result.ok(id);
}
}7. 客户端请求示例
public void demo(@RequestBody JSONObject json) throws Exception {
Security.addProvider(new BouncyCastleProvider());
KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
String clientId = "71aafd0a75704b1881507fbb6e83eccd";
String timestamp = String.valueOf(System.currentTimeMillis());
// 客户端私钥
String clientPrivateKeyStr = "MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQg6Is85KPS/q1s5V6y1ycF6/7rTWSvsRHlXiAKBxuKluigCgYIKoEcz1UBgi2hRANCAAQn26BPH3kr6cPnPaEbeKCcwYXZw6vty6F4/uZTQ51N5Xw6DU0sC/475XtBwLn2f7jtAvas8yfqFKhykN8umR+w";
byte[] clientPrivateKeyBytes = Base64.getDecoder().decode(clientPrivateKeyStr); // 解码Base64私钥
PrivateKey clientPrivateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(clientPrivateKeyBytes)); // 创建SM2私钥对象
// 服务端公钥
String serverPublicKeyStr = "MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEtjV5EVXKwowoqUfCjtTDgIIjyKBCmNu3w5YP28s3N83IgXUmzz+6eXxMcc7+nLwJfQUk5zyH2EoMHapk1+N9pw==";
byte[] serverPublicKeyBytes = Base64.getDecoder().decode(serverPublicKeyStr); // 解码Base64公钥
PublicKey serverPublicKey = keyFactory.generatePublic(new X509EncodedKeySpec(serverPublicKeyBytes)); // 创建SM2公钥对象
// 1. 客户端构建请求
String requestJson = JSONObject.toJSONString(json);
// 2. 使用SM4加密业务数据
SecretKey sm4Key = SMUtil.generateSM4Key();
SMUtil.SM4EncryptResult sm4Result = SMUtil.sm4Encrypt(requestJson.getBytes("UTF-8"), sm4Key);
// 3. 使用SM2加密SM4密钥(密钥交换)(使用服务端的SM2公钥)
String encryptedSm4Key = SMUtil.sm2Encrypt(sm4Key.getEncoded(), serverPublicKey);
// 4. 使用SM3计算请求摘要
String requestDigest = SMUtil.sm3HashHex(requestJson.getBytes("UTF-8"));
// 5. 使用SM2对摘要签名(客户端的SM2私钥)
String signatureStr = clientId + ":" + timestamp + ":" + requestDigest;
String signature = SMUtil.sm2Sign(signatureStr.getBytes("UTF-8"), clientPrivateKey);
// 客户端发送到服务端的数据
Map<String, String> clientData = new HashMap<String, String>();
// 加密业务数据
clientData.put("encryptedData", Base64.getEncoder().encodeToString(sm4Result.getEncryptedData()));
// SM4的随机值,确保相同明文加密结果不同
clientData.put("iv", Base64.getEncoder().encodeToString(sm4Result.getIv()));
// 被加密后的SM4密钥
clientData.put("encryptedKey", encryptedSm4Key);
// 业务数据摘要
clientData.put("digest", requestDigest);
// 客户端签名
clientData.put("signature", signature);
// 客户端Id
clientData.put("clientId", clientId);
// 时间戳
clientData.put("timestamp", timestamp);
JSONObject json = JSONObject.parseObject(JSONObject.toJSONString(clientData));
ResponseEntity<JSONObject> exchange = new RestTemplate().postForEntity("http://xxxx/demo/upload", params, JSONObject.class);
}理论说明
1. 国密算法组件替换
算法对应关系
| 国际算法 | 国密算法 | 用途 |
|---|---|---|
| RSA | SM2 | 非对称加密、数字签名 |
| AES | SM4 | 对称加密 |
| SHA256 | SM3 | 消息摘要 |
| - | SM9 | 标识密码算法(可选) |
2. 核心实现方案
2.1 国密算法工具类
首先添加国密算法依赖:
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
<version>1.72</version>
</dependency>@Component
public class SM2CryptoService {
private static final String PROVIDER = "BC";
private static final String SM2_ALGORITHM = "SM2";
private static final String SM4_ALGORITHM = "SM4";
private static final String SM4_MODE = "SM4/CBC/PKCS5Padding";
static {
Security.addProvider(new BouncyCastleProvider());
}
/**
* 生成SM2密钥对
*/
public KeyPair generateSM2KeyPair() {
try {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(SM2_ALGORITHM, PROVIDER);
keyGen.initialize(256); // SM2使用256位密钥
return keyGen.generateKeyPair();
} catch (Exception e) {
throw new RuntimeException("生成SM2密钥对失败", e);
}
}
/**
* SM2加密
*/
public byte[] sm2Encrypt(byte[] data, PublicKey publicKey) {
try {
Cipher cipher = Cipher.getInstance(SM2_ALGORITHM, PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(data);
} catch (Exception e) {
throw new RuntimeException("SM2加密失败", e);
}
}
/**
* SM2解密
*/
public byte[] sm2Decrypt(byte[] data, PrivateKey privateKey) {
try {
Cipher cipher = Cipher.getInstance(SM2_ALGORITHM, PROVIDER);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(data);
} catch (Exception e) {
throw new RuntimeException("SM2解密失败", e);
}
}
}2.2 SM4对称加密服务
@Service
public class SM4CryptoService {
private static final String PROVIDER = "BC";
private static final String SM4_ALGORITHM = "SM4";
private static final String SM4_MODE = "SM4/CBC/PKCS5Padding";
private static final int IV_LENGTH = 16; // SM4 IV长度为16字节
static {
Security.addProvider(new BouncyCastleProvider());
}
/**
* 生成SM4密钥
*/
public SecretKey generateSM4Key() {
try {
KeyGenerator keyGen = KeyGenerator.getInstance(SM4_ALGORITHM, PROVIDER);
keyGen.init(128); // SM4使用128位密钥
return keyGen.generateKey();
} catch (Exception e) {
throw new RuntimeException("生成SM4密钥失败", e);
}
}
/**
* 从字节数组生成SM4密钥
*/
public SecretKey generateSM4Key(byte[] keyBytes) {
return new SecretKeySpec(keyBytes, SM4_ALGORITHM);
}
/**
* SM4加密
*/
public SM4EncryptResult sm4Encrypt(byte[] data, SecretKey key) {
try {
// 生成IV
byte[] iv = new byte[IV_LENGTH];
SecureRandom random = new SecureRandom();
random.nextBytes(iv);
Cipher cipher = Cipher.getInstance(SM4_MODE, PROVIDER);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
byte[] encrypted = cipher.doFinal(data);
return new SM4EncryptResult(encrypted, iv);
} catch (Exception e) {
throw new RuntimeException("SM4加密失败", e);
}
}
/**
* SM4解密
*/
public byte[] sm4Decrypt(byte[] encryptedData, byte[] iv, SecretKey key) {
try {
Cipher cipher = Cipher.getInstance(SM4_MODE, PROVIDER);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
return cipher.doFinal(encryptedData);
} catch (Exception e) {
throw new RuntimeException("SM4解密失败", e);
}
}
@Data
@AllArgsConstructor
public static class SM4EncryptResult {
private byte[] encryptedData;
private byte[] iv;
}
}2.3 SM3签名服务
@Service
public class SM3SignatureService {
private static final String PROVIDER = "BC";
private static final String SM3_ALGORITHM = "SM3";
private static final String SM2_SIGN_ALGORITHM = "SM3withSM2";
static {
Security.addProvider(new BouncyCastleProvider());
}
/**
* 生成SM3摘要
*/
public byte[] sm3Hash(byte[] data) {
try {
MessageDigest digest = MessageDigest.getInstance(SM3_ALGORITHM, PROVIDER);
return digest.digest(data);
} catch (Exception e) {
throw new RuntimeException("SM3摘要计算失败", e);
}
}
/**
* 生成SM2签名(使用SM3摘要)
*/
public String sm2Sign(String data, PrivateKey privateKey) {
try {
Signature signature = Signature.getInstance(SM2_SIGN_ALGORITHM, PROVIDER);
signature.initSign(privateKey);
signature.update(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(signature.sign());
} catch (Exception e) {
throw new RuntimeException("SM2签名失败", e);
}
}
/**
* 验证SM2签名
*/
public boolean sm2Verify(String data, String signature, PublicKey publicKey) {
try {
Signature sig = Signature.getInstance(SM2_SIGN_ALGORITHM, PROVIDER);
sig.initVerify(publicKey);
sig.update(data.getBytes(StandardCharsets.UTF_8));
return sig.verify(Base64.getDecoder().decode(signature));
} catch (Exception e) {
throw new RuntimeException("SM2签名验证失败", e);
}
}
/**
* 生成带时间戳的签名字符串
*/
public String buildSignString(String clientId, String timestamp, String nonce,
String encryptedData, String apiKey, String method, String path) {
// 按照固定顺序拼接参数,确保签名一致性
return String.join("|", clientId, timestamp, nonce, encryptedData, apiKey, method, path);
}
/**
* 生成请求摘要(用于完整性校验)
*/
public String generateRequestDigest(Map<String, String> params) {
try {
// 对参数进行排序后拼接
String paramString = params.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.joining("&"));
byte[] digest = sm3Hash(paramString.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(digest);
} catch (Exception e) {
throw new RuntimeException("生成请求摘要失败", e);
}
}
}2.4 国密密钥管理器
@Component
public class SMKeyManager {
private final Map<String, SMClientInfo> clientRepository;
private final KeyPair serverKeyPair;
@Autowired
private SM2CryptoService sm2CryptoService;
public SMKeyManager() {
this.serverKeyPair = generateServerKeyPair();
this.clientRepository = initClientRepository();
}
/**
* 客户端信息(国密版)
*/
@Data
public static class SMClientInfo {
private String clientId; // 客户端ID
private String clientName; // 客户端名称
private String apiKey; // API密钥
private String apiSecret; // API密钥(SM4加密存储)
private PublicKey sm2PublicKey; // 客户端SM2公钥
private PrivateKey sm2PrivateKey; // 客户端SM2私钥(加密存储)
private Date issueTime; // 颁发时间
private Date expireTime; // 过期时间
private List<String> permissions; // 权限列表
private boolean enabled; // 是否启用
private String keyVersion; // 密钥版本(支持轮换)
}
/**
* 生成服务端SM2密钥对
*/
private KeyPair generateServerKeyPair() {
return sm2CryptoService.generateSM2KeyPair();
}
/**
* 注册新客户端
*/
public SMClientInfo registerClient(String clientName, List<String> permissions) {
try {
// 生成客户端SM2密钥对
KeyPair clientKeyPair = sm2CryptoService.generateSM2KeyPair();
// 生成API Key和Secret
String apiKey = generateApiKey();
String apiSecret = generateApiSecret();
SMClientInfo clientInfo = new SMClientInfo();
clientInfo.setClientId(generateClientId());
clientInfo.setClientName(clientName);
clientInfo.setApiKey(apiKey);
clientInfo.setApiSecret(encryptApiSecret(apiSecret));
clientInfo.setSm2PublicKey(clientKeyPair.getPublic());
clientInfo.setSm2PrivateKey(encryptPrivateKey(clientKeyPair.getPrivate()));
clientInfo.setIssueTime(new Date());
clientInfo.setExpireTime(Date.from(Instant.now().plus(365, ChronoUnit.DAYS)));
clientInfo.setPermissions(permissions);
clientInfo.setEnabled(true);
clientInfo.setKeyVersion("v1");
clientRepository.put(clientInfo.getClientId(), clientInfo);
return clientInfo;
} catch (Exception e) {
throw new RuntimeException("注册客户端失败", e);
}
}
/**
* 获取客户端公钥证书(X.509格式)
*/
public byte[] getClientCertificate(String clientId) {
try {
SMClientInfo clientInfo = clientRepository.get(clientId);
if (clientInfo == null) {
throw new RuntimeException("客户端不存在");
}
// 生成简单的X.509证书(实际应使用CA签发)
return generateSM2Certificate(clientInfo);
} catch (Exception e) {
throw new RuntimeException("获取客户端证书失败", e);
}
}
private byte[] generateSM2Certificate(SMClientInfo clientInfo) throws Exception {
// 简化的证书生成,实际应使用完整的X.509v3证书
String certInfo = String.format(
"CN=%s,OU=API Client,O=Company,C=CN",
clientInfo.getClientId()
);
return certInfo.getBytes(StandardCharsets.UTF_8);
}
// 其他辅助方法...
private String generateClientId() {
return "CLIENT_" + UUID.randomUUID().toString().replace("-", "").substring(0, 16).toUpperCase();
}
private String generateApiKey() {
return "AK_" + UUID.randomUUID().toString().replace("-", "").substring(0, 24).toUpperCase();
}
private String generateApiSecret() {
return UUID.randomUUID().toString().replace("-", "") +
UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}
private String encryptApiSecret(String secret) {
// 使用服务端SM2私钥加密存储(实际应使用HSM)
byte[] encrypted = sm2CryptoService.sm2Encrypt(
secret.getBytes(StandardCharsets.UTF_8), serverKeyPair.getPublic());
return Base64.getEncoder().encodeToString(encrypted);
}
private PrivateKey encryptPrivateKey(PrivateKey privateKey) {
// 私钥加密存储逻辑
return privateKey;
}
}2.5 国密API认证拦截器
@Component
public class SMApiAuthInterceptor implements HandlerInterceptor {
@Autowired private SMKeyManager smKeyManager;
@Autowired private SM2CryptoService sm2CryptoService;
@Autowired private SM4CryptoService sm4CryptoService;
@Autowired private SM3SignatureService sm3SignatureService;
@Autowired private ReplayAttackService replayAttackService;
private static final Set<String> ALLOWED_SIGNATURE_METHODS =
Set.of("SM3withSM2", "SM3");
private static final Set<String> ALLOWED_ENCRYPT_METHODS =
Set.of("SM4", "SM2");
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
try {
// 1. 解析国密API请求
SMApiRequest apiRequest = parseRequest(request);
// 2. 基础验证
if (!validateRequest(apiRequest)) {
return buildErrorResponse(response, "INVALID_REQUEST", "请求格式错误");
}
// 3. 验证客户端
SMKeyManager.SMClientInfo clientInfo = smKeyManager.getClientInfo(
apiRequest.getHeader().getClientId());
if (clientInfo == null || !clientInfo.isEnabled()) {
return buildErrorResponse(response, "INVALID_CLIENT", "客户端不存在或已禁用");
}
// 4. 验证API Key
if (!validateApiKey(apiRequest, clientInfo)) {
return buildErrorResponse(response, "INVALID_API_KEY", "API Key验证失败");
}
// 5. 验证时间戳和Nonce
SMApiRequest.RequestHeader header = apiRequest.getHeader();
if (!replayAttackService.validateTimestampAndNonce(header.getTimestamp(), header.getNonce())) {
return buildErrorResponse(response, "REPLAY_ATTACK", "请求可能为重放攻击");
}
// 6. 解密SM4密钥
SecretKey sm4Key = decryptSm4Key(header.getEncryptKey(), clientInfo);
// 7. 验证SM2签名
if (!validateSMSignature(apiRequest, clientInfo, sm4Key, request)) {
return buildErrorResponse(response, "INVALID_SIGNATURE", "SM2签名验证失败");
}
// 8. 解密业务数据
String decryptedData = decryptBusinessData(apiRequest.getEncryptedData(), sm4Key, header);
request.setAttribute("decryptedData", decryptedData);
request.setAttribute("clientInfo", clientInfo);
// 9. 记录审计日志
logAuditInfo(request, clientInfo, true);
return true;
} catch (Exception e) {
logAuditInfo(request, null, false);
return buildErrorResponse(response, "SM_AUTH_ERROR", "国密认证异常: " + e.getMessage());
}
}
private boolean validateSMSignature(SMApiRequest apiRequest,
SMKeyManager.SMClientInfo clientInfo,
SecretKey sm4Key,
HttpServletRequest request) {
SMApiRequest.RequestHeader header = apiRequest.getHeader();
// 构建待签名字符串(包含请求方法路径)
String signString = sm3SignatureService.buildSignString(
header.getClientId(),
header.getTimestamp(),
header.getNonce(),
apiRequest.getEncryptedData(),
header.getApiKey(),
request.getMethod(),
request.getRequestURI()
);
// 验证SM2签名
return sm3SignatureService.sm2Verify(signString, apiRequest.getSignature(),
clientInfo.getSm2PublicKey());
}
private SecretKey decryptSm4Key(String encryptedKeyBase64, SMKeyManager.SMClientInfo clientInfo) {
try {
// 使用服务端SM2私钥解密
byte[] encryptedKey = Base64.getDecoder().decode(encryptedKeyBase64);
byte[] sm4KeyBytes = sm2CryptoService.sm2Decrypt(encryptedKey,
smKeyManager.getServerPrivateKey());
return sm4CryptoService.generateSM4Key(sm4KeyBytes);
} catch (Exception e) {
throw new RuntimeException("解密SM4密钥失败", e);
}
}
// 其他辅助方法...
}2.6 国密API请求响应结构
/**
* 国密API请求封装
*/
@Data
public class SMApiRequest<T> {
// 请求头
private RequestHeader header;
// SM4加密的业务数据(Base64编码)
private String encryptedData;
// SM2数字签名(Base64编码)
private String signature;
// 请求摘要(SM3,可选)
private String digest;
@Data
public static class RequestHeader {
private String clientId; // 客户端ID
private String apiKey; // API密钥
private String timestamp; // 时间戳 ISO格式
private String nonce; // 随机数
private String version; // API版本
private String encryptKey; // SM2加密的SM4密钥(Base64)
private String signatureMethod; // 签名算法(SM3withSM2)
private String encryptMethod; // 加密算法(SM4/SM2)
private String keyVersion; // 密钥版本
}
}
/**
* 国密API响应封装
*/
@Data
public class SMApiResponse<T> {
private ResponseHeader header;
private String encryptedData;
private String signature;
private String digest;
private boolean success;
private String errorCode;
private String errorMessage;
@Data
public static class ResponseHeader {
private String responseId;
private String timestamp;
private String clientId;
private String encryptMethod;
private String signatureMethod;
}
}3. 客户端调用示例(国密版)
@Service
public class SMApiClient {
@Autowired
private SM2CryptoService sm2CryptoService;
@Autowired
private SM4CryptoService sm4CryptoService;
@Autowired
private SM3SignatureService sm3SignatureService;
/**
* 调用国密API
*/
public <T> SMApiResponse<T> callSMApi(String url, Object requestData,
Class<T> responseType, String method, String path) {
try {
// 1. 准备请求数据
String requestJson = objectMapper.writeValueAsString(requestData);
// 2. 生成SM4密钥
SecretKey sm4Key = sm4CryptoService.generateSM4Key();
// 3. SM4加密业务数据
SM4CryptoService.SM4EncryptResult encryptResult = sm4CryptoService.sm4Encrypt(
requestJson.getBytes(StandardCharsets.UTF_8), sm4Key);
// 4. SM2加密SM4密钥
byte[] encryptedKey = sm2CryptoService.sm2Encrypt(
sm4Key.getEncoded(), serverSM2PublicKey);
// 5. 生成SM2签名
String timestamp = Instant.now().toString();
String nonce = UUID.randomUUID().toString();
String encryptedDataBase64 = Base64.getEncoder().encodeToString(encryptResult.getEncryptedData());
String signString = sm3SignatureService.buildSignString(
clientId, timestamp, nonce, encryptedDataBase64, apiKey, method, path);
String signature = sm3SignatureService.sm2Sign(signString, clientSM2PrivateKey);
// 6. 生成请求摘要
Map<String, String> params = new HashMap<>();
params.put("clientId", clientId);
params.put("timestamp", timestamp);
params.put("nonce", nonce);
String digest = sm3SignatureService.generateRequestDigest(params);
// 7. 组装国密请求
SMApiRequest<Object> smRequest = new SMApiRequest<>();
SMApiRequest.RequestHeader header = new SMApiRequest.RequestHeader();
header.setClientId(clientId);
header.setApiKey(apiKey);
header.setTimestamp(timestamp);
header.setNonce(nonce);
header.setVersion("1.0");
header.setEncryptKey(Base64.getEncoder().encodeToString(encryptedKey));
header.setSignatureMethod("SM3withSM2");
header.setEncryptMethod("SM4");
header.setKeyVersion("v1");
smRequest.setHeader(header);
smRequest.setEncryptedData(encryptedDataBase64);
smRequest.setSignature(signature);
smRequest.setDigest(digest);
// 8. 发送请求
// ... HTTP客户端调用
return parseSMResponse(response, responseType);
} catch (Exception e) {
throw new RuntimeException("国密API调用失败", e);
}
}
}4. 安全增强特性
4.1 国密算法优势
- 国家认证:符合国家密码管理局标准
- 自主可控:避免对国际算法的依赖
- 同等安全:SM2-256与RSA-2048安全性相当
- 性能优化:在某些场景下性能优于国际算法
4.2 密钥安全管理
- 支持SM2密钥定期轮换
- 使用HSM硬件保护根密钥
- 实现密钥生命周期管理
4.3 合规性要求
- 满足《网络安全法》要求
- 符合金融、政务等行业密码应用要求
- 支持国家商用密码检测认证
这个国密算法方案提供了与国际算法方案同等的安全级别,同时满足国家密码法规要求,适合在对密码算法有国产化要求的场景中使用。
