zoukankan      html  css  js  c++  java
  • RSA非对称加密算法实现:Java

      RSA是1977年由罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)一起提出的。当时他们三人都在麻省理工学院工作。RSA就是他们三人姓氏开头字母拼在一起组成的。

      RSA解决了对称加密的一个不足,比如AES算法加密和解密时使用的是同一个秘钥,因此这个秘钥不能公开,因此对于需要公开秘钥的场合,我们需要在加密和解密过程中使用不同的秘钥,加密使用的公钥可以公开,解密使用的私钥要保密,这就是非对称加密的好处。 

      常用的开发语言来实现RSA加密:

      RSA非对称加密算法实现:Java

      RSA非对称加密算法实现:C#

      RSA非对称加密算法实现:Golang

      RSA非对称加密算法实现:Python

     

      公钥与私钥

      公钥与私钥是成对的,一般的,我们认为的是公钥加密、私钥解密、私钥签名、公钥验证,有人说成私钥加密,公钥解密时不对的。

      公钥与私钥的生成有多种方式,可以通过程序生成(下文具体实现),可以通过openssl工具:  

        # 生成一个私钥,推荐使用1024位的秘钥,秘钥以pem格式保存到-out参数指定的文件中,采用PKCS1格式
        openssl genrsa -out rsa.pem 1024 
        # 生成与私钥对应的公钥,生成的是Subject Public Key,一般配合PKCS8格式私钥使用
        openssl rsa -in rsa.pem -pubout -out rsa.pub  

      RSA生成公钥与私钥一般有两种格式:PKCS1和PKCS8,上面的命令生成的秘钥是PKCS1格式的,而公钥是Subject Public Key,一般配合PKCS8格式私钥使用,所以就可能会涉及到PKCS1和PKCS8之间的转换:

        # PKCS1格式私钥转换为PKCS8格式私钥,私钥直接输出到-out参数指定的文件中
        openssl pkcs8 -topk8 -inform PEM -in rsa.pem -outform pem -nocrypt -out rsa_pkcs8.pem
        # PKCS8格式私钥转换为PKCS1格式私钥,私钥直接输出到-out参数指定的文件中
        openssl rsa -in rsa_pkcs8.pem -out rsa_pkcs1.pem
    
        # PKCS1格式公钥转换为PKCS8格式公钥,转换后的内容直接输出
        openssl rsa -pubin -in rsa.pub -RSAPublicKey_out
        # PKCS8格式公钥转换为PKCS1格式公钥,转换后的内容直接输出
        openssl rsa -RSAPublicKey_in -pubout -in rsa.pub

      现实中,我们往往从pem、crt、pfx文件获取公私和私钥,crt、pfx的制作可以参考:简单的制作ssl证书,并在nginx和IIS中使用,或者使用现成的:https://pan.baidu.com/s/1MJ5YmuZiLBnf-DfNR_6D7A (提取码:c6tj),密码都是:123456

      Java实现

      为简化说明介绍,这里我直接封装了一个工具类,因为要从pem、crt、pfx文件获取公私和私钥,因此引用了一个第三方包:BouncyCastle,可以直接在pom.xml中添加依赖:  

        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk15on</artifactId>
            <version>1.68</version>
        </dependency>

      或者去mvn上下载:跳转

      简单封装的RsaUtil.java:    

      
    import java.io.FileInputStream;
    import java.io.FileReader;
    import java.io.FileWriter;
    import java.math.BigInteger;
    import java.security.KeyFactory;
    import java.security.KeyPair;
    import java.security.KeyPairGenerator;
    import java.security.KeyStore;
    import java.security.PrivateKey;
    import java.security.PublicKey;
    import java.security.SecureRandom;
    import java.security.Security;
    import java.security.Signature;
    import java.security.cert.Certificate;
    import java.security.cert.CertificateFactory;
    import java.security.cert.X509Certificate;
    import java.security.interfaces.RSAPrivateKey;
    import java.security.interfaces.RSAPublicKey;
    import java.security.spec.KeySpec;
    import java.security.spec.PKCS8EncodedKeySpec;
    import java.security.spec.RSAPrivateCrtKeySpec;
    import java.security.spec.RSAPublicKeySpec;
    import java.security.spec.X509EncodedKeySpec;
    import java.util.Enumeration;
    
    import javax.crypto.Cipher;
    
    import org.bouncycastle.asn1.ASN1Integer;
    import org.bouncycastle.asn1.ASN1Primitive;
    import org.bouncycastle.asn1.DLSequence;
    import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
    import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
    import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
    import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
    import org.bouncycastle.jce.provider.BouncyCastleProvider;
    import org.bouncycastle.util.io.pem.PemObject;
    import org.bouncycastle.util.io.pem.PemReader;
    import org.bouncycastle.util.io.pem.PemWriter;
    
    public class RsaUtil {
    
        static {
            Security.addProvider(new BouncyCastleProvider());
        }
    
        /**
         * 随机生成密钥对
         * 
         * @param usePKCS8
         *            是否采用PKCS8填充模式
         */
        public static Object[] generateRsaKey(boolean usePKCS8) throws Exception {
            KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME);
            // 初始化
            keyPairGen.initialize(1024, new SecureRandom());
            // 生成一个密钥对,保存在keyPair中
            KeyPair keyPair = keyPairGen.generateKeyPair();
            RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); // 得到私钥
            RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); // 得到公钥
    
            // 这两个公私钥是PKCS8格式的
            byte[] publicKeyBytes = publicKey.getEncoded();
            byte[] privateKeyBytes = privateKey.getEncoded();
    
            if (!usePKCS8) {
                // 将PSCK8格式公私钥转换为PKCS1格式
                publicKeyBytes = pkcs8ToPkcs1(false, publicKeyBytes);
                privateKeyBytes = pkcs8ToPkcs1(true, privateKeyBytes);
            }
    
            return new Object[] { publicKeyBytes, privateKeyBytes };
        }
    
        /**
         * 从Pem文件读取密钥对
         * 
         * @param reader
         *            输入流
         * @param pemFileName
         *            pem文件
         */
        public static byte[] readFromPem(String pemFileName) throws Exception {
            PemReader pemReader = new PemReader(new FileReader(pemFileName));
            PemObject pemObject = pemReader.readPemObject();
            byte[] publicKey = pemObject.getContent();
            pemReader.close();
            return publicKey;
        }
    
        /**
         * 从Pem文件读取密钥
         * 
         * @param isPrivateKey
         *            是否是私钥
         * @param buffer
         *            字节
         * @param pemFileName
         *            pem文件
         */
        public static void writeToPem(byte[] buffer, boolean isPrivateKey, String pemFileName) throws Exception {
    
            PemObject pemObject = new PemObject(isPrivateKey ? "RSA PRIVATE KEY" : "RSA PUBLIC KEY", buffer);
            FileWriter fileWriter = new FileWriter(pemFileName);
            PemWriter pemWriter = new PemWriter(fileWriter);
            pemWriter.writeObject(pemObject);
            pemWriter.close();
        }
    
        /**
         * 从crt文件读取公钥(pkcs8)
         * 
         * @param crtFileName
         *            crt文件
         * @return 公钥
         */
        public static byte[] readPublicKeyFromCrt(String crtFileName) throws Exception {
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            X509Certificate cert = (X509Certificate) cf.generateCertificate(new FileInputStream(crtFileName));
    
            PublicKey publicKey = cert.getPublicKey();
            return publicKey.getEncoded();
        }
    
        /**
         * 从pfx文件读取秘钥对(pkcs8)
         * 
         * @param pfxFileName
         *            pfx文件
         * @return 秘钥对
         */
        public static Object[] readFromPfx(String pfxFileName, String password) throws Exception {
            KeyStore keystore = KeyStore.getInstance("PKCS12");
            char[] passwordChars = null;
            if (password == null || password.equals("")) {
                passwordChars = null;
            } else {
                passwordChars = password.toCharArray();
            }
            keystore.load(new FileInputStream(pfxFileName), passwordChars);
            Enumeration<String> enums = keystore.aliases();
    
            PrivateKey privateKey = null;
            Certificate certificate = null;
            while (enums.hasMoreElements()) {
                String alias = enums.nextElement();
                System.out.println(alias);
                if (keystore.isKeyEntry(alias)) {
                    privateKey = (PrivateKey) keystore.getKey(alias, passwordChars);
                    certificate = keystore.getCertificate(alias);
                }
                if (privateKey != null && certificate != null)
                    break;
            }
            if (privateKey == null || certificate == null) {
                throw new Exception("fail to read key from pfx");
            }
    
            PublicKey publicKey = certificate.getPublicKey();
            return new Object[] { publicKey.getEncoded(), privateKey.getEncoded() };
        }
    
        /**
         * Pkcs8转Pkcs1
         * 
         * @param isPrivateKey
         *            是否是私钥转换
         * @param buffer
         *            Pkcs1秘钥
         * @return Pkcs8秘钥
         * @throws Exception
         *             加密过程中的异常信息
         */
        public static byte[] pkcs8ToPkcs1(boolean isPrivateKey, byte[] buffer) throws Exception {
            if (isPrivateKey) {
                PrivateKeyInfo privateKeyInfo = PrivateKeyInfo.getInstance(buffer);
                return privateKeyInfo.parsePrivateKey().toASN1Primitive().getEncoded();
            } else {
                SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(buffer);
                return subjectPublicKeyInfo.parsePublicKey().toASN1Primitive().getEncoded();
            }
        }
    
        /**
         * Pkcs1转Pkcs8
         * 
         * @param isPrivateKey
         *            是否是私钥转换
         * @param buffer
         *            Pkcs1秘钥
         * @return Pkcs8秘钥
         * @throws Exception
         *             加密过程中的异常信息
         */
        public static byte[] pkcs1ToPkcs8(boolean isPrivateKey, byte[] buffer) throws Exception {
            AlgorithmIdentifier algorithmIdentifier = new AlgorithmIdentifier(PKCSObjectIdentifiers.rsaEncryption);
            ASN1Primitive asn1Primitive = ASN1Primitive.fromByteArray(buffer);
            if (isPrivateKey) {
                PrivateKeyInfo privateKeyInfo = new PrivateKeyInfo(algorithmIdentifier, asn1Primitive);
                return privateKeyInfo.getEncoded();
            } else {
                SubjectPublicKeyInfo subjectPublicKeyInfo = new SubjectPublicKeyInfo(algorithmIdentifier, asn1Primitive);
                return subjectPublicKeyInfo.getEncoded();
            }
        }
    
        /**
         * RSA公钥
         * 
         * @param usePKCS8
         *            是否采用PKCS8填充模式
         * @param publicKey
         *            公钥
         * @return 公钥
         * @throws Exception
         *             加密过程中的异常信息
         */
        public static RSAPublicKey generatePublicKey(boolean usePKCS8, byte[] publicKey) throws Exception {
            KeySpec keySpec;
            if (usePKCS8) {
                // PKCS8填充
                keySpec = new X509EncodedKeySpec(publicKey);
            } else {
                // PKCS1填充
                DLSequence sequence = (DLSequence) ASN1Primitive.fromByteArray(publicKey);
                BigInteger v1 = ((ASN1Integer) sequence.getObjectAt(0)).getValue();
                BigInteger v2 = ((ASN1Integer) sequence.getObjectAt(1)).getValue();
                keySpec = new RSAPublicKeySpec(v1, v2);
            }
    
            RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME).generatePublic(keySpec);
            return pubKey;
        }
    
        /**
         * RSA私钥
         * 
         * @param usePKCS8
         *            是否采用PKCS8填充模式
         * @param privateKey
         *            私钥
         * @return 私钥
         * @throws Exception
         *             解密过程中的异常信息
         */
        public static RSAPrivateKey generatePrivateKey(boolean usePKCS8, byte[] privateKey) throws Exception {
            KeySpec keySpec;
            if (usePKCS8) {
                // PKCS8填充
                keySpec = new PKCS8EncodedKeySpec(privateKey);
            } else {
                // PKCS1填充
                DLSequence sequence = (DLSequence) ASN1Primitive.fromByteArray(privateKey);
                // BigInteger v1= ((ASN1Integer)sequence.getObjectAt(0)).getValue();
                BigInteger v2 = ((ASN1Integer) sequence.getObjectAt(1)).getValue();
                BigInteger v3 = ((ASN1Integer) sequence.getObjectAt(2)).getValue();
                BigInteger v4 = ((ASN1Integer) sequence.getObjectAt(3)).getValue();
                BigInteger v5 = ((ASN1Integer) sequence.getObjectAt(4)).getValue();
                BigInteger v6 = ((ASN1Integer) sequence.getObjectAt(5)).getValue();
                BigInteger v7 = ((ASN1Integer) sequence.getObjectAt(6)).getValue();
                BigInteger v8 = ((ASN1Integer) sequence.getObjectAt(7)).getValue();
                BigInteger v9 = ((ASN1Integer) sequence.getObjectAt(8)).getValue();
                keySpec = new RSAPrivateCrtKeySpec(v2, v3, v4, v5, v6, v7, v8, v9);
            }
    
            RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME).generatePrivate(keySpec);
            return priKey;
        }
    
        /**
         * RSA公钥加密
         * 
         * @param value
         *            加密字符串
         * @param publicKey
         *            公钥
         * @return 密文
         * @throws Exception
         *             加密过程中的异常信息
         */
        public static String rsaEncrypt(String value, RSAPublicKey publicKey) throws Exception {
            if (value == null || value.length() == 0)
                return "";
    
            // RSA加密
            Cipher cipher = Cipher.getInstance("RSA");
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            byte[] buffer = cipher.doFinal(value.getBytes("utf-8"));
    
            // 使用hex格式输出公钥
            StringBuffer result = new StringBuffer();
            for (int i = 0; i < buffer.length; i++) {
                result.append(String.format("%02x", buffer[i]));
            }
            return result.toString();
        }
    
        /**
         * RSA私钥解密
         * 
         * @param value
         *            加密字符串
         * @param privateKey
         *            私钥
         * @return 明文
         * @throws Exception
         *             解密过程中的异常信息
         */
        public static String rsaDecrypt(String value, RSAPrivateKey privateKey) throws Exception {
            if (value == null || value.length() == 0)
                return "";
    
            byte[] buffer = new byte[value.length() / 2];
            for (int i = 0; i < buffer.length; i++) {
                buffer[i] = (byte) Integer.parseInt(value.substring(i * 2, i * 2 + 2), 16);
            }
    
            // RSA解密
            Cipher cipher = Cipher.getInstance("RSA");
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            buffer = cipher.doFinal(buffer);
            return new String(buffer, "utf-8");
        }
    
        /**
         * RSA签名
         * 
         * @param value
         *            加密字符串
         * @param privateKey
         *            私钥
         * @param halg
         *            加密算法,如MD5, SHA1, SHA256, SHA384, SHA512等
         * @return 签名
         * @throws Exception
         *             签名过程中的异常信息
         */
        public static String sign(String value, RSAPrivateKey privateKey, String halg) throws Exception {
    
            Signature s = Signature.getInstance(halg.toUpperCase().endsWith("WithRSA") ? halg : (halg + "WithRSA"));
    
            s.initSign(privateKey);
            s.update(value.getBytes("utf-8"));
    
            byte[] buffer = s.sign();
    
            // 使用hex格式输出公钥
            StringBuffer result = new StringBuffer();
            for (int i = 0; i < buffer.length; i++) {
                result.append(String.format("%02x", buffer[i]));
            }
            return result.toString();
        }
    
        /**
         * RSA签名验证
         * 
         * @param value
         *            加密字符串
         * @param publicKey
         *            公钥
         * @param halg
         *            加密算法,如MD5, SHA1, SHA256, SHA384, SHA512等
         * @return 签名合法则返回true,否则返回false
         * @throws Exception
         *             验证过程中的异常信息
         */
        public static boolean verify(String value, RSAPublicKey publicKey, String signature, String halg) throws Exception {
            Signature s = Signature.getInstance(halg.toUpperCase().endsWith("WithRSA") ? halg : (halg + "WithRSA"));
            s.initVerify(publicKey);
            s.update(value.getBytes("utf-8"));
    
            byte[] buffer = new byte[signature.length() / 2];
            for (int i = 0; i < buffer.length; i++) {
                buffer[i] = (byte) Integer.parseInt(signature.substring(i * 2, i * 2 + 2), 16);
            }
    
            return s.verify(buffer);
        }
    
    }
    RsaUtil

      生成公钥和私钥:  

        // 生成公私钥
        Object[] rsaKey = RsaUtil.generateRsaKey(usePKCS8); //usePKCS8=true表示是否成PKCS8格式的公私秘钥,否则乘车PKCS1格式的公私秘钥
        byte[] publicKey = (byte[]) rsaKey[0];
        byte[] privateKey = (byte[]) rsaKey[1];

      生成秘钥后,需要保存,一般保存到pem文件中:  

        // 保存到pem文件,filePath是保存目录
        RsaUtil.writeToPem(publicKey, false, filePath + "rsa.pub");
        RsaUtil.writeToPem(privateKey, true, filePath + "rsa.pem");

      可以保存到pem文件中,当然也可以从pem文件中读取了:  

        // 从Pem文件读取公私钥,filePath是文件目录
        byte[] publicKey = RsaUtil.readFromPem(filePath + "rsa.pub");
        byte[] privateKey = RsaUtil.readFromPem(filePath + "rsa.pem");

      还可以从crt证书中读取公钥,而crt文件不包含私钥,因此需要单独获取私钥:  

        // 从crt文件读取公钥(crt文件中不包含私钥),filePath是文件目录
        byte[] publicKey = RsaUtil.readPublicKeyFromCrt(filePath + "demo.crt");
        byte[] privateKey = RsaUtil.readFromPem(filePath + "demo.key");

      pfx文件中包含了公钥和私钥,可以很方便就读取到:  

        // 从pfx文件读取公私钥,filePath是文件目录
        Object[] rsaKey = RsaUtil.readFromPfx(filePath + "demo.pfx", "123456");
        byte[] publicKey = (byte[]) rsaKey[0];
        byte[] privateKey = (byte[]) rsaKey[1];

      有时候我们还可能需要进行秘钥的转换:  

        // Pkcs8格式公钥转换为Pkcs1格式公钥
        publicKey = RsaUtil.pkcs8ToPkcs1(false, publicKey);
        // Pkcs8格式私钥转换为Pkcs1格式私钥
        privateKey = RsaUtil.pkcs8ToPkcs1(true, privateKey);
        // Pkcs1格式公钥转换为Pkcs8格式公钥
        publicKey = RsaUtil.pkcs1ToPkcs8(false, publicKey);
        // Pkcs1格式私钥转换为Pkcs8格式私钥
        privateKey = RsaUtil.pkcs1ToPkcs8(true, privateKey);

      有了公钥和私钥,接下就就能实现加密、解密、签名、验证签名等操作了:  

        RSAPublicKey rsaPublicKey = RsaUtil.generatePublicKey(usePKCS8, publicKey);
        RSAPrivateKey rsaPrivateKey = RsaUtil.generatePrivateKey(usePKCS8, privateKey);
    
        String encryptText = RsaUtil.rsaEncrypt(text, rsaPublicKey);
        System.out.printf("【%s】经过【RSA】加密后:%s
    ", text, encryptText);
    
        String decryptText = RsaUtil.rsaDecrypt(encryptText, rsaPrivateKey);
        System.out.printf("【%s】经过【RSA】解密后:%s
    ", encryptText, decryptText);
    
        String signature = RsaUtil.sign(text, rsaPrivateKey, "MD5");
        System.out.printf("【%s】经过【RSA】签名后:%s
    ", text, signature);
    
        boolean result = RsaUtil.verify(text, rsaPublicKey, signature, "MD5");
        System.out.printf("【%s】的签名【%s】经过【RSA】验证后结果是:" + result, text, signature);

      这里完整的demo代码:  

        import java.security.interfaces.RSAPrivateKey;
        import java.security.interfaces.RSAPublicKey;
        
        public class RsaMain {
        
            public static void main(String[] args) {
                try {
                    String text = "上山打老虎";
                    boolean usePKCS8 = true; // usePKCS8=true表示是否成PKCS8格式的公私秘钥,否则乘车PKCS1格式的公私秘钥
                    String filePath = RsaUtil.class.getClassLoader().getResource("").getPath();
                    System.out.printf("文件路径:%s
    ", filePath);// 存放pem,crt,pfx等文件的目录
                    byte[] publicKey, privateKey;// 公钥和私钥
        
                    // 生成公私钥
                    Object[] rsaKey = RsaUtil.generateRsaKey(usePKCS8); // usePKCS8=true表示是否成PKCS8格式的公私秘钥,否则乘车PKCS1格式的公私秘钥
                    publicKey = (byte[]) rsaKey[0];
                    privateKey = (byte[]) rsaKey[1];
                    // 从Pem文件读取公私钥,filePath是文件目录
                    // publicKey = RsaUtil.readFromPem(filePath + "rsa.pub");
                    // privateKey = RsaUtil.readFromPem(filePath + "rsa.pem");
                    // 从pfx文件读取公私钥,filePath是文件目录
                    // Object[] rsaKey = RsaUtil.readFromPfx(filePath + "demo.pfx",
                    // "123456");
                    // publicKey = (byte[]) rsaKey[0];
                    // privateKey = (byte[]) rsaKey[1];
                    // 从crt文件读取公钥(crt文件中不包含私钥),filePath是文件目录
                    // publicKey = RsaUtil.readPublicKeyFromCrt(filePath + "demo.crt");
                    // privateKey = RsaUtil.readFromPem(filePath + "demo.key");
        
                    // 保存到pem文件,filePath是保存目录
                    RsaUtil.writeToPem(publicKey, false, filePath + "rsa.pub");
                    RsaUtil.writeToPem(privateKey, true, filePath + "rsa.pem");
        
                    // Pkcs8格式公钥转换为Pkcs1格式公钥
                    publicKey = RsaUtil.pkcs8ToPkcs1(false, publicKey);
                    // Pkcs8格式私钥转换为Pkcs1格式私钥
                    privateKey = RsaUtil.pkcs8ToPkcs1(true, privateKey);
                    // Pkcs1格式公钥转换为Pkcs8格式公钥
                    publicKey = RsaUtil.pkcs1ToPkcs8(false, publicKey);
                    // Pkcs1格式私钥转换为Pkcs8格式私钥
                    privateKey = RsaUtil.pkcs1ToPkcs8(true, privateKey);
        
                    RSAPublicKey rsaPublicKey = RsaUtil.generatePublicKey(usePKCS8, publicKey);
                    RSAPrivateKey rsaPrivateKey = RsaUtil.generatePrivateKey(usePKCS8, privateKey);
        
                    String encryptText = RsaUtil.rsaEncrypt(text, rsaPublicKey);
                    System.out.printf("【%s】经过【RSA】加密后:%s
    ", text, encryptText);
        
                    String decryptText = RsaUtil.rsaDecrypt(encryptText, rsaPrivateKey);
                    System.out.printf("【%s】经过【RSA】解密后:%s
    ", encryptText, decryptText);
        
                    String signature = RsaUtil.sign(text, rsaPrivateKey, "MD5");
                    System.out.printf("【%s】经过【RSA】签名后:%s
    ", text, signature);
        
                    boolean result = RsaUtil.verify(text, rsaPublicKey, signature, "MD5");
                    System.out.printf("【%s】的签名【%s】经过【RSA】验证后结果是:" + result, text, signature);
        
                } catch (Exception e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    一个专注于.NetCore的技术小白
  • 相关阅读:
    peudoclass与pseudo的相同点与不同点
    第一个页面
    自我介绍
    Virtual IP Address 学习记录
    OpenStack 学习记录
    Dubbo学习记录 MAC
    售前 银行
    log4j2 学习记录 Pattern Layout
    Zookeeper学习记录 mac下安装部署
    P2695 骑士的工作
  • 原文地址:https://www.cnblogs.com/shanfeng1000/p/14840051.html
Copyright © 2011-2022 走看看