如何将RSA公私钥转为字符串并正确存储到PostgreSQL数据库?
问题:RSA密钥存储与签名验证的核心问题
一、RSA公私钥转字符串存储的问题
我用以下代码生成RSA密钥对:
Security.addProvider(new BouncyCastleProvider()); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); keyPairGenerator.initialize(2048, new SecureRandom()); KeyPair keyPair = keyPairGenerator.generateKeyPair();
直接调用keyPair.getPublic().toString()转字符串存PostgreSQL时,得到的是无法使用的调试格式:
RSA Private CRT Key [5e:cf:06:fc:ff:52:cd:16:3a:b4:28:42:dc:c2:5b:9c:e3:fc:a8:c1],[56:66:d1:a4] modulus: b3b9ae25337bedf971aa2767412eec0f2d7792873e9c500eb0cff747e5f80b182f3f29ed97b28471de8c68346354bb4fdbe8f8f3ba1b09e745e3235ffdd2ff11efe379cca27914a6a6754e6f967f21358c05462bc20426f7f8c4df0f6632673897b17d8a0592b6291c9281dd3e94b0ed9c8f02dc02df9d8ae8dc2688dfadcb2a94840b2b22a4528e1169b79056288b111a70ebebc8e838e17943c0b88b103c27bbaa6c40624babde1e97e86a42e532cb58c0948705b8943a0f203d648828848e20e5d4ea0aaab8b06d804732352c8eae01cc9672e5140e34dc5054d073d8e617b211fa80ccac810045d4def198e587ff0b7ba858dfa11fd3682cdfeb88d360df public exponent: 10001
请问正确的转换存储方法是什么?
二、签名验证代码的两个问题
我写的验证代码存在两个问题:无法从公钥填充data变量,无法从公钥填充signature变量,代码如下:
import io.jsonwebtoken.lang.Assert; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.jupiter.api.Test; import javax.xml.bind.DatatypeConverter; import java.io.ByteArrayInputStream; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.PublicKey; import java.security.SecureRandom; import java.security.Security; import java.security.Signature; import java.security.SignatureException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.util.Base64; public class CertificateGenerateTestApiTest { String data; // complete certificate and metadata. We need to get of when we generate a certificate String signature; // signature of the certificate @Test public void certificateGenerationTest() { // Generate private and public keys KeyPair keyPair = generateCertificate(); PublicKey publicCertificate = keyPair.getPublic(); // Problem 1 - think how to populate "data" variable from public certificate // Problem 2 - think how to populate "signature" variable from public certificate // Get public key as byte[] in order later on to be stored into DB byte[] originalPublicCertificate = privateCertificateToByte(publicCertificate); // Let's say we received some certificate by some web client, and we want to verify it against our original certificate byte[] secondPublicCertificateReceivedByClient = cloneByteArrayData(originalPublicCertificate); // Verify certificate Assert.isTrue(verifyCertificate(secondPublicCertificateReceivedByClient)); } private KeyPair generateCertificate() { try { Security.addProvider(new BouncyCastleProvider()); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); keyPairGenerator.initialize(2048, new SecureRandom()); KeyPair keyPair = keyPairGenerator.generateKeyPair(); return keyPair; } catch (NoSuchAlgorithmException | NoSuchProviderException e) { e.printStackTrace(); } return null; } private boolean verifyCertificate(byte[] secondPublicCertificateReceivedByClient) { PublicKey publicKey = getKeyPayload(byteToString(secondPublicCertificateReceivedByClient)); boolean verified = false; try { verified = verifySignature(data.getBytes("UTF8"), publicKey, DatatypeConverter.parseHexBinary(signature)); } catch (SignatureException | UnsupportedEncodingException e) { } catch (NoSuchAlgorithmException | InvalidKeyException e) { e.printStackTrace(); } return verified; } private boolean verifySignature(byte[] data, PublicKey key, byte[] signature) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { Signature signer = Signature.getInstance("SHA1withDSA"); signer.initVerify(key); signer.update(data); try { if (signer.verify(signature)) { return true; } } catch (SignatureException e) { e.printStackTrace(); } return false; } private PublicKey getKeyPayload(String certificateString) { try { CertificateFactory cf = CertificateFactory.getInstance("X.509"); ByteArrayInputStream bis = new ByteArrayInputStream(certificateString.getBytes(StandardCharsets.UTF_8)); Certificate certificate = null; while (bis.available() > 0) { certificate = cf.generateCertificate(bis); } return (certificate == null) ? null : certificate.getPublicKey(); } catch (CertificateException e) { e.printStackTrace(); } return null; } private String byteToString(byte[] publicKeyBytes) { return Base64.getEncoder().encodeToString(publicKeyBytes); } private byte[] privateCertificateToByte(PublicKey publicCertificate) { return publicCertificate.getEncoded(); } /** * Clone byte[] data */ private byte[] cloneByteArrayData(byte[] sourceArray) { int sourceOffset = 0; /* Starting index in the source array */; int destinationOffset = 0; // Starting index in the destination array byte[] destinationArray = new byte[sourceArray.length]; for (int i = 0; i < sourceArray.length; i++) { destinationArray[destinationOffset + i] = sourceArray[sourceOffset + i]; } return destinationArray; } }
解决方案
一、RSA公私钥转字符串存储的正确方式
toString()返回的是调试用的可读信息,不是可序列化的标准格式。正确做法是用密钥的标准编码格式转Base64字符串:
1. 密钥转字符串(存储到DB)
// 公钥转Base64字符串(X.509标准格式) PublicKey publicKey = keyPair.getPublic(); String publicKeyStr = Base64.getEncoder().encodeToString(publicKey.getEncoded()); // 私钥转Base64字符串(PKCS#8标准格式) PrivateKey privateKey = keyPair.getPrivate(); String privateKeyStr = Base64.getEncoder().encodeToString(privateKey.getEncoded());
直接将Base64字符串存入数据库的TEXT类型字段即可。
2. 从字符串还原密钥(从DB读取后)
// 还原公钥 byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyStr); KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC"); PublicKey restoredPublicKey = keyFactory.generatePublic(new X509EncodedKeySpec(publicKeyBytes)); // 还原私钥 byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyStr); PrivateKey restoredPrivateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes));
二、签名验证代码的问题修正
你的代码存在三个核心错误:混淆了公钥与证书、签名算法不匹配、对签名逻辑的误解。以下是针对性修复:
核心逻辑澄清
公钥本身不包含data和signature:
data是你要签名的业务数据/元数据(比如用户信息、交易内容),需要你主动定义。signature是用私钥对data签名后生成的结果,不是从公钥提取的。
问题1:填充data变量
直接定义你需要签名的内容,比如:
// 示例:签名用户ID和有效期 data = "userId:123,expire:2025-12-31";
问题2:填充signature变量
用私钥对data生成签名:
private void generateSignature(PrivateKey privateKey) { try { // 用RSA签名算法(避免SHA1,推荐SHA256) Signature signer = Signature.getInstance("SHA256withRSA", "BC"); signer.initSign(privateKey, new SecureRandom()); signer.update(data.getBytes(StandardCharsets.UTF_8)); byte[] signatureBytes = signer.sign(); // 转十六进制字符串存储(也可以用Base64) signature = DatatypeConverter.printHexBinary(signatureBytes); } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { e.printStackTrace(); } }
完整修正后的代码
import io.jsonwebtoken.lang.Assert; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.jupiter.api.Test; import javax.xml.bind.DatatypeConverter; import java.nio.charset.StandardCharsets; import java.security.*; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; public class CertificateGenerateTestApiTest { String data; // 要签名的业务数据/元数据 String signature; // 私钥对data生成的签名 @Test public void certificateGenerationTest() { // 生成密钥对 KeyPair keyPair = generateKeyPair(); assert keyPair != null; // 填充data:定义签名内容 data = "userId:123,expire:2025-12-31"; // 填充signature:用私钥生成签名 generateSignature(keyPair.getPrivate()); // 公钥转字节数组存DB byte[] originalPublicKeyBytes = keyPair.getPublic().getEncoded(); // 模拟客户端传来的公钥 byte[] receivedPublicKeyBytes = cloneByteArrayData(originalPublicKeyBytes); // 验证签名 Assert.isTrue(verifySignatureWithReceivedPublicKey(receivedPublicKeyBytes)); } private KeyPair generateKeyPair() { try { Security.addProvider(new BouncyCastleProvider()); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); keyPairGenerator.initialize(2048, new SecureRandom()); return keyPairGenerator.generateKeyPair(); } catch (NoSuchAlgorithmException | NoSuchProviderException e) { e.printStackTrace(); } return null; } private void generateSignature(PrivateKey privateKey) { try { Signature signer = Signature.getInstance("SHA256withRSA", "BC"); signer.initSign(privateKey, new SecureRandom()); signer.update(data.getBytes(StandardCharsets.UTF_8)); byte[] signatureBytes = signer.sign(); signature = DatatypeConverter.printHexBinary(signatureBytes); } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { e.printStackTrace(); } } private boolean verifySignatureWithReceivedPublicKey(byte[] receivedPublicKeyBytes) { try { // 还原公钥(无需解析证书,直接用X509EncodedKeySpec) KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC"); PublicKey receivedPublicKey = keyFactory.generatePublic(new X509EncodedKeySpec(receivedPublicKeyBytes)); // 验证签名 return verifySignature(data.getBytes(StandardCharsets.UTF_8), receivedPublicKey, DatatypeConverter.parseHexBinary(signature)); } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException | InvalidKeySpecException e) { e.printStackTrace(); } return false; } private boolean verifySignature(byte[] data, PublicKey key, byte[] signature) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { Signature verifier = Signature.getInstance("SHA256withRSA", "BC"); verifier.initVerify(key); verifier.update(data); return verifier.verify(signature); } private byte[] cloneByteArrayData(byte[] sourceArray) { byte[] destinationArray = new byte[sourceArray.length]; System.arraycopy(sourceArray, 0, destinationArray, 0, sourceArray.length); return destinationArray; } }
额外说明
- 如果你需要的是X.509证书(而非单纯密钥对),需要用
CertificateGenerator生成证书,证书本身包含公钥、签名和元数据,此时可从证书中提取相关内容。 - 避免使用SHA1算法,安全性较低,推荐使用SHA256及以上的哈希算法。
内容的提问来源于stack exchange,提问作者Peter Penzov
相关产品推荐
相关产品推荐

