RSA加解密&RSA加验签详解
1.1 密钥对生成
RSA非对称加密密钥对,可以用OpenSSL的命令生成,也可以直接在线生成(http://web.chacuo.net/netrsakeypair)。
- 秘钥位数:1024位(bit) 或 2048位(bit) 即,秘钥的长度。在加解密时可能需要分段加解密。1024b和2048b要求的block是不同的。
- 秘钥格式:PKCS#8格式 pkcs#1格式的也可以转成pkcs#8
- 证书密码:指的是私钥文件的密码。如果需要加密,可以指定。 无密码的私钥以“-----BEGIN PRIVATE KEY-----”开头,有密码的私钥以“-----BEGIN ENCRYPTED PRIVATE KEY-----”开头。
注意,在线生成的密钥对,是有开头和结尾标记的,并且有换行符。如果直接copy到程序配置里,则要把这些去掉。
1.2 数字证书格式
格式 | 扩展名 | 说明 |
X.509 PEM格式 | .pem .cer .crt | Base64编码的ASCII文件,以"-----BEGIN CERTIFICATE-----"开头,以"-----END CERTIFICATE-----"结尾。可存放证书,也可存放私钥。 |
X.509 DER格式 | .der .cer .crt | 用于存放证书,它是2进制形式存放的,不含私钥。 |
【公钥证书格式英文介绍】
PEM Format The PEM format is the most common format that Certificate Authorities issue certificates in. PEM certificates usually have extentions such as .pem, .crt, .cer, and .key. They are Base64 encoded ASCII files and contain "-----BEGIN CERTIFICATE-----" and "-----END CERTIFICATE-----" statements. Server certificates, intermediate certificates, and private keys can all be put into the PEM format. Apache and other similar servers use PEM format certificates. Several PEM certificates, and even the private key, can be included in one file, one below the other, but most platforms, such as Apache, expect the certificates and private key to be in separate files. DER Format The DER format is simply a binary form of a certificate instead of the ASCII PEM format. It sometimes has a file extension of .der but it often has a file extension of .cer so the only way to tell the difference between a DER .cer file and a PEM .cer file is to open it in a text editor and look for the BEGIN/END statements. All types of certificates and private keys can be encoded in DER format. DER is typically used with Java platforms. The SSL Converter can only convert certificates to DER format. If you need to convert a private key to DER, please use the OpenSSL commands on this page
【1.2.2 私钥证书格式】
格式 | 扩展名 | 说明 |
PKCS#7/P7B格式 | .p7b .p7c | Base64编码的ASCII文件,以"-----BEGIN PKCS7-----"开头,以"-----END PKCS7-----"结尾。其中,p7b以树状展示证书链(certificate chain),同时也支持单个证书,不含私钥。 |
PKCS#12/PFX格式 | .pfx .p12 | 用于存放个人证书/私钥,他通常包含保护密码,2进制文件。 |
【私钥证书格式英文介绍】
PKCS#7/P7B Format The PKCS#7 or P7B format is usually stored in Base64 ASCII format and has a file extention of .p7b or .p7c. P7B certificates contain "-----BEGIN PKCS7-----" and "-----END PKCS7-----" statements. A P7B file only contains certificates and chain certificates, not the private key. Several platforms support P7B files including Microsoft Windows and Java Tomcat. PKCS#12/PFX Format The PKCS#12 or PFX format is a binary format for storing the server certificate, any intermediate certificates, and the private key in one encryptable file. PFX files usually have extensions such as .pfx and .p12. PFX files are typically used on Windows machines to import and export certificates and private keys. When converting a PFX file to PEM format, OpenSSL will put all the certificates and the private key into a single file. You will need to open the file in a text editor and copy each certificate and private key (including the BEGIN/END statments) to its own individual text file and save them as certificate.cer, CACert.cer, and privateKey.key respectively.
- 公钥,可以对外给任何人的加密和解密密码,公开的,可以任何人访问
- 私钥,私钥是一定要严格保护的,通过私钥可以生成公钥,但是从公钥可以认为是永远无法推导出私钥的。 ∴ pfx文件一般都有文件密码。
- pfx→cer 可以用OpenSSL
OpenSSL工具可以生成RSA公私钥和证书格式转换。不同格式的证书之间可以做如下转换:
![]() |
PEM → DER(由私钥证书生成公钥证书) P7B → PEM PFX → PEM DER → PEM(指公钥证书之间的转换) |
1.2.3 开发语言与私钥证书的关系
开发语言
|
私钥格式 | 证书格式 |
JAVA | .key.p8 | .crt |
PHP | .key.pem | .cert.pem |
.NET | .key.der | .crt |
其它 | .key.pem | .cert.pem |
#商户私钥 PKCS#8标准的私钥 90000002.mer.prikey.path=cert/90000002.key.p8 #平台公钥 X.509证书(DER格式的二进制文件) plat.cert.path=cert/umpay.cert.crt
1.3 RSA在支付api中的使用
下游交易渠道端:持有每个子商户的私钥 和 三方支付平台的公钥
三方支付平台:持有下游渠道的每个子商户的公钥 和 三方平台自己的私钥
获取公钥(PublicKey)/私钥(PrivateKey)
2.1 代码 RSACertUtil.java
两种获取Key的方式:1)从证书获取;2)从密钥串获取
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package rsademo; import com.umpay.core.util.Base64; import com.umpay.core.util.ProFileUtil; import java.io.ByteArrayInputStream; import java.io.IOException; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; public class RSACertUtils { /** * 读公钥证书文件umpay.cert.crt,得到X509证书 * * @return * @throws Exception */ public static X509Certificate getCert() throws Exception { byte[] b = ProFileUtil.getFileByte("plat.cert.path"); try { ByteArrayInputStream bais = new ByteArrayInputStream(b); CertificateFactory cf = CertificateFactory.getInstance("X.509"); return (X509Certificate) cf.generateCertificate(bais); } catch (CertificateException e) { e.printStackTrace(); } return null; } /** * 从X509公钥证书获取PublicKey * * @return * @throws Exception */ public static PublicKey getPublicKey() throws Exception { X509Certificate x509Certificate = getCert(); try { byte[] keyBytes = x509Certificate.getPublicKey().getEncoded(); X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); return keyFactory.generatePublic(x509KeySpec); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (InvalidKeySpecException e) { e.printStackTrace(); } return null; } /** * 从公钥字符串得到公钥对象 * * @param publicKeyStr * @return */ public static PublicKey getPublicKeyFromKeyString(String publicKeyStr) { try { byte[] keyBytes = Base64.decode(publicKeyStr.getBytes()); X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); return keyFactory.generatePublic(x509KeySpec); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (InvalidKeySpecException e) { e.printStackTrace(); } return null; } /** * 从私钥文件 merId.key.p8 得到私钥对象 * * @param merId * @return */ public static PrivateKey getPrivateKey(String merId) { try { byte[] key = ProFileUtil.getFileByte(merId + ".mer.prikey.path"); PKCS8EncodedKeySpec e = new PKCS8EncodedKeySpec(key); KeyFactory kf = KeyFactory.getInstance("RSA"); return kf.generatePrivate(e); } catch (IOException e1) { e1.printStackTrace(); } catch (NoSuchAlgorithmException e1) { e1.printStackTrace(); } catch (InvalidKeySpecException e1) { e1.printStackTrace(); } return null; } /** * 根据私钥字符串得到私钥对象 * * @param privateKeyStr * @return * @throws Exception */ public static PrivateKey getPrivateKeyFromKeyString(String privateKeyStr) throws Exception { KeyFactory keyFactory = KeyFactory.getInstance("RSA"); byte[] decodedKey = Base64.decode(privateKeyStr.getBytes()); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey); return keyFactory.generatePrivate(keySpec); } }
2.2 java.security下的Key
2.3 java.security.spec下两个spec
2.3.1 PKCS8EncodedKeySpec
* This class represents the ASN.1 encoding of a private key, * encoded according to the ASN.1 type {@code PrivateKeyInfo}. * The {@code PrivateKeyInfo} syntax is defined in the PKCS#8 standard * as follows: * * <pre> * PrivateKeyInfo ::= SEQUENCE { * version Version, * privateKeyAlgorithm PrivateKeyAlgorithmIdentifier, * privateKey PrivateKey, * attributes [0] IMPLICIT Attributes OPTIONAL } * * Version ::= INTEGER * * PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier * * PrivateKey ::= OCTET STRING * * Attributes ::= SET OF Attribute * </pre>
*/
public class PKCS8EncodedKeySpec extends EncodedKeySpec {
//class实现代码(略)
}
2.3.2 X509EncodedKeySpec
/** * This class represents the ASN.1 encoding of a public key, * encoded according to the ASN.1 type {@code SubjectPublicKeyInfo}. * The {@code SubjectPublicKeyInfo} syntax is defined in the X.509 * standard as follows: * * <pre> * SubjectPublicKeyInfo ::= SEQUENCE { * algorithm AlgorithmIdentifier, * subjectPublicKey BIT STRING } * </pre> */ public class X509EncodedKeySpec extends EncodedKeySpec {
//class实现代码(略)
}
【数据加密】RSA加密/RSA解密
3.1 利用公钥加密,利用私钥解密
3.2 RSA加解密代码 RSACipherUtil.java
加解密主要借助于javax.crypto.Cipher来实现
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package rsademo; import com.umpay.core.util.Base64; import javax.crypto.Cipher; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.PublicKey; public class RSACipherUtil { /** * 公钥加密 * * @param data * @return * @throws Exception */ public static String encrypt(PublicKey publicKey, String data, String charset) throws Exception { Cipher cipher = Cipher.getInstance(KeyFactory.getInstance("RSA").getAlgorithm()); cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] cipherText = cipher.doFinal(data.getBytes(charset)); byte[] encodedByte = Base64.encode(cipherText); return new String(encodedByte).replace("\n", ""); } /** * RSA私钥解密 * * @param privateKey * @param data * @return * @throws Exception */ public static String decrypt(PrivateKey privateKey, String data, String charset) throws Exception { byte[] byteData = Base64.decode(data.getBytes(charset)); Cipher cipher = Cipher.getInstance(KeyFactory.getInstance("RSA").getAlgorithm()); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] retB = cipher.doFinal(byteData); return new String(retB); } }
【数字签名】RSA签名(加签/验签)
利用私钥签名,利用公钥/公钥证书验签
4.1 常用的数据签名/验签过程描述
生成rsa签名的过程:
请求实体requestModel → 转换成requestMap<String,String> → 将map的key进行排序 → 得到签名原串 plainText → 读取properties,根据property读私钥文件或私钥串得到 PrivateKey privateKey → 生成RSA签名 signData= sign(privateKey, plainText)→ Base64编码 → 转换成签名字符串 signature
验签的过程:
解析请求报文,得到签名原串 plainText → 从请求头或请求报文里得到签名 signature → Base64解码 signatureData = Base64.decode(signature.getBytes(charset)); → 读取properties,根据公钥证书文件得到X509Certificate 或 公钥串转换成PublicKey → 验签 verifySign(X509Certificate/PublicKey, plainText, signatureData)
*base64不是加密算法,但也是SSL经常使用的一种算法,它是编码方式,用来把ascii码和二进制码之间互转。类似的编码方式,还有Hex-16进制编码。
4.2 RSA签名验签代码 RSASignUtil.java
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package rsademo; import com.umpay.core.util.Base64; import java.io.UnsupportedEncodingException; import java.security.*; import java.security.cert.X509Certificate; public class RSASignUtil { private static final String charset = "UTF-8"; private static final String signType = "spay"; /** * 生成签名 * * @param plainText * @param privateKey * @return */ public static String sign(PrivateKey privateKey, String plainText) { try { Signature signature = getSignatureObj(signType); signature.initSign(privateKey);//public final void initSign(PrivateKey privateKey) signature.update(plainText.getBytes(charset)); byte[] signData = signature.sign(); return new String(Base64.encode(signData)); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (SignatureException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return null; } /** * 通过PublicKey来验签 * * @param publicKey * @param plainText * @param signature * @return * @throws Exception */ public static boolean verifySign(PublicKey publicKey, String plainText, String signature) throws Exception { byte[] signData = Base64.decode(signature.getBytes(charset)); try { Signature sig = getSignatureObj(signType); sig.initVerify(publicKey);//public final void initVerify(PublicKey publicKey) sig.update(plainText.getBytes(charset)); return sig.verify(signData); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (SignatureException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return false; } /** * 通过公钥X509证书来验签 * * @param cert * @param plainText * @param signature * @return * @throws Exception */ public static boolean verifySign(X509Certificate cert, String plainText, String signature) throws Exception { byte[] signData = Base64.decode(signature.getBytes(charset)); try { Signature sig = getSignatureObj(signType); sig.initVerify(cert);//public final void initVerify(Certificate certificate) sig.update(plainText.getBytes(charset)); return sig.verify(signData); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (SignatureException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return false; } private static Signature getSignatureObj(String signType) { String shaAlgorithm = null; if ("rest".equals(signType)) { shaAlgorithm = "SHA256withRSA"; } else if ("spay".equals(signType)) { shaAlgorithm = "SHA1withRSA"; } try { return Signature.getInstance(shaAlgorithm); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); return null; } } }
测试
package rsademo; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.X509Certificate; import java.util.UUID; public class TestMain { public static final String publicKeyString = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCvGGQDrUk7ejBfgR6cwDhTUqDe" + "1+ooXzwowLfRRDqu1N0O9KyeAsY8nI8HUvzYGXODNMBEKZ2v8Ck7lelVoxlgkIAT" + "GHB2nM+TBZOPQAdF0X4crJh1yWjdnrGO5fluqUalwRvYIG91mqIPnvSGL9mLDIhi" + "7PR/duEe7KzwDCi3DQIDAQAB"; public static final String privateKeyString = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAK8YZAOtSTt6MF+B" + "HpzAOFNSoN7X6ihfPCjAt9FEOq7U3Q70rJ4CxjycjwdS/NgZc4M0wEQpna/wKTuV" + "6VWjGWCQgBMYcHacz5MFk49AB0XRfhysmHXJaN2esY7l+W6pRqXBG9ggb3Waog+e" + "9IYv2YsMiGLs9H924R7srPAMKLcNAgMBAAECgYEAgzW85QB7K2XyT+87WG23B8GY" + "qcWVRDGxrDxWwyvk6dS73xQ9Mp+TnCIaEHwA25Oe+0iRd8LT1t8alvtNAo6ZWYU9" + "Ek0yuNTJ5YpWIal+A3c25Zm8Ir3CgkCvq7+q+4OOhC4rOMoY6G8rxQQ7fm4noW0A" + "lZbgQJrhaqDfute4v2ECQQDVZZpZgNU7u9E2CoAXUjUMGWmapdyAsfdZo8kq7mP3" + "g/4JdcGVykxP2YTI1rR2/6vPvnRZN8wcSNVRoM+qWTCFAkEA0g0/3m5TD6wR5ajj" + "SoIMmYgu5OFToKVX1mDoPKBnrjsxzuPZHm/KhRtkRzafd+vs/DbLs60QtQTmyY7q" + "BZm26QJBAMq5KgeLF4cWpupS0VrWUuS6o5MxrCdqadPzf6FUNQ2ni8cK4ivdsd9N" + "ghKVvX0q59qEUN2M30+jdVuFjKKE9k0CQBtIHUOGkMM4Vhq+FMdYnMpUJcMUgQgc" + "cYwmigNV0iGPDqkQbuLFIkinhh65uXyZ5+3aMBrmH4VjXZZQOZUAogECQHs7Ywr+" + "ZSMllnuYOM9z+dXCcQRGKfcVa90fGo3bjrqanyhGyjAwwfECajPlTHm75AdgWegH" + "XXWxFu93sms7KJY="; public static void main(String[] args) throws Exception { // testCipher(); testSign(); } public static void testCipher() throws Exception { String text = "abcd" + UUID.randomUUID(); // Key publicKey = RSACertUtils.getPublicKey(); PublicKey publicKey = RSACertUtils.getPublicKeyFromKeyString(TestMain.publicKeyString); String encryptStr = RSACipherUtil.encrypt(publicKey, text, charset); System.out.println("encryptStr:" + encryptStr); // Key privateKey = RSACertUtils.getPrivateKey(merId); PrivateKey privateKey = RSACertUtils.getPrivateKeyFromKeyString(TestMain.privateKeyString); String decryptStr = RSACipherUtil.decrypt(privateKey, encryptStr, charset); System.out.println(decryptStr); System.out.println(text.equals(decryptStr)); } public static void testSign() throws Exception { System.out.println(publicKeyString); String text = "{\"trade_no\":\"1904261100329133\",\"amount\":\"1\",\"mer_id\":\"90000002\",\"mer_date\":\"20190426\",\"order_id\":\"0306262632992608256R\"}"; PrivateKey privateKey = RSACertUtils.getPrivateKeyFromKeyString(privateKeyString); // PrivateKey privateKey = RSACertUtils.getPrivateKey("111"); String signature = RSASignUtil.sign(privateKey, text); System.out.println("signature:" + signature); PublicKey publicKey = RSACertUtils.getPublicKeyFromKeyString(publicKeyString); X509Certificate certificate = RSACertUtils.getCert(); boolean verifiedOK = RSASignUtil.verifySign(publicKey, text, signature); // boolean verifiedOK =RSASignUtil.verifySign(certificate,text,signature); System.out.println(verifiedOK); } }
常见错误/异常:
1)RSA解密报错 javax.crypto.BadPaddingException: Decryption error
2)Base64串的长度是4的倍数,当一个字符串不是有效的base64串时,调用base64.decode方法会返回null。所以,程序要注意NullPointerException。
RSA 一份私钥可以生成多个公钥么?
知乎上也有人问这个问题,>>here
如下的这个私钥与后面的两个公钥都是可以匹配成功的。
正确的理解是,pub1和pub2包含相同的公钥信息,但是它们的格式不同。pub1的格式是pkcs#1,pub2的格式是pkcs#8。 这两种风格的公钥格式之间可以进行相互转换,http://www.metools.info/code/c85.html提供了工具支持。
-----BEGIN RSA PRIVATE KEY----- MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAJcW0k1wK/1f6l87P6PLhY77soLS120MZ9b9esk4WfqZNH9OkmuwvPCyxPKPSEN9SVrXEMJsBaVQSTNvriOw8qT1YAK6ks46ucTLs24bQ7mODfpReV76bn+4ObwH8oZX0FCYS34Q4huXYjM1hLgZ+WRsJO2RvMxz4Nd62XcgczX7AgMBAAECgYEAgCs47b4pYwB5xp1xSBa/TuMPtND9NKGgeQ2Amq/2DJLoqNJTfY1pSlqsngOUTsQ6dRgaPIP8ahdocXzc4aQawSG7j+moLBnuu8Y3lbxC6jqENlsmc1cmF784UsW/Y1A27crfgzHgcy5BzHXSG5ZyJNHlxmBpqqBit+RuwR2BsTECQQD/pEqwnsYPteVtk7xImcAqBmM+35UWrxXnmBTMRFaCEbcskKOi7FyQe1pD5yF+0dLPtW/CLVYtgKc2Wg59L1UjAkEAl00F1uLqz/idhS8gEx40XI3cLJc55HDnNaUl9afRd6jYtYrVtThEEEbuWS2O1Ks8OIPE+K9fY5+W/TyLp9DFSQJBAO9MOAo6pcYhC+FV0ILZQXNVRWOeYO25+TQwPQ+0zJG2yZNy1Wp1/HPWs/kqC0WuXbrG6RWH4Mp5SozrIfL28qcCQF1f/aCWvo/HQX+2i7cAxxPvwNgMJIBlZWvoFjs7bLzKiaPQoP+MUAUzoVmMEkARxcKjH+bSZK5ZCZgTy6Sv5XECQAuU5/ebdXYdkcJR4H0iOGvIzJ+f/y1KCRQL3giMtf1yefhUgKo0KBv1pGlMaNgnWmFkFoL3z9nKLRt/GEZP4es= -----END RSA PRIVATE KEY-----
【pub1】 -----BEGIN RSA PUBLIC KEY----- MIGJAoGBAJcW0k1wK/1f6l87P6PLhY77soLS120MZ9b9esk4WfqZNH9OkmuwvPCyxPKPSEN9SVrXEMJsBaVQSTNvriOw8qT1YAK6ks46ucTLs24bQ7mODfpReV76bn+4ObwH8oZX0FCYS34Q4huXYjM1hLgZ+WRsJO2RvMxz4Nd62XcgczX7AgMBAAE= -----END RSA PUBLIC KEY-----
【pub2】
-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCXFtJNcCv9X+pfOz+jy4WO+7KC0tdtDGfW/XrJOFn6mTR/TpJrsLzwssTyj0hDfUla1xDCbAWlUEkzb64jsPKk9WACupLOOrnEy7NuG0O5jg36UXle+m5/uDm8B/KGV9BQmEt+EOIbl2IzNYS4GflkbCTtkbzMc+DXetl3IHM1+wIDAQAB -----END PUBLIC KEY-----
RSA可以私钥加密吗?
可以。
通过私钥加密,则解密要通过公钥解密。
通过私钥加密,如果通过私钥解密,就会报异常:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
javax.crypto.BadPaddingException: Decryption error at sun.security.rsa.RSAPadding.unpadV15(RSAPadding.java:380) at sun.security.rsa.RSAPadding.unpad(RSAPadding.java:291) at com.sun.crypto.provider.RSACipher.doFinal(RSACipher.java:363) at com.sun.crypto.provider.RSACipher.engineDoFinal(RSACipher.java:389) at javax.crypto.Cipher.doFinal(Cipher.java:2165) at com.emax.channel.util.RSAUtils.decryptByPrivateKey(RSAUtils.java:213) at com.emax.channel.provider.TestMain.testRSAEncrypt(TestMain.java:39)
不过,用私钥加密用公钥解密,无法保证数据的安全。用公钥加密用私钥解密,这才能起到加密作用。
因为公钥是公开的,很多人可以持有公钥。若用私钥加密,那所有持有公钥的人都可以进行解密,这是不安全的!
若用公钥加密,那只能由私钥解密,而私钥是私有不公开的,只能由特定的私钥持有人解密,保证的数据的安全性。
在线验证工具:http://www.metools.info/code/c83.html
当看到一些不好的代码时,会发现我还算优秀;当看到优秀的代码时,也才意识到持续学习的重要!--buguge
本文来自博客园,转载请注明原文链接:https://www.cnblogs.com/buguge/p/11769740.html