Skip to content

接口对接方案

本文档描述一单位一平台与子应用之间的接口对接方案,采用无状态的模式,基于国密 SM2 和 SM4 算法实现端到端加密传输。

交互场景

本文档涵盖三种交互场景:

  1. 主系统 → 子应用:一单位一平台主动向子应用发送数据
  2. 子应用 → 主系统:子应用向一单位一平台上报数据或请求服务
  3. 子应用 ↔ 子应用:子应用之间通过一单位一平台中转进行交互

方案背景

为什么选择无状态设计

传统基于 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...
}

报文格式说明

字段类型必填说明
appIdString发送方应用唯一标识,由平台分配
versionString协议版本号,当前为 "1.0"
timestampLong当前时间戳(毫秒),服务器验证时效性(±60秒)
nonceString随机字符串(建议UUID),用于防重放
encryptKeyStringSM2加密的SM4密钥(Base64编码)
encryptDataStringSM4加密的业务数据(Base64编码)
signStringSM2签名(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)

密钥交换流程

  1. 子应用注册时,生成自己的 SM2 密钥对
  2. 子应用将 SM2 公钥上传到主系统(通过线下安全渠道)
  3. 主系统将自己的 SM2 公钥提供给子应用
  4. 双方安全存储对方的公钥和自己的私钥

加密算法说明

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解密失败检查密钥配置和加密算法
1005appId不存在检查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. 准备工作

  1. 向主系统申请 appId
  2. 生成 SM2 密钥对
  3. 将公钥提交给主系统
  4. 获取主系统 SM2 公钥
  5. 配置密钥到应用

2. 开发集成

  1. 引入国密 SDK(如 gmhelperbcprov
  2. 封装 GmPacket 工具类
  3. 实现加解密和签名验签逻辑
  4. 实现拦截器处理请求和响应
  5. 编写单元测试

3. 联调测试

  1. 使用测试环境进行联调
  2. 验证加解密正确性
  3. 验证签名验签正确性
  4. 验证防重放机制
  5. 性能压力测试

4. 上线部署

  1. 配置生产环境密钥
  2. 部署到生产环境
  3. 监控接口调用情况
  4. 处理异常情况

常见问题

Q1: 为什么每次请求都要生成新的SM4密钥?

: 为了提高安全性。如果使用固定的SM4密钥,一旦密钥泄露,所有历史数据都会被解密。每次使用新的随机密钥,即使某次密钥泄露,也只会影响这一次请求的数据。

Q2: 时间戳验证的误差范围可以调整吗?

: 可以。默认是 ±60 秒,可以根据网络延迟和时钟精度调整。建议不要设置过大,以免降低防重放的效果。

Q3: Redis 挂了会影响服务吗?

: 会有影响。建议 Redis 做高可用(主从、哨兵或集群)。另外可以在应用层增加本地缓存作为降级方案。

Q4: 如何更新密钥?

: 密钥更新流程:

  1. 生成新的密钥对
  2. 将新公钥提交给主系统
  3. 协商密钥切换时间
  4. 在约定时间同时切换密钥
  5. 保留旧密钥一段时间,处理延迟到达的请求

Q5: 性能影响有多大?

: 根据实测,单次加解密+验签耗时约 5-15ms。对于大多数业务来说,这个延迟是可以接受的。如果对性能要求极高,可以考虑:

  • 使用硬件加密卡
  • 使用更快的加密库
  • 对非敏感数据降低加密强度

术语对照表

英文术语中文翻译说明
Digital Envelope对称加密和非对称加密结合的混合加密方案
SM2SM2算法国密非对称加密算法(类似RSA)
SM4SM4算法国密对称加密算法(类似AES)
Nonce随机数用于防重放攻击的随机字符串
Replay Attack重放攻击攻击者截获有效请求并重新发送的攻击方式
Timestamp时间戳用于验证请求时效性的时间标识
Signature签名用于防篡改和防抵赖的数字签名
Asymmetric Encryption非对称加密使用公钥加密、私钥解密的加密方式
Symmetric Encryption对称加密使用同一个密钥加密和解密的加密方式

相关资源


文档版本: v1.0.0
最后更新: 2025-12-30

最近更新