主题
接口对接方案
本文档描述一单位一平台与子应用之间的接口对接方案,采用无状态的模式,基于国密 SM2 和 SM4 算法实现端到端加密传输。
交互场景
本文档涵盖三种交互场景:
- 主系统 → 子应用:一单位一平台主动向子应用发送数据
- 子应用 → 主系统:子应用向一单位一平台上报数据或请求服务
- 子应用 ↔ 子应用:子应用之间通过一单位一平台中转进行交互
方案背景
为什么选择无状态设计
传统基于 Session 的长连接状态维护方式在现代容器化环境中面临诸多挑战:
- IP 地址不固定:Kubernetes 等容器环境中,Pod 会动态创建和销毁,IP 地址经常变化
- 水平扩展困难:有状态的服务需要 sticky session 或状态共享,增加架构复杂度
- 单点故障风险:Session 存储在单点会导致服务不可用
- 维护成本高:需要额外维护 Session 存储和同步机制
无状态设计的优势:
- 易扩展:每个请求独立处理,可任意水平扩展
- 高可用:无单点故障,任何实例都能处理请求
- 云原生:完美契合 Kubernetes 等容器化环境的弹性伸缩特性
为什么使用
(Digital Envelope)是一种结合对称加密和非对称加密的混合加密方案:
- 性能优化:使用 SM4 对称加密加密业务数据,加解密速度快
- 安全传输:使用 SM2 非对称加密加密 SM4 密钥,保证密钥传输安全
- 防重放攻击:每次请求使用新的随机密钥,结合时间戳和随机数防重放
- 防抵赖:使用 SM2 签名保证数据不可篡改和发送方不可抵赖
统一报文协议
数据结构
所有业务接口都采用统一的 GmPacket 报文格式:
java
/**
* 统一国密报文包
*/
public class GmPacket {
// ========== 基础信息 ==========
/**
* 发送方应用ID
* 用于标识请求来源系统
*/
private String appId;
/**
* 协议版本号
* 格式: "1.0", "1.1" 等
*/
private String version;
/**
* 请求时间戳(毫秒)
* 用于防重放攻击验证
*/
private Long timestamp;
/**
* 随机数
* 用于防重放攻击验证
*/
private String nonce;
// ========== ==========
/**
* [] SM2加密后的SM4密钥
* 使用接收方SM2公钥加密随机生成的SM4密钥
*/
private String encryptKey;
/**
* [数据信封] SM4加密后的业务数据
* 使用随机生成的SM4密钥加密业务JSON数据
*/
private String encryptData;
/**
* [防抵赖] SM2签名
* 发送方使用SM2私钥对全报文的签名
*/
private String sign;
// getters and setters...
}报文格式说明
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| appId | String | 是 | 发送方应用唯一标识,由平台分配 |
| version | String | 是 | 协议版本号,当前为 "1.0" |
| timestamp | Long | 是 | 当前时间戳(毫秒),服务器验证时效性(±60秒) |
| nonce | String | 是 | 随机字符串(建议UUID),用于防重放 |
| encryptKey | String | 是 | SM2加密的SM4密钥(Base64编码) |
| encryptData | String | 是 | SM4加密的业务数据(Base64编码) |
| sign | String | 是 | SM2签名(Base64编码) |
交互场景详解
场景一:主系统 → 子应用(推送数据)
使用场景:一单位一平台主动向子应用推送数据,如:
- 配置更新通知
- 数据同步指令
- 系统公告
- 任务下发
通讯流程:
关键点:
- encryptKey 使用子应用A的SM2公钥加密,只有子应用A能解密
- sign 使用主系统的SM2私钥签名,子应用A使用主系统公钥验签
- 子应用A需要提前保存主系统的SM2公钥
场景二:子应用 → 主系统(上报数据)
使用场景:子应用向主系统上报数据或请求服务,如:
- 数据上报
- 业务查询
- 资源申请
- 状态同步
通讯流程:
关键点:
- encryptKey 使用主系统的SM2公钥加密,只有主系统能解密
- sign 使用子应用A的SM2私钥签名,主系统使用子应用A的公钥验签
- 主系统需要维护所有子应用的SM2公钥,根据appId动态获取
场景三:子应用 ↔ 子应用(平台中转)
使用场景:子应用之间通过主系统中转进行交互,如:
- 跨应用数据调用
- 跨应用业务流程
- 子应用之间的协作
通讯流程:
关键点:
- 子应用A → 主系统:使用主系统SM2公钥加密,子应用A SM2私钥签名
- 主系统验签解密后,重新封装为新的报文
- 主系统 → 子应用B:使用子应用B的SM2公钥加密,主系统SM2私钥签名
- 响应路径同样经过主系统重新封装
- 子应用A和子应用B不需要互相知道对方的公钥,只需要知道主系统的公钥
业务数据格式(转发请求):
json
{
"targetAppId": "SUB_APP_B",
"method": "getData",
"params": {
"userId": "12345",
"dataType": "userInfo"
}
}场景对比总结
| 对比维度 | 场景一:主→子 | 场景二:子→主 | 场景三:子↔子 |
|---|---|---|---|
| 发起方 | 主系统 | 子应用 | 子应用A |
| 接收方 | 子应用 | 主系统 | 子应用B(通过主系统中转) |
| encryptKey加密 | 子应用公钥 | 主系统公钥 | 主系统公钥(第一段) 子应用B公钥(第二段) |
| sign签名 | 主系统私钥 | 子应用私钥 | 子应用A私钥(第一段) 主系统私钥(第二段) |
| 验签 | 子应用验主系统签名 | 主系统验子应用签名 | 主系统验子应用A签名 子应用B验主系统签名 |
| 密钥数量 | 双方各1对 | 双方各1对 | 三方各1对 |
| 是否中转 | 否 | 否 | 是 |
| 典型用途 | 配置推送、任务下发 | 数据上报、业务查询 | 跨应用协作 |
详细实现代码
通用工具类
SM2 加密工具类
java
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.signers.PlainSM2Signer;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
/**
* SM2 非对称加密工具类
*/
public class SM2Util {
static {
Security.addProvider(new BouncyCastleProvider());
}
/**
* SM2加密
* @param data 明文数据
* @param publicKeyBase64 SM2公钥(Base64编码)
* @return Base64编码的密文
*/
public static String encrypt(String data, String publicKeyBase64) throws Exception {
// 解码公钥
byte[] keyBytes = Base64.getDecoder().decode(publicKeyBase64);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
PublicKey publicKey = keyFactory.generatePublic(keySpec);
// SM2加密
SM2Engine engine = new SM2Engine();
ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(
((java.security.interfaces.ECPublicKey) publicKey).getParams(),
((java.security.interfaces.ECPublicKey) publicKey).getW()
);
engine.init(true, publicKeyParameters);
byte[] encrypted = engine.processBlock(data.getBytes(), 0, data.getBytes().length);
return Base64.getEncoder().encodeToString(encrypted);
}
/**
* SM2解密
* @param encryptedDataBase64 Base64编码的密文
* @param privateKeyBase64 SM2私钥(Base64编码)
* @return 明文数据
*/
public static String decrypt(String encryptedDataBase64, String privateKeyBase64) throws Exception {
// 解码私钥
byte[] keyBytes = Base64.getDecoder().decode(privateKeyBase64);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
// SM2解密
SM2Engine engine = new SM2Engine();
ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(
((java.security.interfaces.ECPrivateKey) privateKey).getS(),
((java.security.interfaces.ECPrivateKey) privateKey).getParams()
);
engine.init(false, privateKeyParameters);
byte[] encryptedData = Base64.getDecoder().decode(encryptedDataBase64);
byte[] decrypted = engine.processBlock(encryptedData, 0, encryptedData.length);
return new String(decrypted);
}
/**
* SM2签名
* @param data 待签名数据
* @param privateKeyBase64 SM2私钥(Base64编码)
* @return Base64编码的签名
*/
public static String sign(String data, String privateKeyBase64) throws Exception {
// 解码私钥
byte[] keyBytes = Base64.getDecoder().decode(privateKeyBase64);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
// SM2签名
PlainSM2Signer signer = new PlainSM2Signer();
ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(
((java.security.interfaces.ECPrivateKey) privateKey).getS(),
((java.security.interfaces.ECPrivateKey) privateKey).getParams()
);
signer.init(true, privateKeyParameters);
signer.update(data.getBytes(), 0, data.getBytes().length);
byte[] signature = signer.generateSignature();
return Base64.getEncoder().encodeToString(signature);
}
/**
* SM2验签
* @param data 原始数据
* @param signBase64 Base64编码的签名
* @param publicKeyBase64 SM2公钥(Base64编码)
* @return 是否验签通过
*/
public static boolean verify(String data, String signBase64, String publicKeyBase64) throws Exception {
// 解码公钥
byte[] keyBytes = Base64.getDecoder().decode(publicKeyBase64);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
PublicKey publicKey = keyFactory.generatePublic(keySpec);
// SM2验签
PlainSM2Signer signer = new PlainSM2Signer();
ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(
((java.security.interfaces.ECPublicKey) publicKey).getParams(),
((java.security.interfaces.ECPublicKey) publicKey).getW()
);
signer.init(false, publicKeyParameters);
signer.update(data.getBytes(), 0, data.getBytes().length);
byte[] signature = Base64.getDecoder().decode(signBase64);
return signer.verifySignature(signature);
}
}SM4 加密工具类
java
import org.bouncycastle.crypto.engines.SM4Engine;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
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.nio.charset.StandardCharsets;
import java.security.*;
import java.util.Base64;
/**
* SM4 对称加密工具类
*/
public class SM4Util {
static {
Security.addProvider(new BouncyCastleProvider());
}
private static final String ALGORITHM = "SM4";
private static final String TRANSFORMATION = "SM4/CBC/PKCS7Padding";
private static final int KEY_SIZE = 128;
/**
* 生成随机SM4密钥
* @return Base64编码的密钥
*/
public static String generateKey() throws Exception {
KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM, "BC");
keyGenerator.init(KEY_SIZE);
SecretKey secretKey = keyGenerator.generateKey();
return Base64.getEncoder().encodeToString(secretKey.getEncoded());
}
/**
* SM4加密
* @param data 明文数据
* @param keyBase64 Base64编码的密钥
* @return Base64编码的密文
*/
public static String encrypt(String data, String keyBase64) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(keyBase64);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, ALGORITHM);
Cipher cipher = Cipher.getInstance(TRANSFORMATION, "BC");
// 生成随机IV
byte[] iv = new byte[16];
SecureRandom random = new SecureRandom();
random.nextBytes(iv);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
// 将IV和密文拼接后返回
byte[] result = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, result, 0, iv.length);
System.arraycopy(encrypted, 0, result, iv.length, encrypted.length);
return Base64.getEncoder().encodeToString(result);
}
/**
* SM4解密
* @param encryptedDataBase64 Base64编码的密文
* @param keyBase64 Base64编码的密钥
* @return 明文数据
*/
public static String decrypt(String encryptedDataBase64, String keyBase64) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(keyBase64);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, ALGORITHM);
byte[] encryptedData = Base64.getDecoder().decode(encryptedDataBase64);
// 提取IV(前16字节)
byte[] iv = new byte[16];
System.arraycopy(encryptedData, 0, iv, 0, 16);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
// 提取密文(16字节之后)
byte[] encrypted = new byte[encryptedData.length - 16];
System.arraycopy(encryptedData, 16, encrypted, 0, encrypted.length);
Cipher cipher = Cipher.getInstance(TRANSFORMATION, "BC");
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
byte[] decrypted = cipher.doFinal(encrypted);
return new String(decrypted, StandardCharsets.UTF_8);
}
}报文封装工具类
java
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 报文工具类
*/
public class GmPacketUtil {
/**
* 构建请求报文
* @param appId 发送方应用ID
* @param businessData 业务数据(JSON字符串)
* @param receiverPublicKey 接收方SM2公钥
* @param senderPrivateKey 发送方SM2私钥
* @return GmPacket报文对象
*/
public static GmPacket buildRequest(String appId,
String businessData,
String receiverPublicKey,
String senderPrivateKey) throws Exception {
// 1. 生成随机SM4密钥
String sm4Key = SM4Util.generateKey();
// 2. SM4加密业务数据
String encryptData = SM4Util.encrypt(businessData, sm4Key);
// 3. SM2加密SM4密钥
String encryptKey = SM2Util.encrypt(sm4Key, receiverPublicKey);
// 4. 生成签名
long timestamp = System.currentTimeMillis();
String nonce = UUID.randomUUID().toString();
String signData = appId + timestamp + encryptData + encryptKey;
String sign = SM2Util.sign(signData, senderPrivateKey);
// 5. 组装报文
GmPacket packet = new GmPacket();
packet.setAppId(appId);
packet.setVersion("1.0");
packet.setTimestamp(timestamp);
packet.setNonce(nonce);
packet.setEncryptKey(encryptKey);
packet.setEncryptData(encryptData);
packet.setSign(sign);
return packet;
}
/**
* 解析请求报文
* @param packet GmPacket报文对象
* @param receiverPrivateKey 接收方SM2私钥
* @param senderPublicKey 发送方SM2公钥
* @return 业务数据(JSON字符串)
*/
public static String parseRequest(GmPacket packet,
String receiverPrivateKey,
String senderPublicKey) throws Exception {
// 1. 验证签名
String signData = packet.getAppId() + packet.getTimestamp()
+ packet.getEncryptData() + packet.getEncryptKey();
boolean signValid = SM2Util.verify(signData, packet.getSign(), senderPublicKey);
if (!signValid) {
throw new SecurityException("签名验证失败");
}
// 2. SM2解密SM4密钥
String sm4Key = SM2Util.decrypt(packet.getEncryptKey(), receiverPrivateKey);
// 3. SM4解密业务数据
String businessData = SM4Util.decrypt(packet.getEncryptData(), sm4Key);
return businessData;
}
}场景一:主系统 → 子应用(完整实现)
主系统代码
java
@Service
public class MainToSubAppService {
@Value("${main.app.id}")
private String mainAppId;
@Value("${main.private.key}")
private String mainPrivateKey;
@Value("${sub.app.public.key}")
private String subAppPublicKey;
@Value("${sub.app.gateway.url}")
private String subAppGatewayUrl;
/**
* 主系统推送数据到子应用
*/
public void pushToSubApp(String method, Map<String, Object> params) {
try {
// 1. 构建业务数据
Map<String, Object> businessData = new HashMap<>();
businessData.put("method", method);
businessData.put("params", params);
String businessJson = JSON.toJSONString(businessData);
// 2. 构建加密报文(使用子应用公钥加密,主系统私钥签名)
GmPacket packet = GmPacketUtil.buildRequest(
mainAppId, // appId
businessJson, // 业务数据
subAppPublicKey, // 接收方公钥(子应用公钥)
mainPrivateKey // 发送方私钥(主系统私钥)
);
// 3. 发送HTTP请求
String response = HttpClient.post(subAppGatewayUrl, packet);
// 4. 处理响应
log.info("推送成功,响应: {}", response);
} catch (Exception e) {
log.error("推送失败", e);
throw new RuntimeException("推送失败: " + e.getMessage());
}
}
}子应用代码
java
@RestController
@RequestMapping("/api")
public class SubAppController {
@Value("${sub.app.id}")
private String subAppId;
@Value("${sub.private.key}")
private String subPrivateKey;
@Value("${main.public.key}")
private String mainPublicKey;
/**
* 子应用接收主系统推送的接口
*/
@PostMapping("/gateway")
public Result<?> handleGateway(@RequestBody GmPacket packet) {
try {
// 1. 解析报文(使用子应用私钥解密,主系统公钥验签)
String businessJson = GmPacketUtil.parseRequest(
packet,
subPrivateKey, // 接收方私钥(子应用私钥)
mainPublicKey // 发送方公钥(主系统公钥)
);
// 2. 解析业务数据
JSONObject businessData = JSON.parseObject(businessJson);
String method = businessData.getString("method");
JSONObject params = businessData.getJSONObject("params");
// 3. 处理业务逻辑
Object result = handleMethod(method, params);
// 4. 构建响应报文
Map<String, Object> responseData = new HashMap<>();
responseData.put("code", 200);
responseData.put("message", "success");
responseData.put("data", result);
String responseJson = JSON.toJSONString(responseData);
GmPacket responsePacket = GmPacketUtil.buildRequest(
subAppId,
responseJson,
mainPublicKey, // 接收方公钥(主系统公钥)
subPrivateKey // 发送方私钥(子应用私钥)
);
return Result.success(responsePacket);
} catch (Exception e) {
log.error("处理请求失败", e);
return Result.error("处理失败: " + e.getMessage());
}
}
private Object handleMethod(String method, JSONObject params) {
switch (method) {
case "updateConfig":
return updateConfig(params);
case "syncData":
return syncData(params);
default:
throw new IllegalArgumentException("未知的方法: " + method);
}
}
}场景二:子应用 → 主系统(完整实现)
子应用代码
java
@Service
public class SubToMainService {
@Value("${sub.app.id}")
private String subAppId;
@Value("${sub.private.key}")
private String subPrivateKey;
@Value("${main.public.key}")
private String mainPublicKey;
@Value("${main.report.url}")
private String mainReportUrl;
/**
* 子应用上报数据到主系统
*/
public <T> T reportToMain(String method, Map<String, Object> params, Class<T> responseType) {
try {
// 1. 构建业务数据
Map<String, Object> businessData = new HashMap<>();
businessData.put("method", method);
businessData.put("params", params);
String businessJson = JSON.toJSONString(businessData);
// 2. 构建加密报文(使用主系统公钥加密,子应用私钥签名)
GmPacket packet = GmPacketUtil.buildRequest(
subAppId, // appId
businessJson, // 业务数据
mainPublicKey, // 接收方公钥(主系统公钥)
subPrivateKey // 发送方私钥(子应用私钥)
);
// 3. 发送HTTP请求
String responseJson = HttpClient.post(mainReportUrl, packet);
JSONObject response = JSON.parseObject(responseJson);
GmPacket responsePacket = response.getJSONObject("data").toJavaObject(GmPacket.class);
// 4. 解析响应报文(使用子应用私钥解密,主系统公钥验签)
String resultJson = GmPacketUtil.parseRequest(
responsePacket,
subPrivateKey, // 接收方私钥(子应用私钥)
mainPublicKey // 发送方公钥(主系统公钥)
);
return JSON.parseObject(resultJson, responseType);
} catch (Exception e) {
log.error("上报失败", e);
throw new RuntimeException("上报失败: " + e.getMessage());
}
}
}主系统代码
java
@RestController
@RequestMapping("/api")
public class MainSystemController {
@Value("${main.app.id}")
private String mainAppId;
@Value("${main.private.key}")
private String mainPrivateKey;
@Autowired
private KeyService keyService;
/**
* 主系统接收子应用上报的接口
*/
@PostMapping("/report")
public Result<?> handleReport(@RequestBody GmPacket packet) {
try {
// 1. 根据appId获取子应用公钥
String subAppPublicKey = keyService.getPublicKey(packet.getAppId());
if (subAppPublicKey == null) {
return Result.error(1005, "appId不存在");
}
// 2. 解析报文(使用主系统私钥解密,子应用公钥验签)
String businessJson = GmPacketUtil.parseRequest(
packet,
mainPrivateKey, // 接收方私钥(主系统私钥)
subAppPublicKey // 发送方公钥(子应用公钥)
);
// 3. 解析业务数据
JSONObject businessData = JSON.parseObject(businessJson);
String method = businessData.getString("method");
JSONObject params = businessData.getJSONObject("params");
// 4. 处理业务逻辑
Object result = handleMethod(method, params);
// 5. 构建响应报文
Map<String, Object> responseData = new HashMap<>();
responseData.put("code", 200);
responseData.put("message", "success");
responseData.put("data", result);
String responseJson = JSON.toJSONString(responseData);
// 获取子应用公钥用于加密响应
String subAppPublicKeyForResponse = keyService.getPublicKey(packet.getAppId());
GmPacket responsePacket = GmPacketUtil.buildRequest(
mainAppId,
responseJson,
subAppPublicKeyForResponse, // 接收方公钥(子应用公钥)
mainPrivateKey // 发送方私钥(主系统私钥)
);
return Result.success(responsePacket);
} catch (Exception e) {
log.error("处理上报失败", e);
return Result.error("处理失败: " + e.getMessage());
}
}
}场景三:子应用 ↔ 子应用(完整实现)
子应用A代码
java
@Service
public class SubAppAService {
@Value("${sub.app.a.id}")
private String subAppAId;
@Value("${sub.app.a.private.key}")
private String subAppAPrivateKey;
@Value("${main.public.key}")
private String mainPublicKey;
@Value("${main.forward.url}")
private String mainForwardUrl;
/**
* 子应用A通过主系统调用子应用B
*/
public <T> T callSubAppB(String targetAppId, String method,
Map<String, Object> params, Class<T> responseType) {
try {
// 1. 构建业务数据(包含目标应用ID)
Map<String, Object> businessData = new HashMap<>();
businessData.put("targetAppId", targetAppId);
businessData.put("method", method);
businessData.put("params", params);
String businessJson = JSON.toJSONString(businessData);
// 2. 构建加密报文(使用主系统公钥加密,子应用A私钥签名)
GmPacket packet = GmPacketUtil.buildRequest(
subAppAId, // appId
businessJson, // 业务数据
mainPublicKey, // 接收方公钥(主系统公钥)
subAppAPrivateKey // 发送方私钥(子应用A私钥)
);
// 3. 发送到主系统的转发接口
String responseJson = HttpClient.post(mainForwardUrl, packet);
JSONObject response = JSON.parseObject(responseJson);
GmPacket responsePacket = response.getJSONObject("data").toJavaObject(GmPacket.class);
// 4. 解析响应报文(使用子应用A私钥解密,主系统公钥验签)
String resultJson = GmPacketUtil.parseRequest(
responsePacket,
subAppAPrivateKey, // 接收方私钥(子应用A私钥)
mainPublicKey // 发送方公钥(主系统公钥)
);
return JSON.parseObject(resultJson, responseType);
} catch (Exception e) {
log.error("调用子应用B失败", e);
throw new RuntimeException("调用失败: " + e.getMessage());
}
}
}主系统转发代码
java
@RestController
@RequestMapping("/api")
public class MainSystemForwardController {
@Value("${main.app.id}")
private String mainAppId;
@Value("${main.private.key}")
private String mainPrivateKey;
@Autowired
private KeyService keyService;
/**
* 主系统转发接口(子应用A → 子应用B)
*/
@PostMapping("/forward")
public Result<?> forwardToSubApp(@RequestBody GmPacket packet) {
try {
// ========== 第一段:子应用A → 主系统 ==========
// 1. 获取子应用A的公钥
String subAppAPublicKey = keyService.getPublicKey(packet.getAppId());
if (subAppAPublicKey == null) {
return Result.error(1005, "发送方appId不存在");
}
// 2. 解析报文(使用主系统私钥解密,子应用A公钥验签)
String businessJson = GmPacketUtil.parseRequest(
packet,
mainPrivateKey, // 接收方私钥(主系统私钥)
subAppAPublicKey // 发送方公钥(子应用A公钥)
);
// 3. 解析业务数据
JSONObject businessData = JSON.parseObject(businessJson);
String targetAppId = businessData.getString("targetAppId");
String method = businessData.getString("method");
JSONObject params = businessData.getJSONObject("params");
// 4. 获取目标子应用B的信息
String subAppBPublicKey = keyService.getPublicKey(targetAppId);
String subAppBUrl = keyService.getGatewayUrl(targetAppId);
if (subAppBPublicKey == null || subAppBUrl == null) {
return Result.error(1005, "目标应用不存在");
}
// ========== 第二段:主系统 → 子应用B ==========
// 5. 重新构建业务数据(移除targetAppId)
Map<String, Object> forwardData = new HashMap<>();
forwardData.put("method", method);
forwardData.put("params", params);
String forwardJson = JSON.toJSONString(forwardData);
// 6. 重新构建加密报文(使用子应用B公钥加密,主系统私钥签名)
GmPacket forwardPacket = GmPacketUtil.buildRequest(
mainAppId, // appId
forwardJson, // 业务数据
subAppBPublicKey, // 接收方公钥(子应用B公钥)
mainPrivateKey // 发送方私钥(主系统私钥)
);
// 7. 转发到子应用B
String subAppBResponseJson = HttpClient.post(subAppBUrl, forwardPacket);
JSONObject subAppBResponse = JSON.parseObject(subAppBResponseJson);
GmPacket subAppBResponsePacket = subAppBResponse.getJSONObject("data").toJavaObject(GmPacket.class);
// ========== 第三段:子应用B → 主系统 → 子应用A ==========
// 8. 解析子应用B的响应(使用主系统私钥解密,子应用B公钥验签)
String resultJson = GmPacketUtil.parseRequest(
subAppBResponsePacket,
mainPrivateKey, // 接收方私钥(主系统私钥)
subAppBPublicKey // 发送方公钥(子应用B公钥)
);
// 9. 重新构建响应报文(使用子应用A公钥加密,主系统私钥签名)
GmPacket responsePacket = GmPacketUtil.buildRequest(
mainAppId,
resultJson,
subAppAPublicKey, // 接收方公钥(子应用A公钥)
mainPrivateKey // 发送方私钥(主系统私钥)
);
return Result.success(responsePacket);
} catch (Exception e) {
log.error("转发失败", e);
return Result.error("转发失败: " + e.getMessage());
}
}
}安全机制
1. 防重放攻击
时间戳验证:
- 服务器验证请求时间戳与当前时间的差值
- 超过 ±60 秒的请求会被拒绝
- 防止攻击者截获旧请求后重放
Nonce 验证:
- 每个请求包含唯一的随机数(nonce)
- 服务器使用 Redis 存储已使用的 nonce(5分钟过期)
- 收到重复 nonce 的请求会被拒绝
- 结合时间戳实现双重防护
2. 防篡改
数字签名:
- 发送方使用 SM2 私钥对完整报文签名
- 接收方使用发送方 SM2 公钥验签
- 任何字段被篡改都会导致验签失败
3. 防窃听
端到端加密:
- 业务数据使用随机 SM4 密钥加密
- SM4 密钥使用接收方 SM2 公钥加密
- 只有拥有对应私钥的接收方才能解密
- 每次请求使用新的随机密钥,即使某次密钥泄露也不影响其他请求
4. 密钥管理
密钥分配:
子应用系统:
- SM2密钥对(SUB_APP_PRIVATE_KEY, SUB_APP_PUBLIC_KEY)
- 主系统SM2公钥(MAIN_SYSTEM_PUBLIC_KEY)
主系统平台:
- SM2密钥对(MAIN_SYSTEM_PRIVATE_KEY, MAIN_SYSTEM_PUBLIC_KEY)
- 所有子应用的SM2公钥(SUB_APP_PUBLIC_KEY)密钥交换流程:
- 子应用注册时,生成自己的 SM2 密钥对
- 子应用将 SM2 公钥上传到主系统(通过线下安全渠道)
- 主系统将自己的 SM2 公钥提供给子应用
- 双方安全存储对方的公钥和自己的私钥
加密算法说明
SM2 非对称加密
- 密钥长度:256位
- 用途:
- 加密 SM4 密钥()
- 签名和验签(防篡改)
- 特点:
- 仅支持小数据量加密(密钥长度)
- 计算速度较慢
- 安全性高
SM4 对称加密
- 密钥长度:128位
- 模式:ECB 或 CBC
- 用途:
- 加密业务数据(数据信封)
- 特点:
- 支持大数据量加密
- 计算速度快
- 需要安全传输密钥(通过 SM2 加密)
报文示例
请求报文示例
json
{
"appId": "SUB_APP_001",
"version": "1.0",
"timestamp": 1735516800000,
"nonce": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"encryptKey": "MIIBOgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu5+5sN+r3pFzPZ0DE9l1l4cJ9sJ5wKj8r8UCAwEAAQJAQPJxOl+m0qUjV5K8M5ZJL8qN3sEfDJ5KL8qN3sEfDJ5KL8qN3sEfDJ5KL8qN3sEfDJ5KQJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu5+5sN+r3pFzPZ0DE9l1l4cJ9sJ5wKj8r8U=",
"encryptData": "U2FsdGVkX1+jmZn5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5Kg==",
"sign": "MEUCIQDZ5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPgIhAM5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQ=="
}响应报文示例
json
{
"appId": "MAIN_SYSTEM",
"version": "1.0",
"timestamp": 1735516801500,
"nonce": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"encryptKey": "MIIBOgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu5+5sN+r3pFzPZ0DE9l1l4cJ9sJ5wKj8r8UCAwEAAQJAQPJxOl+m0qUjV5K8M5ZJL8qN3sEfDJ5KL8qN3sEfDJ5KL8qN3sEfDJ5KL8qN3sEfDJ5KQJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu5+5sN+r3pFzPZ0DE9l1l4cJ9sJ5wKj8r8U=",
"encryptData": "U2FsdGVkX1+jmZn5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5Kg==",
"sign": "MEUCIQDZ5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPgIhAM5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQKpH5KqWYEgjPkQ=="
}异常处理
常见错误码
| 错误码 | 说明 | 处理建议 |
|---|---|---|
| 1001 | 时间戳过期 | 检查客户端时间是否同步 |
| 1002 | 重复请求 | 检查nonce生成逻辑,确保每次请求唯一 |
| 1003 | 签名验证失败 | 检查密钥配置和签名算法 |
| 1004 | 解密失败 | 检查密钥配置和加密算法 |
| 1005 | appId不存在 | 检查appId是否正确注册 |
| 1006 | 业务逻辑错误 | 根据具体错误信息处理 |
异常响应格式
json
{
"code": 1003,
"message": "签名验证失败",
"timestamp": 1735516801000
}性能优化建议
1. 密钥缓存
- 在应用启动时加载所有公钥到内存
- 定期刷新密钥缓存(如每小时)
- 使用 Map 存储密钥,提高查询速度
java
@Component
public class KeyCache {
private final Map<String, String> publicKeyCache = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
loadPublicKeys();
}
@Scheduled(cron = "0 0 * * * ?")
public void refresh() {
loadPublicKeys();
}
public String getPublicKey(String appId) {
return publicKeyCache.get(appId);
}
}2. Redis 连接池
- 使用 Redis 连接池提高性能
- 合理设置连接池大小
- 使用 lettuce 或 jedis 客户端
3. 异步处理
- 对于非关键业务,可以异步处理
- 使用消息队列解耦加解密和业务处理
- 提高系统吞吐量
接入步骤
1. 准备工作
- 向主系统申请 appId
- 生成 SM2 密钥对
- 将公钥提交给主系统
- 获取主系统 SM2 公钥
- 配置密钥到应用
2. 开发集成
- 引入国密 SDK(如
gmhelper或bcprov) - 封装
GmPacket工具类 - 实现加解密和签名验签逻辑
- 实现拦截器处理请求和响应
- 编写单元测试
3. 联调测试
- 使用测试环境进行联调
- 验证加解密正确性
- 验证签名验签正确性
- 验证防重放机制
- 性能压力测试
4. 上线部署
- 配置生产环境密钥
- 部署到生产环境
- 监控接口调用情况
- 处理异常情况
常见问题
Q1: 为什么每次请求都要生成新的SM4密钥?
答: 为了提高安全性。如果使用固定的SM4密钥,一旦密钥泄露,所有历史数据都会被解密。每次使用新的随机密钥,即使某次密钥泄露,也只会影响这一次请求的数据。
Q2: 时间戳验证的误差范围可以调整吗?
答: 可以。默认是 ±60 秒,可以根据网络延迟和时钟精度调整。建议不要设置过大,以免降低防重放的效果。
Q3: Redis 挂了会影响服务吗?
答: 会有影响。建议 Redis 做高可用(主从、哨兵或集群)。另外可以在应用层增加本地缓存作为降级方案。
Q4: 如何更新密钥?
答: 密钥更新流程:
- 生成新的密钥对
- 将新公钥提交给主系统
- 协商密钥切换时间
- 在约定时间同时切换密钥
- 保留旧密钥一段时间,处理延迟到达的请求
Q5: 性能影响有多大?
答: 根据实测,单次加解密+验签耗时约 5-15ms。对于大多数业务来说,这个延迟是可以接受的。如果对性能要求极高,可以考虑:
- 使用硬件加密卡
- 使用更快的加密库
- 对非敏感数据降低加密强度
术语对照表
| 英文术语 | 中文翻译 | 说明 |
|---|---|---|
| Digital Envelope | 对称加密和非对称加密结合的混合加密方案 | |
| SM2 | SM2算法 | 国密非对称加密算法(类似RSA) |
| SM4 | SM4算法 | 国密对称加密算法(类似AES) |
| Nonce | 随机数 | 用于防重放攻击的随机字符串 |
| Replay Attack | 重放攻击 | 攻击者截获有效请求并重新发送的攻击方式 |
| Timestamp | 时间戳 | 用于验证请求时效性的时间标识 |
| Signature | 签名 | 用于防篡改和防抵赖的数字签名 |
| Asymmetric Encryption | 非对称加密 | 使用公钥加密、私钥解密的加密方式 |
| Symmetric Encryption | 对称加密 | 使用同一个密钥加密和解密的加密方式 |
相关资源
文档版本: v1.0.0
最后更新: 2025-12-30