cer, pfx 创建,并且读取公钥/密钥,加解密 (C#程序实现)

PKI技术(public key infrastructure)里面,cer文件和pfx文件是很常见的。通常cer文件里面保存着公钥以及用户的一些信息,pfx里面则含有私钥和公钥。

用makecert.exe可以创建公钥证书和私钥证书,具体看

http://msdn.microsoft.com/zh-cn/library/bfsktky3(v=vs.110).aspx

http://blog.csdn.net/hacode/article/details/4240238

这里使用程序的方法来创建。参考了http://www.cnblogs.com/luminji/archive/2010/10/28/1863179.html

下面的代码封装了一个类,可以在store里面创建一个认证,并且导出到cer,pfx,然后从store,cer,pfx读取信息

[csharp] view plaincopy
 
  1. public sealed class DataCertificate     
  2.     {    
  3.         #region 生成证书     
  4.         /// <summary>     
  5.         /// 根据指定的证书名和makecert全路径生成证书(包含公钥和私钥,并保存在MY存储区)     
  6.         /// </summary>     
  7.         /// <param name="subjectName"></param>     
  8.         /// <param name="makecertPath"></param>     
  9.         /// <returns></returns>     
  10.         public static bool CreateCertWithPrivateKey(string subjectName, string makecertPath)     
  11.         {     
  12.             subjectName = "CN=" + subjectName;     
  13.             string param = " -pe -ss my -n "" + subjectName + "" ";     
  14.             try    
  15.             {     
  16.                 Process p = Process.Start(makecertPath, param);     
  17.                 p.WaitForExit();     
  18.                 p.Close();     
  19.             }     
  20.             catch (Exception e)     
  21.             {     
  22.                 return false;     
  23.             }     
  24.             return true;     
  25.         }    
  26.         #endregion    
  27.   
  28.         #region 文件导入导出     
  29.         /// <summary>     
  30.         /// 从WINDOWS证书存储区的个人MY区找到主题为subjectName的证书,     
  31.         /// 并导出为pfx文件,同时为其指定一个密码     
  32.         /// 并将证书从个人区删除(如果isDelFromstor为true)     
  33.         /// </summary>     
  34.         /// <param name="subjectName">证书主题,不包含CN=</param>     
  35.         /// <param name="pfxFileName">pfx文件名</param>     
  36.         /// <param name="password">pfx文件密码</param>     
  37.         /// <param name="isDelFromStore">是否从存储区删除</param>     
  38.         /// <returns></returns>     
  39.         public static bool ExportToPfxFile(string subjectName, string pfxFileName,     
  40.             string password, bool isDelFromStore)     
  41.         {     
  42.             subjectName = "CN=" + subjectName;     
  43.             X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);     
  44.             store.Open(OpenFlags.ReadWrite);     
  45.             X509Certificate2Collection storecollection = (X509Certificate2Collection)store.Certificates;     
  46.             foreach (X509Certificate2 x509 in storecollection)     
  47.             {     
  48.                 if (x509.Subject == subjectName)     
  49.                 {     
  50.                     Debug.Print(string.Format("certificate name: {0}", x509.Subject));     
  51.     
  52.                     byte[] pfxByte = x509.Export(X509ContentType.Pfx, password);     
  53.                     using (FileStream fileStream = new FileStream(pfxFileName, FileMode.Create))     
  54.                     {     
  55.                         // Write the data to the file, byte by byte.     
  56.                         for (int i = 0; i < pfxByte.Length; i++)     
  57.                             fileStream.WriteByte(pfxByte[i]);     
  58.                         // Set the stream position to the beginning of the file.     
  59.                         fileStream.Seek(0, SeekOrigin.Begin);     
  60.                         // Read and verify the data.     
  61.                         for (int i = 0; i < fileStream.Length; i++)     
  62.                         {     
  63.                             if (pfxByte[i] != fileStream.ReadByte())     
  64.                             {     
  65.                                 fileStream.Close();     
  66.                                 return false;     
  67.                             }     
  68.                         }     
  69.                         fileStream.Close();     
  70.                     }     
  71.                     if( isDelFromStore == true)     
  72.                         store.Remove(x509);     
  73.                 }     
  74.             }     
  75.             store.Close();     
  76.             store = null;     
  77.             storecollection = null;     
  78.             return true;     
  79.         }     
  80.         /// <summary>     
  81.         /// 从WINDOWS证书存储区的个人MY区找到主题为subjectName的证书,     
  82.         /// 并导出为CER文件(即,只含公钥的)     
  83.         /// </summary>     
  84.         /// <param name="subjectName"></param>     
  85.         /// <param name="cerFileName"></param>     
  86.         /// <returns></returns>     
  87.         public static bool ExportToCerFile(string subjectName, string cerFileName)     
  88.         {     
  89.             subjectName = "CN=" + subjectName;     
  90.             X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);     
  91.             store.Open(OpenFlags.ReadWrite);     
  92.             X509Certificate2Collection storecollection = (X509Certificate2Collection)store.Certificates;     
  93.             foreach (X509Certificate2 x509 in storecollection)     
  94.             {     
  95.                 if (x509.Subject == subjectName)     
  96.                 {     
  97.                     Debug.Print(string.Format("certificate name: {0}", x509.Subject));     
  98.                     //byte[] pfxByte = x509.Export(X509ContentType.Pfx, password);     
  99.                     byte[] cerByte = x509.Export(X509ContentType.Cert);     
  100.                     using (FileStream fileStream = new FileStream(cerFileName, FileMode.Create))     
  101.                     {     
  102.                         // Write the data to the file, byte by byte.     
  103.                         for (int i = 0; i < cerByte.Length; i++)     
  104.                             fileStream.WriteByte(cerByte[i]);     
  105.                         // Set the stream position to the beginning of the file.     
  106.                         fileStream.Seek(0, SeekOrigin.Begin);     
  107.                         // Read and verify the data.     
  108.                         for (int i = 0; i < fileStream.Length; i++)     
  109.                         {     
  110.                             if (cerByte[i] != fileStream.ReadByte())     
  111.                             {     
  112.                                 fileStream.Close();     
  113.                                 return false;     
  114.                             }     
  115.                         }     
  116.                         fileStream.Close();     
  117.                     }     
  118.                 }     
  119.             }     
  120.             store.Close();     
  121.             store = null;     
  122.             storecollection = null;     
  123.             return true;     
  124.         }    
  125.         #endregion    
  126.   
  127.         #region 从证书中获取信息     
  128.         /// <summary>     
  129.         /// 根据私钥证书得到证书实体,得到实体后可以根据其公钥和私钥进行加解密     
  130.         /// 加解密函数使用DEncrypt的RSACryption类     
  131.         /// </summary>     
  132.         /// <param name="pfxFileName"></param>     
  133.         /// <param name="password"></param>     
  134.         /// <returns></returns>     
  135.         public static X509Certificate2 GetCertificateFromPfxFile(string pfxFileName,     
  136.             string password)     
  137.         {     
  138.             try    
  139.             {     
  140.                 return new X509Certificate2(pfxFileName, password, X509KeyStorageFlags.Exportable);     
  141.             }     
  142.             catch (Exception e)     
  143.             {     
  144.                 return null;     
  145.             }     
  146.         }     
  147.         /// <summary>     
  148.         /// 到存储区获取证书     
  149.         /// </summary>     
  150.         /// <param name="subjectName"></param>     
  151.         /// <returns></returns>     
  152.         public static X509Certificate2 GetCertificateFromStore(string subjectName)     
  153.         {     
  154.             subjectName = "CN=" + subjectName;     
  155.             X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);     
  156.             store.Open(OpenFlags.ReadWrite);     
  157.             X509Certificate2Collection storecollection = (X509Certificate2Collection)store.Certificates;     
  158.             foreach (X509Certificate2 x509 in storecollection)     
  159.             {     
  160.                 if (x509.Subject == subjectName)     
  161.                 {     
  162.                     return x509;     
  163.                 }     
  164.             }     
  165.             store.Close();     
  166.             store = null;     
  167.             storecollection = null;     
  168.             return null;     
  169.         }     
  170.        /// <summary>     
  171.         /// 根据公钥证书,返回证书实体     
  172.         /// </summary>     
  173.         /// <param name="cerPath"></param>     
  174.         public static X509Certificate2 GetCertFromCerFile(string cerPath)     
  175.         {     
  176.             try    
  177.             {     
  178.                 return new X509Certificate2(cerPath);     
  179.             }     
  180.             catch (Exception e)     
  181.             {      
  182.                 return null;     
  183.             }                 
  184.         }    
  185.         #endregion            
  186.     }    

两个RSA加解密辅助函数:

[csharp] view plaincopy
 
  1. static string RSADecrypt(string xmlPrivateKey, string m_strDecryptString)     
  2. {     
  3.     RSACryptoServiceProvider provider = new RSACryptoServiceProvider();     
  4.     provider.FromXmlString(xmlPrivateKey);     
  5.     byte[] rgb = Convert.FromBase64String(m_strDecryptString);     
  6.     byte[] bytes = provider.Decrypt(rgb, false);     
  7.     return new UnicodeEncoding().GetString(bytes);     
  8. }     
  9. /// <summary>     
  10. /// RSA加密     
  11. /// </summary>     
  12. /// <param name="xmlPublicKey"></param>     
  13. /// <param name="m_strEncryptString"></param>     
  14. /// <returns></returns>     
  15. static string RSAEncrypt(string xmlPublicKey, string m_strEncryptString)     
  16. {     
  17.     RSACryptoServiceProvider provider = new RSACryptoServiceProvider();     
  18.     provider.FromXmlString(xmlPublicKey);     
  19.     byte[] bytes = new UnicodeEncoding().GetBytes(m_strEncryptString);     
  20.     return Convert.ToBase64String(provider.Encrypt(bytes, false));     
  21. }    


使用例子,下面的代码做了几个事情

1. 在个人store里面创建了一个认证, 从认证里面读取信息得到一个X509Certificate2的对象,这个对象内部包含公钥和私钥,然后做了次rsa加解密测试。

2. 从store里面导出一个cer文件,因为cer文件并没有私钥,只有公钥。测试代码就是用公钥加密然后用前面得到的私钥解密。

3. 导出一个pfx文件,pfx包括公钥和私钥,可以自己加解密。

这是个很简单的例子,但是对于理解cer文件和pfx文件已经公钥私钥应该有帮助。

[csharp] view plaincopy
 
  1. // 在personal(个人)里面创建一个foo的证书  
  2. DataCertificate.CreateCertWithPrivateKey("foo", "C:\Program Files (x86)\Windows Kits\8.1\bin\x64\makecert.exe");  
  3.   
  4. // 获取证书  
  5. X509Certificate2 c1 = DataCertificate.GetCertificateFromStore("foo");  
  6.   
  7. string keyPublic = c1.PublicKey.Key.ToXmlString(false);  // 公钥  
  8. string keyPrivate = c1.PrivateKey.ToXmlString(true);  // 私钥  
  9.   
  10. string cypher = RSAEncrypt(keyPublic, "程序员");  // 加密  
  11. string plain = RSADecrypt(keyPrivate, cypher);  // 解密  
  12.   
  13. Debug.Assert(plain == "程序员");  
  14.   
  15. // 生成一个cert文件  
  16. DataCertificate.ExportToCerFile("foo", "d:\mycert\foo.cer");  
  17.   
  18. X509Certificate2 c2 = DataCertificate.GetCertFromCerFile("d:\mycert\foo.cer");  
  19.   
  20. string keyPublic2 = c2.PublicKey.Key.ToXmlString(false);  
  21.   
  22. bool b = keyPublic2 == keyPublic;  
  23. string cypher2 = RSAEncrypt(keyPublic2, "程序员2");  // 加密  
  24. string plain2 = RSADecrypt(keyPrivate, cypher2);  // 解密, cer里面并没有私钥,所以这里使用前面得到的私钥来解密  
  25.   
  26. Debug.Assert(plain2 == "程序员2");  
  27.   
  28. // 生成一个pfx, 并且从store里面删除  
  29. DataCertificate.ExportToPfxFile("foo", "d:\mycert\foo.pfx", "111", true);  
  30.   
  31. X509Certificate2 c3 = DataCertificate.GetCertificateFromPfxFile("d:\mycert\foo.pfx", "111");  
  32.   
  33. string keyPublic3 = c3.PublicKey.Key.ToXmlString(false);  // 公钥  
  34. string keyPrivate3 = c3.PrivateKey.ToXmlString(true);  // 私钥  
  35.   
  36. string cypher3 = RSAEncrypt(keyPublic3, "程序员3");  // 加密  
  37. string plain3 = RSADecrypt(keyPrivate3, cypher3);  // 解密  
  38.   
  39. Debug.Assert(plain3 == "程序员3");  

附:完整代码

[csharp] view plaincopy
 
    1. using System;  
    2. using System.Collections.Generic;  
    3. using System.Diagnostics;  
    4. using System.IO;  
    5. using System.Linq;  
    6. using System.Security.Cryptography;  
    7. using System.Security.Cryptography.X509Certificates;  
    8. using System.Text;  
    9.   
    10.   
    11. namespace ConsoleApplication1  
    12. {  
    13.     public sealed class DataCertificate     
    14.     {    
    15.         #region 生成证书     
    16.         /// <summary>     
    17.         /// 根据指定的证书名和makecert全路径生成证书(包含公钥和私钥,并保存在MY存储区)     
    18.         /// </summary>     
    19.         /// <param name="subjectName"></param>     
    20.         /// <param name="makecertPath"></param>     
    21.         /// <returns></returns>     
    22.         public static bool CreateCertWithPrivateKey(string subjectName, string makecertPath)     
    23.         {     
    24.             subjectName = "CN=" + subjectName;     
    25.             string param = " -pe -ss my -n "" + subjectName + "" ";     
    26.             try    
    27.             {     
    28.                 Process p = Process.Start(makecertPath, param);     
    29.                 p.WaitForExit();     
    30.                 p.Close();     
    31.             }     
    32.             catch (Exception e)     
    33.             {     
    34.                 return false;     
    35.             }     
    36.             return true;     
    37.         }    
    38.         #endregion    
    39.   
    40.         #region 文件导入导出     
    41.         /// <summary>     
    42.         /// 从WINDOWS证书存储区的个人MY区找到主题为subjectName的证书,     
    43.         /// 并导出为pfx文件,同时为其指定一个密码     
    44.         /// 并将证书从个人区删除(如果isDelFromstor为true)     
    45.         /// </summary>     
    46.         /// <param name="subjectName">证书主题,不包含CN=</param>     
    47.         /// <param name="pfxFileName">pfx文件名</param>     
    48.         /// <param name="password">pfx文件密码</param>     
    49.         /// <param name="isDelFromStore">是否从存储区删除</param>     
    50.         /// <returns></returns>     
    51.         public static bool ExportToPfxFile(string subjectName, string pfxFileName,     
    52.             string password, bool isDelFromStore)     
    53.         {     
    54.             subjectName = "CN=" + subjectName;     
    55.             X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);     
    56.             store.Open(OpenFlags.ReadWrite);     
    57.             X509Certificate2Collection storecollection = (X509Certificate2Collection)store.Certificates;     
    58.             foreach (X509Certificate2 x509 in storecollection)     
    59.             {     
    60.                 if (x509.Subject == subjectName)     
    61.                 {     
    62.                     Debug.Print(string.Format("certificate name: {0}", x509.Subject));     
    63.     
    64.                     byte[] pfxByte = x509.Export(X509ContentType.Pfx, password);     
    65.                     using (FileStream fileStream = new FileStream(pfxFileName, FileMode.Create))     
    66.                     {     
    67.                         // Write the data to the file, byte by byte.     
    68.                         for (int i = 0; i < pfxByte.Length; i++)     
    69.                             fileStream.WriteByte(pfxByte[i]);     
    70.                         // Set the stream position to the beginning of the file.     
    71.                         fileStream.Seek(0, SeekOrigin.Begin);     
    72.                         // Read and verify the data.     
    73.                         for (int i = 0; i < fileStream.Length; i++)     
    74.                         {     
    75.                             if (pfxByte[i] != fileStream.ReadByte())     
    76.                             {     
    77.                                 fileStream.Close();     
    78.                                 return false;     
    79.                             }     
    80.                         }     
    81.                         fileStream.Close();     
    82.                     }     
    83.                     if( isDelFromStore == true)     
    84.                         store.Remove(x509);     
    85.                 }     
    86.             }     
    87.             store.Close();     
    88.             store = null;     
    89.             storecollection = null;     
    90.             return true;     
    91.         }     
    92.         /// <summary>     
    93.         /// 从WINDOWS证书存储区的个人MY区找到主题为subjectName的证书,     
    94.         /// 并导出为CER文件(即,只含公钥的)     
    95.         /// </summary>     
    96.         /// <param name="subjectName"></param>     
    97.         /// <param name="cerFileName"></param>     
    98.         /// <returns></returns>     
    99.         public static bool ExportToCerFile(string subjectName, string cerFileName)     
    100.         {     
    101.             subjectName = "CN=" + subjectName;     
    102.             X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);     
    103.             store.Open(OpenFlags.ReadWrite);     
    104.             X509Certificate2Collection storecollection = (X509Certificate2Collection)store.Certificates;     
    105.             foreach (X509Certificate2 x509 in storecollection)     
    106.             {     
    107.                 if (x509.Subject == subjectName)     
    108.                 {     
    109.                     Debug.Print(string.Format("certificate name: {0}", x509.Subject));     
    110.                     //byte[] pfxByte = x509.Export(X509ContentType.Pfx, password);     
    111.                     byte[] cerByte = x509.Export(X509ContentType.Cert);     
    112.                     using (FileStream fileStream = new FileStream(cerFileName, FileMode.Create))     
    113.                     {     
    114.                         // Write the data to the file, byte by byte.     
    115.                         for (int i = 0; i < cerByte.Length; i++)     
    116.                             fileStream.WriteByte(cerByte[i]);     
    117.                         // Set the stream position to the beginning of the file.     
    118.                         fileStream.Seek(0, SeekOrigin.Begin);     
    119.                         // Read and verify the data.     
    120.                         for (int i = 0; i < fileStream.Length; i++)     
    121.                         {     
    122.                             if (cerByte[i] != fileStream.ReadByte())     
    123.                             {     
    124.                                 fileStream.Close();     
    125.                                 return false;     
    126.                             }     
    127.                         }     
    128.                         fileStream.Close();     
    129.                     }     
    130.                 }     
    131.             }     
    132.             store.Close();     
    133.             store = null;     
    134.             storecollection = null;     
    135.             return true;     
    136.         }    
    137.         #endregion    
    138.   
    139.         #region 从证书中获取信息     
    140.         /// <summary>     
    141.         /// 根据私钥证书得到证书实体,得到实体后可以根据其公钥和私钥进行加解密     
    142.         /// 加解密函数使用DEncrypt的RSACryption类     
    143.         /// </summary>     
    144.         /// <param name="pfxFileName"></param>     
    145.         /// <param name="password"></param>     
    146.         /// <returns></returns>     
    147.         public static X509Certificate2 GetCertificateFromPfxFile(string pfxFileName,     
    148.             string password)     
    149.         {     
    150.             try    
    151.             {     
    152.                 return new X509Certificate2(pfxFileName, password, X509KeyStorageFlags.Exportable);     
    153.             }     
    154.             catch (Exception e)     
    155.             {     
    156.                 return null;     
    157.             }     
    158.         }     
    159.         /// <summary>     
    160.         /// 到存储区获取证书     
    161.         /// </summary>     
    162.         /// <param name="subjectName"></param>     
    163.         /// <returns></returns>     
    164.         public static X509Certificate2 GetCertificateFromStore(string subjectName)     
    165.         {     
    166.             subjectName = "CN=" + subjectName;     
    167.             X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);     
    168.             store.Open(OpenFlags.ReadWrite);     
    169.             X509Certificate2Collection storecollection = (X509Certificate2Collection)store.Certificates;     
    170.             foreach (X509Certificate2 x509 in storecollection)     
    171.             {     
    172.                 if (x509.Subject == subjectName)     
    173.                 {     
    174.                     return x509;     
    175.                 }     
    176.             }     
    177.             store.Close();     
    178.             store = null;     
    179.             storecollection = null;     
    180.             return null;     
    181.         }     
    182.        /// <summary>     
    183.         /// 根据公钥证书,返回证书实体     
    184.         /// </summary>     
    185.         /// <param name="cerPath"></param>     
    186.         public static X509Certificate2 GetCertFromCerFile(string cerPath)     
    187.         {     
    188.             try    
    189.             {     
    190.                 return new X509Certificate2(cerPath);     
    191.             }     
    192.             catch (Exception e)     
    193.             {      
    194.                 return null;     
    195.             }                 
    196.         }    
    197.         #endregion            
    198.     }    
    199.   
    200.     class Program  
    201.     {  
    202.         static string RSADecrypt(string xmlPrivateKey, string m_strDecryptString)     
    203.         {     
    204.             RSACryptoServiceProvider provider = new RSACryptoServiceProvider();     
    205.             provider.FromXmlString(xmlPrivateKey);     
    206.             byte[] rgb = Convert.FromBase64String(m_strDecryptString);     
    207.             byte[] bytes = provider.Decrypt(rgb, false);     
    208.             return new UnicodeEncoding().GetString(bytes);     
    209.         }     
    210.         /// <summary>     
    211.         /// RSA加密     
    212.         /// </summary>     
    213.         /// <param name="xmlPublicKey"></param>     
    214.         /// <param name="m_strEncryptString"></param>     
    215.         /// <returns></returns>     
    216.         static string RSAEncrypt(string xmlPublicKey, string m_strEncryptString)     
    217.         {     
    218.             RSACryptoServiceProvider provider = new RSACryptoServiceProvider();     
    219.             provider.FromXmlString(xmlPublicKey);     
    220.             byte[] bytes = new UnicodeEncoding().GetBytes(m_strEncryptString);     
    221.             return Convert.ToBase64String(provider.Encrypt(bytes, false));     
    222.         }    
    223.   
    224.         static void Main(string[] args)  
    225.         {  
    226.             // 在personal(个人)里面创建一个foo的证书  
    227.             DataCertificate.CreateCertWithPrivateKey("foo", "C:\Program Files (x86)\Windows Kits\8.1\bin\x64\makecert.exe");  
    228.   
    229.             // 获取证书  
    230.             X509Certificate2 c1 = DataCertificate.GetCertificateFromStore("foo");  
    231.   
    232.             string keyPublic = c1.PublicKey.Key.ToXmlString(false);  // 公钥  
    233.             string keyPrivate = c1.PrivateKey.ToXmlString(true);  // 私钥  
    234.   
    235.             string cypher = RSAEncrypt(keyPublic, "程序员");  // 加密  
    236.             string plain = RSADecrypt(keyPrivate, cypher);  // 解密  
    237.   
    238.             Debug.Assert(plain == "程序员");  
    239.   
    240.             // 生成一个cert文件  
    241.             DataCertificate.ExportToCerFile("foo", "d:\mycert\foo.cer");  
    242.   
    243.             X509Certificate2 c2 = DataCertificate.GetCertFromCerFile("d:\mycert\foo.cer");  
    244.   
    245.             string keyPublic2 = c2.PublicKey.Key.ToXmlString(false);  
    246.   
    247.             bool b = keyPublic2 == keyPublic;  
    248.             string cypher2 = RSAEncrypt(keyPublic2, "程序员2");  // 加密  
    249.             string plain2 = RSADecrypt(keyPrivate, cypher2);  // 解密, cer里面并没有私钥,所以这里使用前面得到的私钥来解密  
    250.   
    251.             Debug.Assert(plain2 == "程序员2");  
    252.   
    253.             // 生成一个pfx, 并且从store里面删除  
    254.             DataCertificate.ExportToPfxFile("foo", "d:\mycert\foo.pfx", "111", true);  
    255.   
    256.             X509Certificate2 c3 = DataCertificate.GetCertificateFromPfxFile("d:\mycert\foo.pfx", "111");  
    257.   
    258.             string keyPublic3 = c3.PublicKey.Key.ToXmlString(false);  // 公钥  
    259.             string keyPrivate3 = c3.PrivateKey.ToXmlString(true);  // 私钥  
    260.   
    261.             string cypher3 = RSAEncrypt(keyPublic3, "程序员3");  // 加密  
    262.             string plain3 = RSADecrypt(keyPrivate3, cypher3);  // 解密  
    263.   
    264.             Debug.Assert(plain3 == "程序员3");  
    265.         }  
    266.     }  
    267. }  
原文地址:https://www.cnblogs.com/aiqingqing/p/4503049.html