非对称加密----加解密和数字签名

一、对称加密

对称加密:加密和解密使用相同密钥的加密算法。

对称加密的特点:

1)、速度快,通常在消息发送方需要加密大量数据时使用。

2)、密钥是控制加密及解密过程的指令。

3)、算法是一组规则,规定如何进行加密和解密。

典型应用场景:离线的大量数据加密(用于存储的)

常用的加密算法:DES、3DES、AES、TDEA、Blowfifish、RC2、RC4、RC5、IDEA、SKIPJACK等。

对称加密的工作过程如下图所示

加密的安全性不仅取决于加密算法本身,密钥管理的安全性更是重要。如何把密钥安全地传递到解密者手上就成了必须要解决的问题。 

二、非对称加密

非对称加密算法是一种密钥的保密方法,加密和解密使用两个不同的密钥公开密钥(publickey:简称公钥)和私有密钥(privatekey:简称私钥)。公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密。 

非对称加密算法的特点: 

1)、算法强度复杂

2)、加密解密速度没有对称密钥算法的速度快 

经典应用场景:1、数字签名(私钥加密,公钥验证) 。2、加解密(公钥加密,私钥解密)

常用的算法:RSA、Elgamal、背包算法、Rabin、D-H、ECC(椭圆曲线加密算法)。 

三、非对称加密之加解密

加解密示意图如下

四、非对称加密之数字签名

数字签名通常使用私钥生成签名,使用公钥验证签名。 

签名及验证过程: 

1. 发送方用一个哈希函数(例如MD5)从报文文本中生成报文摘要,然后用自己的私钥对这个摘要进行加密

2. 将加密后的摘要作为报文的数字签名和原始报文一起发送给接收方

3. 接收方用与发送方一样的哈希函数从接收到的原始报文中计算出报文摘要,

4. 接收方再用发送方的公用密钥来对报文附加的数字签名进行解密

5. 如果这两个摘要相同、接收方就能确认该数字签名是发送方的。

数字签名验证的两个作用:

1)、确定消息确实是由发送方签名并发出来的

2)、确定消息的完整性 

五、公钥和私钥到底谁来加密?

第一种用法:公钥加密,私钥解密。----用于加解密

第二种用法:私钥签名,公钥验签。----用于签名

可以理解如下:

既然是加密,那肯定是不希望别人知道我的消息,所以只有我才能解密,所以可以得出公钥加密,私钥解密。

既然是签名,那肯定不希望别人冒充我发消息,只有我才能发布这个签名,所以可以得出私钥签名,公钥验签。用于让所有公钥所有者验证私钥所有者的身份,防止私钥所有者发布的内容被篡改。但是不用来保证内容不被他人获得。

六、加解密的应用----登录时对密码进行加密

使用前后端分离的系统架构,它们的联系是通过接口调用把数据联系起来的,所有东西都走接口需要保证接口调用数据的安全,黑客可以通过抓包工具获取我们访问的信息(请求数据和响应数据)。因为http协议传输的数据是明文的,虽然https确实对传输的数据进行了一次加密,但是可以破解的。

 前后端rsa加密的流程:

第一步:返回publicKey给前端,用来对password等敏感字段的加密。

第二步,前端进行password敏感字段的加密。

第三步:通过post请求将数据给后端。

第四步:用privateKey进行解密。

由于安装的jsencrypt.js是没有压缩得,里面包含YUI,会产生安全漏洞:JavaScript库YUI版本过低,jsencrypt.min.js文件中不含有YUI,故用jsencrypt.min.js

1、将jsencrypt.min.js文件拷贝到utils目录中

  2、在index.html中引入jsencrypt.min.js文件

 为什么在index.html中引入jsencrypt.min.js?

vue中的页面都是单页面,但是都是index.html上承载的,这就是为什么你能在index.html中看到id为app的div,其实就是和App.vue对应,App.vue里面的标签将会把路由相关内容(index.js)渲染在这个地方,总之index.html是项目运行的入口。

项目加载的过程是index.tml->main.js->app.vue->index.js->单页面(XXX.vue)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title>vue_project_01</title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but vue_project_01 doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

 3、前端代码

<el-form-item prop="password">
        <span class="svg-container">
          <svg-icon icon-class="password" />
        </span>
        <el-input :type="pwdType" v-model.trim="loginForm.password" name="password" auto-complete="off"
          placeholder="密码" />
        <span class="show-pwd" @click="showPwd">
          <svg-icon icon-class="eye" />
        </span>
      </el-form-item>
<el-form-item>
<el-button :loading="loading" type="primary" style="100%;" @click.native.prevent="handleLogin">
登录
</el-button>
</el-form-item>

handleLogin方法:

handleLogin() {
        this.$refs.loginForm.validate(valid => {
          if (valid) {
            .....
            const loginData = {
              ....
              password: this.encryptedData(this.loginForm.password),
              .....
            }
            this.$store.dispatch('Login', loginData).then((res) => {
              ....
            }).catch((error) => {
              console.log(error)
            }).finally(() => {
              this.loading = false
            })
          } else {
            return false
          }
        })
      },

encryptedData方法:

encryptedData(data) {
        // 私钥 和后端沟通写死了
        var publicKey = '省略不写'
        // 新建JSEncrypt对象
        const encryptor = new JSEncrypt()
        // 设置公钥
        encryptor.setPublicKey(publicKey)
        // 加密数据
        return encryptor.encrypt(data)
      },

login方法

export function login(.., password, ...) {
  const params = { .., 'password': password,.... }
  return request({
    url: '/user/loginForNew',
    method: 'post',
    data: params
  })
}

4、后台代码

controller

@PostMapping("/loginForNew")
    @ResponseBody
    public Result loginForNew(@RequestBody JSONObject map) {
        String password = map.getString("password");
     ....
        try {
            password = rsaService.RSADecryptDataPEM(password, RsaKeys.getServerPrvKeyPkcs8());

RSADecryptDataPEM方法:

public class RsaServiceImpl implements RsaService {

    /***
     * RSA解密
     *
     * @param encryptData
     * @return
     * @throws Exception
     */
    public String RSADecryptDataPEM(String encryptData, String prvKey) throws Exception {
        byte[] encryptBytes = encryptData.getBytes();
        byte[] prvdata = RSA.decryptByPrivateKey(Base64Utils.decode(encryptData), prvKey);

        String outString = new String(prvdata, "UTF-8");
        return outString;
    }
    
    @Override
    public String RSADecryptDataBytes(byte[] encryptBytes, String prvKey)
            throws Exception {
        // TODO Auto-generated method stub
        byte[] prvdata = RSA.decryptByPrivateKey(encryptBytes, prvKey);
        String outString = new String(prvdata, "utf-8");
        return outString;
    }

    /***
     * RSA加密
     *
     * @param data
     * @return
     * @throws Exception
     */
    public String RSAEncryptDataPEM(String data, String pubKey) throws Exception {
        byte[] pubdata = RSA.encryptByPublicKey(data.getBytes("UTF-8"), pubKey);
        String outString = new String(Base64Utils.encode(pubdata));
        return outString;
    }

    @Override
    public String getRsaAlgorithm() {
        // TODO Auto-generated method stub
        KeyFactory keyFactory = null;
        try {
            keyFactory = KeyFactory.getInstance("RSA");
        } catch (NoSuchAlgorithmException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return keyFactory.getAlgorithm();
    }

    
}

RSA工具类

public class RSA {
    /** */
    /**
     * 加密算法RSA
     */
    public static final String KEY_ALGORITHM = "RSA";

    /** */
    /**
     * 签名算法
     */
    public static final String SIGNATURE_ALGORITHM = "MD5withRSA";

    /** */
    /**
     * 获取公钥的key
     */
    private static final String PUBLIC_KEY = "RSAPublicKey";

    /** */
    /**
     * 获取私钥的key
     */
    private static final String PRIVATE_KEY = "RSAPrivateKey";

    /** */
    /**
     * RSA最大加密明文大小
     */
    //private static final int MAX_ENCRYPT_BLOCK = 117;
    private static final int MAX_ENCRYPT_BLOCK = 245;

    /** */
    /**
     * RSA最大解密密文大小
     */
    //private static final int MAX_DECRYPT_BLOCK = 128;
    private static final int MAX_DECRYPT_BLOCK = 256;

    /** */
    /**
     * <p>
     * 生成密钥对(公钥和私钥)
     * </p>
     *
     * @return
     * @throws Exception
     */
    public static Map<String, Object> genKeyPair() throws Exception {
        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
        keyPairGen.initialize(1024);
        KeyPair keyPair = keyPairGen.generateKeyPair();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        Map<String, Object> keyMap = new HashMap<String, Object>(2);
        keyMap.put(PUBLIC_KEY, publicKey);
        keyMap.put(PRIVATE_KEY, privateKey);
        return keyMap;
    }

    /** */
    /**
     * <p>
     * 用私钥对信息生成数字签名
     * </p>
     *
     * @param data       已加密数据
     * @param privateKey 私钥(BASE64编码)
     * @return
     * @throws Exception
     */
    public static String sign(byte[] data, String privateKey) throws Exception {
        byte[] keyBytes = Base64Utils.decode(privateKey);
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        PrivateKey privateK = keyFactory.generatePrivate(pkcs8KeySpec);
        Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
        signature.initSign(privateK);
        signature.update(data);
        return Base64Utils.encode(signature.sign());
    }

    /** */
    /**
     * 112.     * <p>
     * 113.     * 校验数字签名
     * 114.     * </p>
     * 115.     *
     * 116.     * @param data 已加密数据
     * 117.     * @param publicKey 公钥(BASE64编码)
     * 118.     * @param sign 数字签名
     * 119.     *
     * 120.     * @return
     * 121.     * @throws Exception
     * 122.     *
     * 123.
     */
    public static boolean verify(byte[] data, String publicKey, String sign)
            throws Exception {
        byte[] keyBytes = Base64Utils.decode(publicKey);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        PublicKey publicK = keyFactory.generatePublic(keySpec);
        Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
        signature.initVerify(publicK);
        signature.update(data);
        return signature.verify(Base64Utils.decode(sign));
    }

    /** */
    /**
     * 137.     * <P>
     * 138.     * 私钥解密
     * 139.     * </p>
     * 140.     *
     * 141.     * @param encryptedData 已加密数据
     * 142.     * @param privateKey 私钥(BASE64编码)
     * 143.     * @return
     * 144.     * @throws Exception
     * 145.
     */
    public static byte[] decryptByPrivateKey(byte[] encryptedData, String privateKey)
            throws Exception {
        // byte[] keyBytes = Base64Utils.decode(privateKey);
        // PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        Key privateK = getPrivateKey(privateKey);
        //Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        cipher.init(Cipher.DECRYPT_MODE, privateK);
        
        int inputLen = encryptedData.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offSet = 0;
        byte[] cache;
        int i = 0;
        // 对数据分段解密
        while (inputLen - offSet > 0) {
            if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
                cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK);
            } else {
                cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
            }
            out.write(cache, 0, cache.length);
            i++;
            offSet = i * MAX_DECRYPT_BLOCK;
        }
        byte[] decryptedData = out.toByteArray();
        out.close();
        return decryptedData;
    }

    /**
     * 得到私钥
     *
     * @param key 密钥字符串(经过base64编码)
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(String key) throws Exception {
        byte[] keyBytes;
        keyBytes = (new BASE64Decoder()).decodeBuffer(key);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
        return privateKey;
    }

    /**
     * 得到公钥
     *
     * @param key 密钥字符串(经过base64编码)
     * @throws Exception
     */
    public static PublicKey getPublicKey(String key) throws Exception {
        byte[] keyBytes;
        keyBytes = (new BASE64Decoder()).decodeBuffer(key);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey publicKey = keyFactory.generatePublic(keySpec);
        return publicKey;
    }
    /** */
    /**
     * 176.     * <p>
     * 177.     * 公钥解密
     * 178.     * </p>
     * 179.     *
     * 180.     * @param encryptedData 已加密数据
     * 181.     * @param publicKey 公钥(BASE64编码)
     * 182.     * @return
     * 183.     * @throws Exception
     * 184.
     */
    public static byte[] decryptByPublicKey(byte[] encryptedData, String publicKey)
            throws Exception {
        byte[] keyBytes = Base64Utils.decode(publicKey);
        X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        Key publicK = keyFactory.generatePublic(x509KeySpec);
        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        cipher.init(Cipher.DECRYPT_MODE, publicK);
        int inputLen = encryptedData.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offSet = 0;
        byte[] cache;
        int i = 0;
        // 对数据分段解密
        while (inputLen - offSet > 0) {
            if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
                cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK);
            } else {
                cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
            }
            out.write(cache, 0, cache.length);
            i++;
            offSet = i * MAX_DECRYPT_BLOCK;
        }
        byte[] decryptedData = out.toByteArray();
        out.close();
        return decryptedData;
    }

    /** */
    /**
     * 215.     * <p>
     * 216.     * 公钥加密
     * 217.     * </p>
     * 218.     *
     * 219.     * @param data 源数据
     * 220.     * @param publicKey 公钥(BASE64编码)
     * 221.     * @return
     * 222.     * @throws Exception
     * 223.
     */
    public static byte[] encryptByPublicKey(byte[] data, String publicKey)
            throws Exception {
        // byte[] keyBytes = Base64Utils.decode(publicKey);
        // X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        Key publicK = getPublicKey(publicKey);
        // 对数据加密
        //Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        cipher.init(Cipher.ENCRYPT_MODE, publicK);
        int inputLen = data.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offSet = 0;
        byte[] cache;
        int i = 0;
        // 对数据分段加密
        while (inputLen - offSet > 0) {
            if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
                cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK);
            } else {
                cache = cipher.doFinal(data, offSet, inputLen - offSet);
            }
            out.write(cache, 0, cache.length);
            i++;
            offSet = i * MAX_ENCRYPT_BLOCK;
        }
        byte[] encryptedData = out.toByteArray();
        out.close();
        return encryptedData;
    }

    /** */
    /**
     * 255.     * <p>
     * 256.     * 私钥加密
     * 257.     * </p>
     * 258.     *
     * 259.     * @param data 源数据
     * 260.     * @param privateKey 私钥(BASE64编码)
     * 261.     * @return
     * 262.     * @throws Exception
     * 263.
     */
    public static byte[] encryptByPrivateKey(byte[] data, String privateKey)
            throws Exception {
        byte[] keyBytes = Base64Utils.decode(privateKey);
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        Key privateK = keyFactory.generatePrivate(pkcs8KeySpec);
        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        cipher.init(Cipher.ENCRYPT_MODE, privateK);
        int inputLen = data.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offSet = 0;
        byte[] cache;
        int i = 0;
        // 对数据分段加密
        while (inputLen - offSet > 0) {
            if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
                cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK);
            } else {
                cache = cipher.doFinal(data, offSet, inputLen - offSet);
            }
            out.write(cache, 0, cache.length);
            i++;
            offSet = i * MAX_ENCRYPT_BLOCK;
        }
        byte[] encryptedData = out.toByteArray();
        out.close();
        return encryptedData;
    }
    /** */
    /**
     * 309.     * <p>
     * 310.     * 获取公钥
     * 311.     * </p>
     * 312.     *
     * 313.     * @param keyMap 密钥对
     * 314.     * @return
     * 315.     * @throws Exception
     * 316.
     */
    public static String getPublicKey(Map<String, Object> keyMap)
            throws Exception {
        Key key = (Key) keyMap.get(PUBLIC_KEY);
        return Base64Utils.encode(key.getEncoded());
    }


}

公钥和私钥所在实体类

生成公钥和私钥的方法参见https://www.cnblogs.com/zwh0910/p/15214672.html的方法一。

public class RsaKeys {//服务器公钥
    private static final String serverPubKey = "省略";

    //服务器私钥(经过pkcs8格式处理)
    private static final String serverPrvKeyPkcs8 = "省略";

    public static String getServerPubKey() {
        return serverPubKey;
    }
    public static String getServerPrvKeyPkcs8() {
        return serverPrvKeyPkcs8;
    }
    
}

七、加解密的应用----使用私钥对JWT进行签名,使用公钥验签

创建测试类,测试jwt令牌的生成与验证。

生成公钥的方法参见https://www.cnblogs.com/zwh0910/p/15214672.html的方法二。

Spring Security 提供对JWT的支持,本节我们使用Spring Security 提供的JwtHelper来创建JWT令牌,校验JWT令牌等操作。

1、生成令牌

//生成一个jwt令牌 
@Test 
public void testCreateJwt(){ 
    //证书文件 
    String key_location = "xc.keystore"; 
    //密钥库密码 
    String keystore_password = "xuechengkeystore"; 
    //访问证书路径 
    ClassPathResource resource = new ClassPathResource(key_location); 
    //密钥工厂 
    KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource, keystore_password.toCharArray()); 
    //密钥的密码,此密码和别名要匹配
    String keypassword = "xuecheng"; 
    //密钥别名 
    String alias = "xckey"; 
    //密钥对(密钥和公钥) 
    KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias,keypassword.toCharArray()); 
    //私钥 
    RSAPrivateKey aPrivate = (RSAPrivateKey) keyPair.getPrivate(); 
    //定义payload信息 
    Map<String, Object> tokenMap = new HashMap<>(); 
    tokenMap.put("id", "123"); 
    tokenMap.put("name", "mrt"); 
    tokenMap.put("roles", "r01,r02"); 
    tokenMap.put("ext", "1"); 
    //生成jwt令牌 
    Jwt jwt = JwtHelper.encode(JSON.toJSONString(tokenMap), new RsaSigner(aPrivate));  // 使用私钥签名
    //取出jwt令牌 
    String token = jwt.getEncoded(); 
    System.out.println("token="+token); 
}

2、验证jwt令牌

//资源服务使用公钥验证jwt的合法性,并对jwt解码 
@Test 
public void testVerify(){ 
    //jwt令牌 
    String token ="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHQiOiIxIiwicm9sZXMiOiJyMDEscjAyIiwibmFtZSI6Im1ydCIsI mlkIjoiMTIzIn0.
    KK7_67N5d1Dthd1PgDHMsbi0UlmjGRcm_XJUUwseJ2eZyJJWoPP2IcEZgAU3tUaaKEHUf9wSRwaDgwhrw fyIcSHbs8oy3zOQEL8j5AOjzBBs7vnRmB7DbSaQD7eJ
    iQVJOXO1QpdmEFgjhc_IBCVTJCVWgZw60IEW1_Lg5tqaLvCiIl26K 48pJB5f‐le2zgYMzqR1L2LyTFkq39rG57VOqqSCi3dapsZQd4ctq95SJCXgGdrUDWtD52rp5
    o6_0uq‐ mrbRdRxkrQfsa1j8C5IW2‐T4eUmiN3f9wF9JxUK1__XC1OQkOn‐ZTBCdqwWIygDFbU7sf6KzfHJTm5vfjp6NIA"; 
    //公钥 
    String publickey = "‐‐‐‐‐BEGIN PUBLIC KEY‐‐‐‐‐ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAijyxMdq4S6L1Af1rtB8SjCZHNgsQG8JTfGy55e
    YvzG0B/E4AudR2 prSRBvF7NYPL47scRCNPgLnvbQczBHbBug6uOr78qnWsYxHlW6Aa5dI5NsmOD4DLtSw8eX0hFyK5Fj6ScYOSFBz9cd1nNTvx 2+oIv0lJDcpQdQ
    hsfgsEr1ntvWterZt/8r7xNN83gHYuZ6TM5MYvjQNBc5qC7Krs9wM7UoQuL+s0X6RlOib7/mcLn/lFLsLD dYQAZkSDx/6+t+1oHdMarChIPYT1sx9Dwj2j2mvFNDT
    KKKKAq0cv14Vrhz67Vjmz2yMJePDqUi0JYS2r0iIo7n8vN7s83v5u OQIDAQAB‐‐‐‐‐END PUBLIC KEY‐‐‐‐‐"; 
    //校验jwt 
    Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publickey));  // 使用公钥校验签名
    //获取jwt原始内容 
    String claims = jwt.getClaims(); 
    //获取jwt令牌 
    String encoded = jwt.getEncoded(); 
    System.out.println(encoded); 
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
原文地址:https://www.cnblogs.com/zwh0910/p/15215671.html