Java非对称加密实战:pan-common 从RSA到国密SM2业务落地全指南
·
Java非对称加密实战:RSA/SM2签名验签与密钥管理全指南
选题说明:非对称加密的核心应用场景不是"加密数据",而是"密钥交换"和"数字签名"。RSA 和 SM2 在 API 对接、电子合同、支付回调等场景中广泛使用,但密钥管理混乱、签名算法选择不当、密文格式不兼容等问题经常导致对接失败。本文从业务视角出发,覆盖 RSA/SM2 的完整 API 和真实对接场景。
一、非对称加密的典型业务场景
非对称加密(公钥加密、私钥解密)在项目中的核心价值:
- API 签名验签:开放平台对接(微信支付、支付宝、钉钉)需要对请求签名,防止数据篡改
- 密钥交换:用对方公钥加密对称密钥,传输给对方,实现"数字信封"
- 数字证书:HTTPS、电子合同、发票签章基于非对称加密实现身份认证
- 国密合规:政务/金融系统要求使用 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 设计。核心价值:
- API 统一:RSA 和 SM2 的加密/签名方法命名一致,切换算法只需改类名
- 密钥管理灵活:支持 Base64/Hex/字节数组等多种格式导入导出
- 签名验签便捷:
signToHex/verifyFromBase64Str等方法减少编码转换 - 国密合规:SM2 完全符合国密标准,支持政务/金融场景
生产环境 Checklist:
- ✅ RSA 密钥长度 ≥ 2048 位
- ✅ 使用 SHA256withRSA 或更强签名算法
- ✅ 密钥通过 KMS 或安全渠道管理
- ✅ 大文件使用混合加密(RSA + AES)
- ✅ 国密场景使用 SM2
- ✅ 定期轮换密钥(建议每年)
项目地址:https://gitee.com/apanlh/pan-common
更多推荐
所有评论(0)