Java非对称加密实战:RSA/SM2签名验签与密钥管理全指南

选题说明:非对称加密的核心应用场景不是"加密数据",而是"密钥交换"和"数字签名"。RSA 和 SM2 在 API 对接、电子合同、支付回调等场景中广泛使用,但密钥管理混乱、签名算法选择不当、密文格式不兼容等问题经常导致对接失败。本文从业务视角出发,覆盖 RSA/SM2 的完整 API 和真实对接场景。


一、非对称加密的典型业务场景

非对称加密(公钥加密、私钥解密)在项目中的核心价值:

  1. API 签名验签:开放平台对接(微信支付、支付宝、钉钉)需要对请求签名,防止数据篡改
  2. 密钥交换:用对方公钥加密对称密钥,传输给对方,实现"数字信封"
  3. 数字证书:HTTPS、电子合同、发票签章基于非对称加密实现身份认证
  4. 国密合规:政务/金融系统要求使用 SM2 替代 RSA

与对称加密的区别:

特性 对称加密(AES/SM4) 非对称加密(RSA/SM2)
密钥 单一密钥,双方共享 公钥+私钥,成对使用
速度 快(MB/s 级) 慢(KB/s 级)
数据量 支持任意大小 受密钥长度限制
用途 数据加密 密钥交换、数字签名

选型建议:

  • 加密大量数据 → 对称加密(AES)
  • 密钥交换/签名验签 → 非对称加密(RSA/SM2)
  • 国密合规 → SM2
  • 通用场景 → RSA-2048

环境准备:

<!-- Spring Boot 2.x (javax) -->
<dependency>
    <groupId>com.gitee.apanlh</groupId>
    <artifactId>pan-common</artifactId>
    <version>2.0.6</version>
</dependency>

<!-- Spring Boot 3.x (jakarta) -->
<dependency>
    <groupId>com.gitee.apanlh</groupId>
    <artifactId>pan-common</artifactId>
    <version>3.0.6</version>
</dependency>

<!-- SM2 国密算法需要 Bouncy Castle -->
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk18on</artifactId>
    <version>1.84</version>
</dependency>

二、RSA 加密与签名实战

2.1 生成密钥对

// 生成默认 1024 位 RSA 密钥对(仅用于测试,生产环境不推荐)
RSA rsa = new RSA();
byte[] publicKey = rsa.getPublicEncode();
byte[] privateKey = rsa.getPrivateEncode();

// 生成 2048 位密钥对(生产推荐)
RSA rsa2048 = new RSA(2048);

// 生成 4096 位密钥对(高安全场景)
RSA rsa4096 = new RSA(4096);

密钥长度选择:

  • 1024 位:已被破解,仅用于测试
  • 2048 位:当前推荐,安全性足够
  • 4096 位:高安全场景,性能较慢

2.2 加密解密

// 使用已有密钥加密/解密
RSA rsa = new RSA(publicKey, privateKey);
byte[] plain = "Hello RSA".getBytes(StandardCharsets.UTF_8);

// 加密
byte[] cipher = rsa.encrypt(plain);

// 解密
byte[] decrypted = rsa.decrypt(cipher);
System.out.println(new String(decrypted)); // "Hello RSA"

加密块大小限制:

  • 2048 位密钥(256 字节)+ PKCS1Padding → 最大加密 245 字节
  • 超过限制需要分段加密或使用混合加密(对称密钥 + RSA 加密密钥)

2.3 从字符串加载密钥

// 从 Base64 字符串加载(自动识别)
String pubBase64 = rsa.getPublicEncodeToBase64Str();
String priBase64 = rsa.getPrivateEncodeToBase64Str();
RSA rsa2 = new RSA(pubBase64, priBase64);

// 从 Hex 字符串加载
String pubHex = rsa.getPublicEncodeToHex();
String priHex = rsa.getPrivateEncodeToHex();
RSA rsa3 = new RSA(pubHex, priHex);

// 从模数和指数还原公钥
BigInteger modulus = rsa.getModulus();
BigInteger pubExp = rsa.getPublicExponent();
RSA rsa4 = new RSA(modulus, pubExp, null); // 第三个参数为私钥指数,可 null

// 从 X.509/PKCS#8 字节数组加载
byte[] pubBytes = rsa.getPublicEncode();
byte[] priBytes = rsa.getPrivateEncode();
RSA rsa5 = new RSA(pubBytes, priBytes);

2.4 签名验签

RSA rsa = new RSA(publicKey, privateKey);
byte[] data = "待签名数据".getBytes(StandardCharsets.UTF_8);

// 签名(默认 SHA256withRSA)
byte[] signature = rsa.sign(data);
String signHex = rsa.signToHex(data);
String signBase64 = rsa.signToBase64Str(data);

// 验签
boolean valid1 = rsa.verify(data, signature);
boolean valid2 = rsa.verifyFromHex(data, signHex);
boolean valid3 = rsa.verifyFromBase64Str(data, signBase64);

System.out.println("验签结果: " + valid1); // true

指定签名算法:

// 使用 SHA512withRSA(更高安全性)
RSA rsaPss = new RSA(publicKey, privateKey, 
    "RSA/ECB/PKCS1Padding", "SHA512withRSA");
byte[] sigPss = rsaPss.sign(data);

签名算法选择:

  • SHA256withRSA:通用推荐
  • SHA384withRSA / SHA512withRSA:高安全场景
  • SHA1withRSA:已不推荐,仅兼容旧系统

2.5 自定义加密算法(OAEP)

// 使用 OAEP 填充(比 PKCS1 更安全)
RSA rsaOaep = new RSA(publicKey, privateKey, 
    "RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
byte[] cipherOaep = rsaOaep.encrypt(plain);
byte[] plainOaep = rsaOaep.decrypt(cipherOaep);

2.6 导出密钥为不同格式

// 字节数组
byte[] pubEncoded = rsa.getPublicEncode();
byte[] priEncoded = rsa.getPrivateEncode();

// Hex 字符串
String pubHex = rsa.getPublicEncodeToHex();
String priHex = rsa.getPrivateEncodeToHex();

// Base64 字符串
String pubBase64Str = rsa.getPublicEncodeToBase64Str();
String priBase64Str = rsa.getPrivateEncodeToBase64Str();

// Base64 字节数组
byte[] pubBase64Bytes = rsa.getPublicEncodeToBase64();

2.7 实战:微信支付回调验签

@RestController
@RequestMapping("/api/payment")
public class PaymentCallbackController {
    
    @Value("${wechat.pay.public-key}")
    private String wechatPublicKeyBase64;
    
    /**
     * 微信支付回调接口
     */
    @PostMapping("/wechat/callback")
    public String wechatCallback(
            @RequestBody String requestBody,
            @RequestHeader("Wechatpay-Signature") String signature,
            @RequestHeader("Wechatpay-Timestamp") String timestamp,
            @RequestHeader("Wechatpay-Nonce") String nonce) {
        
        // 1. 构造待验签字符串:timestamp\nnonce\nrequestBody\n
        String message = timestamp + "\n" + nonce + "\n" + requestBody + "\n";
        
        // 2. 创建 RSA 验签器(仅需要公钥)
        RSA rsa = new RSA(wechatPublicKeyBase64, null);
        
        // 3. 验签
        boolean valid = rsa.verifyFromBase64Str(
            message.getBytes(StandardCharsets.UTF_8), 
            signature
        );
        
        if (!valid) {
            log.warn("微信支付回调验签失败");
            return "{\"code\":\"FAIL\",\"message\":\"验签失败\"}";
        }
        
        // 4. 验签通过,处理业务逻辑
        WechatPayNotification notification = JSON.parseObject(
            requestBody, WechatPayNotification.class);
        
        paymentService.handleWechatPaySuccess(notification);
        
        return "{\"code\":\"SUCCESS\",\"message\":\"成功\"}";
    }
}

2.8 实战:API 接口签名(开放平台)

@Service
public class ApiSignatureService {
    
    @Value("${api.private-key}")
    private String privateKeyBase64;
    
    @Value("${api.partner-public-key}")
    private String partnerPublicKeyBase64;
    
    /**
     * 对 API 请求签名(发送给合作方)
     */
    public Map<String, String> signRequest(Map<String, Object> params) {
        // 1. 参数排序并拼接
        String sortedParams = params.entrySet().stream()
            .sorted(Map.Entry.comparingByKey())
            .map(e -> e.getKey() + "=" + e.getValue())
            .collect(Collectors.joining("&"));
        
        // 2. 用私钥签名
        RSA rsa = new RSA(null, privateKeyBase64); // 仅需要私钥
        String signature = rsa.signToBase64Str(
            sortedParams.getBytes(StandardCharsets.UTF_8)
        );
        
        // 3. 返回参数 + 签名
        Map<String, String> result = new HashMap<>();
        params.forEach((k, v) -> result.put(k, String.valueOf(v)));
        result.put("sign", signature);
        result.put("sign_type", "RSA");
        
        return result;
    }
    
    /**
     * 验证合作方回调的签名
     */
    public boolean verifyCallback(Map<String, String> params) {
        String signature = params.remove("sign");
        String signType = params.remove("sign_type");
        
        // 1. 参数排序并拼接
        String sortedParams = params.entrySet().stream()
            .sorted(Map.Entry.comparingByKey())
            .map(e -> e.getKey() + "=" + e.getValue())
            .collect(Collectors.joining("&"));
        
        // 2. 用对方公钥验签
        RSA rsa = new RSA(partnerPublicKeyBase64, null); // 仅需要公钥
        return rsa.verifyFromBase64Str(
            sortedParams.getBytes(StandardCharsets.UTF_8),
            signature
        );
    }
}

使用示例:

// 发送请求
Map<String, Object> params = new HashMap<>();
params.put("order_id", "202506040001");
params.put("amount", 100.00);
params.put("timestamp", System.currentTimeMillis());

Map<String, String> signedParams = apiSignatureService.signRequest(params);
// 发送 signedParams 到合作方 API

// 接收回调
@PostMapping("/callback")
public String callback(@RequestParam Map<String, String> params) {
    if (!apiSignatureService.verifyCallback(params)) {
        return "验签失败";
    }
    // 处理业务
    return "success";
}

2.9 实战:数字信封(RSA + AES 混合加密)

@Service
public class DigitalEnvelopeService {
    
    @Value("${partner.public-key}")
    private String partnerPublicKeyBase64;
    
    @Value("${my.private-key}")
    private String myPrivateKeyBase64;
    
    /**
     * 加密大文件(RSA 加密 AES 密钥 + AES 加密数据)
     */
    public EncryptedEnvelope encryptFile(File file) throws IOException {
        // 1. 生成随机 AES 密钥
        byte[] aesKey = KeyUtils.generate256(); // 32 字节
        AES aes = new AES(aesKey);
        
        // 2. 用 AES 加密文件
        File encryptedFile = new File(file.getAbsolutePath() + ".enc");
        aes.encrypt(file, encryptedFile);
        
        // 3. 用对方 RSA 公钥加密 AES 密钥
        RSA rsa = new RSA(partnerPublicKeyBase64, null);
        byte[] encryptedAesKey = rsa.encrypt(aesKey);
        
        // 4. 返回数字信封
        EncryptedEnvelope envelope = new EncryptedEnvelope();
        envelope.setEncryptedFile(encryptedFile);
        envelope.setEncryptedKey(Base64.getEncoder().encodeToString(encryptedAesKey));
        
        return envelope;
    }
    
    /**
     * 解密数字信封
     */
    public File decryptEnvelope(EncryptedEnvelope envelope) throws IOException {
        // 1. 用私钥解密 AES 密钥
        RSA rsa = new RSA(null, myPrivateKeyBase64);
        byte[] encryptedAesKey = Base64.getDecoder().decode(envelope.getEncryptedKey());
        byte[] aesKey = rsa.decrypt(encryptedAesKey);
        
        // 2. 用 AES 密钥解密文件
        AES aes = new AES(aesKey);
        File decryptedFile = new File(
            envelope.getEncryptedFile().getAbsolutePath().replace(".enc", ".dec")
        );
        aes.decrypt(envelope.getEncryptedFile(), decryptedFile);
        
        return decryptedFile;
    }
}

// 数字信封对象
@Data
public class EncryptedEnvelope {
    private File encryptedFile;
    private String encryptedKey; // Base64 编码的加密 AES 密钥
}

优势:

  • RSA 只加密短小的 AES 密钥(32 字节),速度快
  • AES 加密大文件,支持 GB 级数据
  • 结合两者优势,实现安全高效的大数据加密

三、SM2 国密加密实战

3.1 生成密钥对

// 生成密钥对(默认 C1C3C2 模式)
SM2 sm2 = SM2.create();
byte[] publicKey = sm2.getPublicEncode();
byte[] privateKey = sm2.getPrivateEncode();

// 指定加密模式
SM2 sm2C1C2C3 = SM2.create(SM2Mode.C1C2C3); // 旧标准
SM2 sm2C1C3C2 = SM2.create(SM2Mode.C1C3C2); // 新标准(推荐)

加密模式选择:

  • C1C3C2:新国标,推荐使用
  • C1C2C3:旧标准,兼容老系统

3.2 加密解密

// 仅公钥加密
SM2 sm2Enc = SM2.create(publicKey, null);
byte[] plain = "Hello SM2".getBytes(StandardCharsets.UTF_8);
byte[] cipher = sm2Enc.encrypt(plain);

// 仅私钥解密
SM2 sm2Dec = SM2.create(null, privateKey);
byte[] decrypted = sm2Dec.decrypt(cipher);
System.out.println(new String(decrypted)); // "Hello SM2"

// 字符串便捷方法
String cipherHex = sm2Enc.encryptToHex("Hello SM2");
String plainStr = sm2Dec.decryptFromHexStr(cipherHex);

3.3 从原始 Q 值/D 值加载

// 获取原始 Q 值(公钥点)和 D 值(私钥)
ECPoint q = sm2.getEcPoint();
BigInteger d = sm2.getD();

// 从 Q/D 值创建
SM2 sm2ByQ = SM2.create(q, null); // 仅公钥加密
SM2 sm2ByD = SM2.create(null, d); // 仅私钥解密

3.4 签名验签

SM2 sm2 = SM2.create(publicKey, privateKey);
byte[] data = "待签名数据".getBytes();

// 签名(默认使用随机数)
byte[] signature = sm2.sign(data);
String signHex = sm2.signToHex(data);

// 验签
boolean valid = sm2.verify(data, signature);
boolean validFromHex = sm2.verifyFromHex(data, signHex);

System.out.println("验签结果: " + valid); // true

自定义用户 ID 和随机数:

// 自定义用户 ID(默认 "1234567812345678")
byte[] customUserId = "CustomUser".getBytes();

// 关闭随机数(确定性签名,用于测试)
SM2 sm2Custom = new SM2(publicKey, privateKey, 
    SM2Mode.C1C3C2, customUserId, false);

byte[] sig2 = sm2Custom.sign(data);
boolean valid2 = sm2Custom.verify(data, sig2);

用户 ID 说明:

  • SM2 签名标准要求使用用户 ID
  • 默认值 "1234567812345678" 为国密推荐值
  • 与外部系统对接时,双方必须使用相同的用户 ID

3.5 导出密钥

// Base64 字符串
String pubBase64 = sm2.getPublicEncodeToBase64Str();
String priBase64 = sm2.getPrivateEncodeToBase64Str();

// Hex 字符串
String pubHex = sm2.getPublicEncodeToHex();
String priHex = sm2.getPrivateEncodeToHex();

// 字节数组
byte[] pubBytes = sm2.getPublicEncode();
byte[] priBytes = sm2.getPrivateEncode();

3.6 密钥对对象(AsymmetricKey)

// 创建密钥对对象(默认 Base64 编码)
AsymmetricKey keyPair = sm2.createKeyObject();

// 指定 Hex 编码
AsymmetricKey keyPairHex = sm2.createKeyObject(true);

// 获取字符串
String storedPub = keyPair.getPublicKeyStr();
String storedPri = keyPair.getPrivateKeyStr();

// 解码为字节数组
keyPair.decode();
byte[] pub = keyPair.getPublicKey();
byte[] pri = keyPair.getPrivateKey();

3.7 实战:政务系统电子签章

@Service
public class GovDocumentSignService {
    
    @Value("${gov.sm2.private-key}")
    private String govPrivateKeyBase64;
    
    @Value("${gov.sm2.public-key}")
    private String govPublicKeyBase64;
    
    /**
     * 对政府公文进行电子签章
     */
    public SignedDocument signDocument(Document doc) {
        // 1. 计算文档摘要(SM3)
        SM3 sm3 = DigestUtils.sm3();
        String docContent = doc.getTitle() + doc.getContent() + doc.getIssueDate();
        byte[] docHash = sm3.digest(docContent.getBytes(StandardCharsets.UTF_8));
        
        // 2. 用私钥签名
        SM2 sm2 = SM2.create(null, govPrivateKeyBase64);
        String signature = sm2.signToHex(docHash);
        
        // 3. 构造签章文档
        SignedDocument signedDoc = new SignedDocument();
        signedDoc.setDocument(doc);
        signedDoc.setSignature(signature);
        signedDoc.setSignerId("GOV-001");
        signedDoc.setSignTime(LocalDateTime.now().toString());
        
        return signedDoc;
    }
    
    /**
     * 验证电子签章
     */
    public boolean verifySignature(SignedDocument signedDoc) {
        // 1. 计算文档摘要
        Document doc = signedDoc.getDocument();
        SM3 sm3 = DigestUtils.sm3();
        String docContent = doc.getTitle() + doc.getContent() + doc.getIssueDate();
        byte[] docHash = sm3.digest(docContent.getBytes(StandardCharsets.UTF_8));
        
        // 2. 用公钥验签
        SM2 sm2 = SM2.create(govPublicKeyBase64, null);
        return sm2.verifyFromHex(docHash, signedDoc.getSignature());
    }
}

@Data
public class SignedDocument {
    private Document document;
    private String signature;
    private String signerId;
    private String signTime;
}

3.8 实战:国密 HTTPS 证书验证

@Component
public class SM2CertificateValidator {
    
    /**
     * 验证国密 SSL 证书链
     */
    public boolean validateCertificateChain(X509Certificate cert, 
                                            String trustedPublicKeyBase64) {
        try {
            // 1. 提取证书签名
            byte[] signature = cert.getSignature();
            
            // 2. 提取证书 TBS(To Be Signed)部分
            byte[] tbsCertificate = cert.getTBSCertificate();
            
            // 3. 用信任的公钥验签
            SM2 sm2 = SM2.create(trustedPublicKeyBase64, null);
            return sm2.verify(tbsCertificate, signature);
            
        } catch (Exception e) {
            log.error("证书验证失败", e);
            return false;
        }
    }
}

四、密钥管理最佳实践

4.1 密钥存储

方式一:配置文件(开发/测试)

# application.yml
rsa:
  public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A..."
  private-key: "MIIEvQIBADANBgkqhkiG9w0BAQEFAASC..."

sm2:
  public-key: "04a1b2c3d4e5f6..."
  private-key: "1a2b3c4d5e6f..."

方式二:环境变量(生产)

export RSA_PUBLIC_KEY="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A..."
export RSA_PRIVATE_KEY="MIIEvQIBADANBgkqhkiG9w0BAQEFAASC..."

方式三:KMS 密钥管理服务(推荐生产)

阿里云 KMS、AWS KMS 等,密钥不落地。

4.2 密钥轮换

@Service
public class KeyRotationService {
    
    /**
     * 生成新密钥对并归档旧密钥
     */
    public void rotateKeys(String keyId) {
        // 1. 生成新密钥对
        RSA newRsa = new RSA(2048);
        String newPubKey = newRsa.getPublicEncodeToBase64Str();
        String newPriKey = newRsa.getPrivateEncodeToBase64Str();
        
        // 2. 归档旧密钥
        KeyArchive archive = keyArchiveRepository.findByKeyId(keyId);
        archive.setStatus("ARCHIVED");
        archive.setArchivedTime(LocalDateTime.now());
        keyArchiveRepository.save(archive);
        
        // 3. 保存新密钥
        KeyStore newKey = new KeyStore();
        newKey.setKeyId(keyId);
        newKey.setPublicKey(newPubKey);
        newKey.setPrivateKey(newPriKey);
        newKey.setStatus("ACTIVE");
        newKey.setCreateTime(LocalDateTime.now());
        keyStoreRepository.save(newKey);
        
        log.info("密钥轮换完成: {}", keyId);
    }
}

4.3 密钥备份

/**
 * 导出密钥对到安全存储
 */
public void backupKeys(String keyId, String backupPath) throws IOException {
    KeyStore key = keyStoreRepository.findByKeyId(keyId);
    
    // 用主密码加密私钥
    AES aes = new AES(masterPassword.getBytes());
    byte[] encryptedPriKey = aes.encrypt(
        Base64.getDecoder().decode(key.getPrivateKey())
    );
    
    // 保存公钥(明文)和私钥(加密)
    Properties props = new Properties();
    props.setProperty("public_key", key.getPublicKey());
    props.setProperty("encrypted_private_key", 
        Base64.getEncoder().encodeToString(encryptedPriKey));
    props.setProperty("backup_time", LocalDateTime.now().toString());
    
    try (FileOutputStream fos = new FileOutputStream(backupPath)) {
        props.store(fos, "Key backup");
    }
}

五、安全注意事项

5.1 RSA 密钥长度

密钥长度 安全性 适用场景
1024 位 ❌ 已不安全 仅用于测试
2048 位 ✅ 推荐 通用场景
3072 位 ✅ 高安全 金融/医疗
4096 位 ✅ 极高安全 国家机密

5.2 RSA 填充方式

填充 安全性 适用场景
PKCS1Padding ★★★☆☆ 通用场景
OAEP ★★★★★ 高安全场景(推荐)
NoPadding ❌ 不安全 不推荐使用

5.3 SM2 用户 ID

// ❌ 错误:使用空用户 ID
SM2 sm2 = new SM2(pub, pri, SM2Mode.C1C3C2, null, true);

// ✅ 正确:使用标准用户 ID 或自定义 ID
SM2 sm2 = SM2.create(pub, pri); // 默认 "1234567812345678"
SM2 sm2Custom = new SM2(pub, pri, SM2Mode.C1C3C2, 
    "CustomApp".getBytes(), true);

5.4 签名算法选择

算法 安全性 适用场景
SHA1withRSA ❌ 已不推荐 仅兼容旧系统
SHA256withRSA ✅ 推荐 通用场景
SHA384withRSA ✅ 高安全 金融/政务
SHA512withRSA ✅ 极高安全 国家机密

5.5 密钥分发

// ❌ 错误:通过不安全渠道分发私钥
email.send("my_private_key", privateKey); // 绝对不要这样做!

// ✅ 正确:使用安全渠道
// 1. KMS 密钥管理服务
// 2. 硬件安全模块(HSM)
// 3. 加密邮件(PGP/GPG)
// 4. 线下安全传输(U 盘、加密硬盘)

六、总结

pan-common 的非对称加密工具覆盖了 RSA 和 SM2 两种算法,提供统一的 API 设计。核心价值:

  1. API 统一:RSA 和 SM2 的加密/签名方法命名一致,切换算法只需改类名
  2. 密钥管理灵活:支持 Base64/Hex/字节数组等多种格式导入导出
  3. 签名验签便捷signToHex/verifyFromBase64Str 等方法减少编码转换
  4. 国密合规:SM2 完全符合国密标准,支持政务/金融场景

生产环境 Checklist:

  • ✅ RSA 密钥长度 ≥ 2048 位
  • ✅ 使用 SHA256withRSA 或更强签名算法
  • ✅ 密钥通过 KMS 或安全渠道管理
  • ✅ 大文件使用混合加密(RSA + AES)
  • ✅ 国密场景使用 SM2
  • ✅ 定期轮换密钥(建议每年)

项目地址:https://gitee.com/apanlh/pan-common


更多推荐