非對稱加密算法 (RSA、DSA、ECC、DH)

一、簡介

1.1 概念

非對稱加密需要兩個(gè)密鑰:公鑰(publickey) 和私鑰 (privatekey)。公鑰和私鑰是一對,如果用公鑰對數(shù)據(jù)加密,那么只能用對應(yīng)的私鑰解密。如果用私鑰對數(shù)據(jù)加密,只能用對應(yīng)的公鑰進(jìn)行解密。因?yàn)榧用芎徒饷苡玫氖遣煌拿荑€,所以稱為非對稱加密。

非對稱加密算法的保密性好,它消除了最終用戶交換密鑰的需要。但是加解密速度要遠(yuǎn)遠(yuǎn)慢于對稱加密,在某些極端情況下,甚至能比對稱加密慢上1000倍。

1.2 特點(diǎn)

算法強(qiáng)度復(fù)雜、安全性依賴于算法與密鑰但是由于其算法復(fù)雜,而使得加密解密速度沒有對稱加密解密的速度快。對稱密碼體制中只有一種密鑰,并且是非公開的,如果要解密就得讓對方知道密鑰。所以保證其安全性就是保證密鑰的安全,而非對稱密鑰體制有兩種密鑰,其中一個(gè)是公開的,這樣就可以不需要像對稱密碼那樣傳輸對方的密鑰了。這樣安全性就大了很多。

1.3 工作原理

  • (1) A 要向 B 發(fā)送信息,A 和 B 都要產(chǎn)生一對用于加密和解密的公鑰和私鑰。
  • (2) A 的私鑰保密,A 的公鑰告訴 B;B 的私鑰保密,B 的公鑰告訴 A。
  • (3) A 要給 B 發(fā)送信息時(shí),A 用 B 的公鑰加密信息,因?yàn)?A 知道 B 的公鑰。
  • (4) A 將這個(gè)消息發(fā)給 B (已經(jīng)用 B 的公鑰加密消息)。
  • (5) B 收到這個(gè)消息后,B 用自己的私鑰解密 A 的消息。其他所有收到這個(gè)報(bào)文的人都無法解密,因?yàn)橹挥?B 才有 B 的私鑰。

1.4 主要算法

RSA、Elgamal、背包算法、Rabin、D-H、ECC (橢圓曲線加密算法)。使用最廣泛的是 RSA 算法,Elgamal 是另一種常用的非對稱加密算法。

1.5 應(yīng)用場景

  • (1) 信息加密

收信者是唯一能夠解開加密信息的人,因此收信者手里的必須是私鑰。發(fā)信者手里的是公鑰,其它人知道公鑰沒有關(guān)系,因?yàn)槠渌税l(fā)來的信息對收信者沒有意義。

  • (2) 登錄認(rèn)證

客戶端需要將認(rèn)證標(biāo)識傳送給服務(wù)器,此認(rèn)證標(biāo)識 (可能是一個(gè)隨機(jī)數(shù)) 其它客戶端可以知道,因此需要用私鑰加密,客戶端保存的是私鑰。服務(wù)器端保存的是公鑰,其它服務(wù)器知道公鑰沒有關(guān)系,因?yàn)榭蛻舳瞬恍枰卿浧渌?wù)器。

  • (3) 數(shù)字簽名

數(shù)字簽名是為了表明信息沒有受到偽造,確實(shí)是信息擁有者發(fā)出來的,附在信息原文的后面。就像手寫的簽名一樣,具有不可抵賴性和簡潔性。

簡潔性:對信息原文做哈希運(yùn)算,得到消息摘要,信息越短加密的耗時(shí)越少。

不可抵賴性:信息擁有者要保證簽名的唯一性,必須是唯一能夠加密消息摘要的人,因此必須用私鑰加密 (就像字跡他人無法學(xué)會一樣),得到簽名。如果用公鑰,那每個(gè)人都可以偽造簽名了。

  • (4) 數(shù)字證書

問題起源:對1和3,發(fā)信者怎么知道從網(wǎng)上獲取的公鑰就是真的?沒有遭受中間人攻擊?

這樣就需要第三方機(jī)構(gòu)來保證公鑰的合法性,這個(gè)第三方機(jī)構(gòu)就是 CA (Certificate Authority),證書中心。

CA 用自己的私鑰對信息原文所有者發(fā)布的公鑰和相關(guān)信息進(jìn)行加密,得出的內(nèi)容就是數(shù)字證書。

信息原文的所有者以后發(fā)布信息時(shí),除了帶上自己的簽名,還帶上數(shù)字證書,就可以保證信息不被篡改了。信息的接收者先用 CA給的公鑰解出信息所有者的公鑰,這樣可以保證信息所有者的公鑰是真正的公鑰,然后就能通過該公鑰證明數(shù)字簽名是否真實(shí)了。

二、RSA算法

2.1 簡介

RSA 是目前最有影響力的公鑰加密算法,該算法基于一個(gè)十分簡單的數(shù)論事實(shí):將兩個(gè)大素?cái)?shù)相乘十分容易,但想要對其乘積進(jìn)行因式分解卻極其困難,因此可以將乘積公開作為加密密鑰,即公鑰,而兩個(gè)大素?cái)?shù)組合成私鑰。公鑰是可發(fā)布的供任何人使用,私鑰則為自己所有,供解密之用。

2.2 工作流程

A 要把信息發(fā)給 B 為例,確定角色:A 為加密者,B 為解密者。首先由 B 隨機(jī)確定一個(gè) KEY,稱之為私鑰,將這個(gè) KEY 始終保存在機(jī)器 B 中而不發(fā)出來;然后,由這個(gè) KEY 計(jì)算出另一個(gè) KEY,稱之為公鑰。這個(gè)公鑰的特性是幾乎不可能通過它自身計(jì)算出生成它的私鑰。接下來通過網(wǎng)絡(luò)把這個(gè)公鑰傳給 A,A 收到公鑰后,利用公鑰對信息加密,并把密文通過網(wǎng)絡(luò)發(fā)送到 B,最后 B 利用已知的私鑰,就能對密文進(jìn)行解碼了。以上就是 RSA 算法的工作流程。

2.3 運(yùn)算速度

由于進(jìn)行的都是大數(shù)計(jì)算,使得 RSA 最快的情況也比 DES 慢上好幾倍,無論是軟件還是硬件實(shí)現(xiàn)。速度一直是 RSA 的缺陷。一般來說只用于少量數(shù)據(jù)加密。RSA 的速度是對應(yīng)同樣安全級別的對稱密碼算法的1/1000左右。

比起 DES 和其它對稱算法來說,RSA 要慢得多。實(shí)際上一般使用一種對稱算法來加密信息,然后用 RSA 來加密比較短的公鑰,然后將用 RSA 加密的公鑰和用對稱算法加密的消息發(fā)送給接收方。

這樣一來對隨機(jī)數(shù)的要求就更高了,尤其對產(chǎn)生對稱密碼的要求非常高,否則的話可以越過 RSA 來直接攻擊對稱密碼。

2.4 公鑰傳遞安全

和其它加密過程一樣,對 RSA 來說分配公鑰的過程是非常重要的。分配公鑰的過程必須能夠抵擋中間人攻擊。假設(shè) A 交給 B 一個(gè)公鑰,并使 B 相信這是A 的公鑰,并且 C 可以截下 A 和 B 之間的信息傳遞,那么 C 可以將自己的公鑰傳給 B,B 以為這是 A 的公鑰。C 可以將所有 B 傳遞給 A 的消息截下來,將這個(gè)消息用自己的密鑰解密,讀這個(gè)消息,然后將這個(gè)消息再用 A 的公鑰加密后傳給 A。理論上 A 和 B 都不會發(fā)現(xiàn) C 在偷聽它們的消息,今天人們一般用數(shù)字認(rèn)證來防止這樣的攻擊。

2.5 攻擊

(1) 針對 RSA 最流行的攻擊一般是基于大數(shù)因數(shù)分解。1999年,RSA-155 (512 bits) 被成功分解,花了五個(gè)月時(shí)間(約8000 MIPS 年)和224 CPU hours 在一臺有3.2G 中央內(nèi)存的 Cray C916計(jì)算機(jī)上完成。

RSA-158 表示如下:

39505874583265144526419767800614481996020776460304936454139376051579355626529450683609727842468219535093544305870490251995655335710209799226484977949442955603= 3388495837466721394368393204672181522815830368604993048084925840555281177×  11658823406671259903148376558383270818131012258146392600439520994131344334162924536139

2009年12月12日,編號為 RSA-768 (768 bits, 232 digits) 數(shù)也被成功分解。這一事件威脅了現(xiàn)通行的1024-bit 密鑰的安全性,普遍認(rèn)為用戶應(yīng)盡快升級到2048-bit 或以上。

RSA-768表示如下:

1230186684530117755130494958384962720772853569595334792197322452151726400507263657518745202199786469389956474942774063845925192557326303453731548268507917026122142913461670429214311602221240479274737794080665351419597459856902143413= 3347807169895689878604416984821269081770479498371376856891  2431388982883793878002287614711652531743087737814467999489×  3674604366679959042824463379962795263227915816434308764267  6032283815739666511279233373417143396810270092798736308917

(2) 秀爾算法
量子計(jì)算里的秀爾算法能使窮舉的效率大大的提高。由于 RSA 算法是基于大數(shù)分解 (無法抵抗窮舉攻擊),因此在未來量子計(jì)算能對 RSA 算法構(gòu)成較大的威脅。一個(gè)擁有 N 量子位的量子計(jì)算機(jī),每次可進(jìn)行2^N 次運(yùn)算,理論上講,密鑰為1024位長的 RSA 算法,用一臺512量子比特位的量子計(jì)算機(jī)在1秒內(nèi)即可破解。

2.6 例子

  • 首先引入commons-codec
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.14</version>
</dependency>
  • 密鑰對對象
/**
 * @author: huangyibo
 * @Date: 2022/4/29 18:47
 * @Description: 非對稱加密 密鑰對對象
 */

public class RsaKeyPair {

    private String publicKey;

    private String privateKey;

    public RsaKeyPair(String publicKey, String privateKey) {
        this.publicKey = publicKey;
        this.privateKey = privateKey;
    }

    public String getPublicKey() {
        return publicKey;
    }

    public String getPrivateKey() {
        return privateKey;
    }
}
  • RSA工具類
import org.apache.commons.codec.binary.Base64;

import javax.crypto.Cipher;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

/**
 * @author: huangyibo
 * @Date: 2022/4/28 18:03
 * @Description: RSA 非對稱加密算法
 */

public class RSAUtil {

    //RSA編碼
    public static final String ALGORITHM = "RSA";

    /**
     * 默認(rèn)種子
     */
    private static final String DEFAULT_SEED = "0f22507a10bbddd07d8a3082122966e3";

    /**
     * 構(gòu)建RSA密鑰對
     * @return
     * @throws NoSuchAlgorithmException
     */
    public static RsaKeyPair generateKeyPair() throws NoSuchAlgorithmException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM);
        // 初始化隨機(jī)產(chǎn)生器
        SecureRandom secureRandom = new SecureRandom();
        secureRandom.setSeed(DEFAULT_SEED.getBytes());
        keyPairGenerator.initialize(1024, secureRandom);
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
        String publicKeyString = Base64.encodeBase64String(rsaPublicKey.getEncoded());
        String privateKeyString = Base64.encodeBase64String(rsaPrivateKey.getEncoded());
        return new RsaKeyPair(publicKeyString, privateKeyString);
    }


    /**
     * 公鑰解密
     *
     * @param publicKeyText 公鑰
     * @param text          加密字符串
     * @return              明文
     * @throws Exception    解密過程中的異常信息
     */
    public static String decryptByPublicKey(String publicKeyText, String text) throws Exception {
        X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyText));
        KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
        PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, publicKey);
        byte[] result = cipher.doFinal(Base64.decodeBase64(text));
        return new String(result);
    }

    /**
     * 私鑰加密
     *
     * @param privateKeyText    私鑰
     * @param text              加密數(shù)據(jù)
     * @return                  密文
     * @throws Exception        加密過程中的異常信息
     */
    public static String encryptByPrivateKey(String privateKeyText, String text) throws Exception {
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyText));
        KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
        PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, privateKey);
        byte[] result = cipher.doFinal(text.getBytes());
        return Base64.encodeBase64String(result);
    }


    /**
     * 私鑰解密
     *
     * @param privateKeyText    私鑰
     * @param text              加密字符串
     * @return                  明文
     * @throws Exception        解密過程中的異常信息
     */
    public static String decryptByPrivateKey(String privateKeyText, String text) throws Exception {
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec5 = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyText));
        KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
        PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec5);
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] result = cipher.doFinal(Base64.decodeBase64(text));
        return new String(result);
    }


    /**
     * 公鑰加密
     * @param publicKeyText     公鑰
     * @param text              加密字符串
     * @return                  密文
     * @throws Exception        加密過程中的異常信息
     */
    public static String encryptByPublicKey(String publicKeyText, String text) throws Exception {
        X509EncodedKeySpec x509EncodedKeySpec2 = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyText));
        KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
        PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec2);
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        byte[] result = cipher.doFinal(text.getBytes());
        return Base64.encodeBase64String(result);
    }
}
  • RSA測試
/**
 * @author: huangyibo
 * @Date: 2022/4/29 18:57
 * @Description:
 */

public class RSATest {

    private static final String src = "abcdefghijklmnopqrstuvwxyz";

    public static void main(String[] args) throws Exception {
        System.out.println("\n");
        RsaKeyPair rsaKeyPair = RSAUtil.generateKeyPair();
        System.out.println("公鑰:" + rsaKeyPair.getPublicKey());
        System.out.println("私鑰:" + rsaKeyPair.getPrivateKey());
        System.out.println("\n");
        test1(rsaKeyPair, src);
        System.out.println("\n");
        test2(rsaKeyPair, src);
        System.out.println("\n");
    }

    /**
     * 公鑰加密私鑰解密
     */
    private static void test1(RsaKeyPair keyPair, String source) throws Exception {
        System.out.println("***************** 公鑰加密私鑰解密開始 *****************");
        String text1 = RSAUtil.encryptByPublicKey(keyPair.getPublicKey(), source);
        String text2 = RSAUtil.decryptByPrivateKey(keyPair.getPrivateKey(), text1);
        System.out.println("加密前:" + source);
        System.out.println("加密后:" + text1);
        System.out.println("解密后:" + text2);
        if (source.equals(text2)) {
            System.out.println("解密字符串和原始字符串一致,解密成功");
        } else {
            System.out.println("解密字符串和原始字符串不一致,解密失敗");
        }
        System.out.println("***************** 公鑰加密私鑰解密結(jié)束 *****************");
    }

    /**
     * 私鑰加密公鑰解密
     *
     * @throws Exception
     */
    private static void test2(RsaKeyPair keyPair, String source) throws Exception {
        System.out.println("***************** 私鑰加密公鑰解密開始 *****************");
        String text1 = RSAUtil.encryptByPrivateKey(keyPair.getPrivateKey(), source);
        String text2 = RSAUtil.decryptByPublicKey(keyPair.getPublicKey(), text1);
        System.out.println("加密前:" + source);
        System.out.println("加密后:" + text1);
        System.out.println("解密后:" + text2);
        if (source.equals(text2)) {
            System.out.println("解密字符串和原始字符串一致,解密成功");
        } else {
            System.out.println("解密字符串和原始字符串不一致,解密失敗");
        }
        System.out.println("***************** 私鑰加密公鑰解密結(jié)束 *****************");
    }
}

三、DSA算法

3.1 簡介

DSA (Digital Signature Algorithm) 是 Schnorr 和 ElGamal 簽名算法的變種,被美國 NIST 作為 DSS (DigitalSignature Standard)。 DSA 是基于整數(shù)有限域離散對數(shù)難題的。

簡單的說,這是一種更高級的驗(yàn)證方式,用作數(shù)字簽名。不單單只有公鑰、私鑰,還有數(shù)字簽名。私鑰加密生成數(shù)字簽名,公鑰驗(yàn)證數(shù)據(jù)及簽名,如果數(shù)據(jù)和簽名不匹配則認(rèn)為驗(yàn)證失敗。數(shù)字簽名的作用就是校驗(yàn)數(shù)據(jù)在傳輸過程中不被修改,數(shù)字簽名,是單向加密的升級。

3.2 處理過程

  • (1) 使用消息摘要算法將發(fā)送數(shù)據(jù)加密生成數(shù)字摘要。
  • (2) 發(fā)送方用自己的私鑰對摘要再加密,形成數(shù)字簽名。
  • (3) 將原文和加密的摘要同時(shí)傳給對方。
  • (4) 接受方用發(fā)送方的公鑰對摘要解密,同時(shí)對收到的數(shù)據(jù)用消息摘要算法產(chǎn)生同一摘要。
  • (5) 將解密后的摘要和收到的數(shù)據(jù)在接收方重新加密產(chǎn)生的摘要相互對比,如果兩者一致,則說明在傳送過程中信息沒有破壞和篡改。否則,則說明信息已經(jīng)失去安全性和保密性。

3.3 例子

  • 非對稱加密算法基類
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

/**
 * @author: huangyibo
 * @Date: 2022/4/28 18:03
 * @Description: 非對稱加密算法基類
 */

public class EncryptAlgorithmBase {

    /**
     * 默認(rèn)種子
     */
    private static final String DEFAULT_SEED = "0f22507a10bbddd07d8a3082122966e3";


    /**
     * 生成密鑰實(shí)際方法,可以使用多種方式
     * 提供以下多種方式
     * { "DSA", "SHA1withDSA", "1024" }, { "DSA", "SHA256withDSA", "1024" },
     * { "DSA", "SHA256withDSA", "2048" }, { "RSA", "SHA256withRSA", "1024" },
     * { "RSA", "SHA256withRSA", "2048" }, { "RSA", "SHA256withRSA", "3192" },
     * { "RSA", "SHA512withRSA", "1024" }, { "RSA", "SHA512withRSA", "2048" },
     * { "RSA", "SHA512withRSA", "3192" }, { "RSA", "MD5withRSA", "1024" },
     * { "RSA", "MD5withRSA", "2048" },
     * { "RSA", "MD5withRSA", "3192" }, { "EC", "SHA1withECDSA", "128" },
     * { "EC", "SHA1withECDSA", "256" },
     * { "EC", "SHA256withECDSA", "128" }, { "EC", "SHA256withECDSA", "256" },
     * { "EC", "SHA512withECDSA", "128" }, { "EC", "SHA512withECDSA", "256" },
     *
     * @param algorithm
     * @param bit
     * @return
     * @throws Exception
     */
    protected static RsaKeyPair generateKeyPair(String algorithm, int bit) throws Exception {
        //KeyPairGenerator類用于生成公鑰和私鑰對,基于RSA算法生成對象
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
        //初始化密鑰對生成器,密鑰大小為96-1024位
        keyPairGenerator.initialize(bit, new SecureRandom());
        // 生成一個(gè)密鑰對,保存在keyPair中
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        String publicKey = ByteUtils.bytesToHexString(keyPair.getPublic().getEncoded());
        String privateKey = ByteUtils.bytesToHexString(keyPair.getPrivate().getEncoded());
        return new RsaKeyPair(publicKey, privateKey);
    }


    /**
     * 非對稱加密簽名 - 私鑰加密
     * @param str
     * @param privateKey
     * @param algorithm
     * @param signAlgorithm
     * @return
     * @throws Exception
     */
    public static String sign(String str, String privateKey, String algorithm, String signAlgorithm) throws Exception {
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(ByteUtils.hexStringToBytes(privateKey));
        KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
        PrivateKey dsaPrivateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
        Signature signature = Signature.getInstance(signAlgorithm);
        signature.initSign(dsaPrivateKey);
        signature.update(str.getBytes());
        return ByteUtils.bytesToHexString(signature.sign());
    }


    /**
     * 非對稱加密驗(yàn)證 - 公鑰驗(yàn)證
     * @param sign
     * @param str
     * @param publicKey
     * @param algorithm
     * @param signAlgorithm
     * @return
     * @throws Exception
     */
    public static boolean verify(String sign, String str, String publicKey,String algorithm,String signAlgorithm) throws Exception {
        //base64編碼的公鑰
        X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(ByteUtils.hexStringToBytes(publicKey));
        KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
        PublicKey dsaPublicKey = keyFactory.generatePublic(x509EncodedKeySpec);
        Signature signature = Signature.getInstance(signAlgorithm);
        signature.initVerify(dsaPublicKey);
        signature.update(str.getBytes());
        return signature.verify(ByteUtils.hexStringToBytes(sign));
    }
}
  • 對稱加密 密鑰對對象
/**
 * @author: huangyibo
 * @Date: 2022/4/29 18:47
 * @Description: 非對稱加密 密鑰對對象
 */

public class RsaKeyPair {

    private String publicKey;

    private String privateKey;

    public RsaKeyPair(String publicKey, String privateKey) {
        this.publicKey = publicKey;
        this.privateKey = privateKey;
    }

    public String getPublicKey() {
        return publicKey;
    }

    public String getPrivateKey() {
        return privateKey;
    }
}
  • DSA 加解密實(shí)現(xiàn)
/**
 * @author: huangyibo
 * @Date: 2022/4/28 18:52
 * @Description:  DSA 加解密實(shí)現(xiàn)
 */

public class DSAUtils extends EncryptAlgorithmBase {

    //字符編碼
    public static final String ALGORITHM = "DSA";

    public static final String SIGN_ALGORITHM = "SHA1withDSA";


    /**
     * DSA 驗(yàn)簽
     *
     * @param str       加密字符串
     * @param publicKey 公鑰
     * @return 密文
     * @throws Exception 加密過程中的異常信息
     */
    public static boolean verify(String sign, String str, String publicKey) throws Exception {
        return verify(sign, str, publicKey, ALGORITHM, SIGN_ALGORITHM);
    }


    /**
     * DSA 簽名
     *
     * @param str        加密字符串
     * @param privateKey 私鑰
     * @return 銘文
     * @throws Exception 解密過程中的異常信息
     */
    public static String sign(String str, String privateKey) throws Exception {
        return sign(str, privateKey, ALGORITHM, SIGN_ALGORITHM);
    }


    public static void main(String[] args) throws Exception {
        RsaKeyPair rsaKeyPair = generateKeyPair(ALGORITHM, 1024);
        System.out.println(rsaKeyPair.getPublicKey());
        System.out.println(rsaKeyPair.getPrivateKey());
        String message = "我要測試DSA";
        String sign = sign(message, rsaKeyPair.getPrivateKey());
        System.out.println(verify(sign, message, rsaKeyPair.getPublicKey()));
    }
}
  • RSA 加解密實(shí)現(xiàn)
/**
 * @author: huangyibo
 * @Date: 2022/4/28 18:52
 * @Description:  RSA 加解密實(shí)現(xiàn)
 */

public class RSAUtils extends EncryptAlgorithmBase {

    //字符編碼
    public static final String ALGORITHM = "RSA";

    public static final String SIGN_ALGORITHM = "SHA512withRSA";


    /**
     * RSA 驗(yàn)簽
     *
     * @param str       加密字符串
     * @param publicKey 公鑰
     * @return 密文
     * @throws Exception 加密過程中的異常信息
     */
    public static boolean verify(String sign, String str, String publicKey) throws Exception {
        return verify(sign, str, publicKey, ALGORITHM, SIGN_ALGORITHM);
    }


    /**
     * RSA 簽名
     *
     * @param str        加密字符串
     * @param privateKey 私鑰
     * @return 銘文
     * @throws Exception 解密過程中的異常信息
     */
    public static String sign(String str, String privateKey) throws Exception {
        return sign(str, privateKey, ALGORITHM, SIGN_ALGORITHM);
    }


    public static void main(String[] args) throws Exception {
        RsaKeyPair rsaKeyPair = generateKeyPair(ALGORITHM, 1024);
        System.out.println(rsaKeyPair.getPublicKey());
        System.out.println(rsaKeyPair.getPrivateKey());
        String message = "我要測試DSA";
        String sign = sign(message, rsaKeyPair.getPrivateKey());
        System.out.println(verify(sign, message, rsaKeyPair.getPublicKey()));
    }
}
  • ECDSA簽名算法
/**
 * @author: huangyibo
 * @Date: 2022/4/28 18:52
 * @Description:  ECDSA簽名算法
 */

public class ECDSAUtils extends EncryptAlgorithmBase {

    //字符編碼
    public static final String ALGORITHM = "EC";

    public static final String SIGN_ALGORITHM = "SHA256withECDSA";


    /**
     * ECDSA 驗(yàn)簽
     *
     * @param str       加密字符串
     * @param publicKey 公鑰
     * @return 密文
     * @throws Exception 加密過程中的異常信息
     */
    public static boolean verify(String sign, String str, String publicKey) throws Exception {
        return verify(sign, str, publicKey, ALGORITHM, SIGN_ALGORITHM);
    }


    /**
     * ECDSA 簽名
     *
     * @param str        加密字符串
     * @param privateKey 私鑰
     * @return 銘文
     * @throws Exception 解密過程中的異常信息
     */
    public static String sign(String str, String privateKey) throws Exception {
        return sign(str, privateKey, ALGORITHM, SIGN_ALGORITHM);
    }


    public static void main(String[] args) throws Exception {
        RsaKeyPair rsaKeyPair = generateKeyPair(ALGORITHM, 256);
        System.out.println(rsaKeyPair.getPublicKey());
        System.out.println(rsaKeyPair.getPrivateKey());
        String message = "我要測試DSA";
        String sign = sign(message, rsaKeyPair.getPrivateKey());
        System.out.println(verify(sign, message, rsaKeyPair.getPublicKey()));
    }
}
  • 基本數(shù)據(jù)類型轉(zhuǎn)換
/**
 * @author: huangyibo
 * @Date: 2022/4/28 18:47
 * @Description: 基本數(shù)據(jù)類型轉(zhuǎn)換(主要是byte和其它類型之間的互轉(zhuǎn))
 */

public class ByteUtils {

    /**
     *
     * <pre>
     * 將4個(gè)byte數(shù)字組成的數(shù)組合并為一個(gè)float數(shù).
     * </pre>
     *
     * @param arr
     * @return
     */
    public static float byte4ToFloat(byte[] arr) {
        if (arr == null || arr.length != 4) {
            throw new IllegalArgumentException("byte數(shù)組必須不為空,并且是4位!");
        }
        int i = byte4ToInt(arr);
        return Float.intBitsToFloat(i);
    }

    /**
     *
     * <pre>
     * 將一個(gè)float數(shù)字轉(zhuǎn)換為4個(gè)byte數(shù)字組成的數(shù)組.
     * </pre>
     *
     * @param f
     * @return
     */
    public static byte[] floatToByte4(float f) {
        int i = Float.floatToIntBits(f);
        return intToByte4(i);
    }

    /**
     *
     * <pre>
     * 將八個(gè)byte數(shù)字組成的數(shù)組轉(zhuǎn)換為一個(gè)double數(shù)字.
     * </pre>
     *
     * @param arr
     * @return
     */
    public static double byte8ToDouble(byte[] arr) {
        if (arr == null || arr.length != 8) {
            throw new IllegalArgumentException("byte數(shù)組必須不為空,并且是8位!");
        }
        long l = byte8ToLong(arr);
        return Double.longBitsToDouble(l);
    }

    /**
     *
     * <pre>
     * 將一個(gè)double數(shù)字轉(zhuǎn)換為8個(gè)byte數(shù)字組成的數(shù)組.
     * </pre>
     *
     * @param i
     * @return
     */
    public static byte[] doubleToByte8(double i) {
        long j = Double.doubleToLongBits(i);
        return longToByte8(j);
    }

    /**
     *
     * <pre>
     * 將一個(gè)char字符轉(zhuǎn)換為兩個(gè)byte數(shù)字轉(zhuǎn)換為的數(shù)組.
     * </pre>
     *
     * @param c
     * @return
     */
    public static byte[] charToByte2(char c) {
        byte[] arr = new byte[2];
        arr[0] = (byte) (c >> 8);
        arr[1] = (byte) (c & 0xff);
        return arr;
    }

    /**
     *
     * <pre>
     * 將2個(gè)byte數(shù)字組成的數(shù)組轉(zhuǎn)換為一個(gè)char字符.
     * </pre>
     *
     * @param arr
     * @return
     */
    public static char byte2ToChar(byte[] arr) {
        if (arr == null || arr.length != 2) {
            throw new IllegalArgumentException("byte數(shù)組必須不為空,并且是2位!");
        }
        return (char) (((char) (arr[0] << 8)) | ((char) arr[1]));
    }

    /**
     *
     * <pre>
     * 將一個(gè)16位的short轉(zhuǎn)換為長度為2的8位byte數(shù)組.
     * </pre>
     *
     * @param s
     * @return
     */
    public static byte[] shortToByte2(Short s) {
        byte[] arr = new byte[2];
        arr[0] = (byte) (s >> 8);
        arr[1] = (byte) (s & 0xff);
        return arr;
    }

    /**
     *
     * <pre>
     * 長度為2的8位byte數(shù)組轉(zhuǎn)換為一個(gè)16位short數(shù)字.
     * </pre>
     *
     * @param arr
     * @return
     */
    public static short byte2ToShort(byte[] arr) {
        if (arr != null && arr.length != 2) {
            throw new IllegalArgumentException("byte數(shù)組必須不為空,并且是2位!");
        }
        return (short) (((short) arr[0] << 8) | ((short) arr[1] & 0xff));
    }

    /**
     *
     * <pre>
     * 將short轉(zhuǎn)換為長度為16的byte數(shù)組.
     * 實(shí)際上每個(gè)8位byte只存儲了一個(gè)0或1的數(shù)字
     * 比較浪費(fèi).
     * </pre>
     *
     * @param s
     * @return
     */
    public static byte[] shortToByte16(short s) {
        byte[] arr = new byte[16];
        for (int i = 15; i >= 0; i--) {
            arr[i] = (byte) (s & 1);
            s >>= 1;
        }
        return arr;
    }

    public static short byte16ToShort(byte[] arr) {
        if (arr == null || arr.length != 16) {
            throw new IllegalArgumentException("byte數(shù)組必須不為空,并且長度為16!");
        }
        short sum = 0;
        for (int i = 0; i < 16; ++i) {
            sum |= (arr[i] << (15 - i));
        }
        return sum;
    }

    /**
     *
     * <pre>
     * 將32位int轉(zhuǎn)換為由四個(gè)8位byte數(shù)字.
     * </pre>
     *
     * @param sum
     * @return
     */
    public static byte[] intToByte4(int sum) {
        byte[] arr = new byte[4];
        arr[0] = (byte) (sum >> 24);
        arr[1] = (byte) (sum >> 16);
        arr[2] = (byte) (sum >> 8);
        arr[3] = (byte) (sum & 0xff);
        return arr;
    }

    /**
     * <pre>
     * 將長度為4的8位byte數(shù)組轉(zhuǎn)換為32位int.
     * </pre>
     *
     * @param arr
     * @return
     */
    public static int byte4ToInt(byte[] arr) {
        if (arr == null || arr.length != 4) {
            throw new IllegalArgumentException("byte數(shù)組必須不為空,并且是4位!");
        }
        return (int) (((arr[0] & 0xff) << 24) | ((arr[1] & 0xff) << 16) | ((arr[2] & 0xff) << 8) | ((arr[3] & 0xff)));
    }

    /**
     *
     * <pre>
     * 將長度為8的8位byte數(shù)組轉(zhuǎn)換為64位long.
     * </pre>
     *
     * 0xff對應(yīng)16進(jìn)制,f代表1111,0xff剛好是8位 byte[]
     * arr,byte[i]&0xff剛好滿足一位byte計(jì)算,不會導(dǎo)致數(shù)據(jù)丟失. 如果是int計(jì)算. int[] arr,arr[i]&0xffff
     *
     * @param arr
     * @return
     */
    public static long byte8ToLong(byte[] arr) {
        if (arr == null || arr.length != 8) {
            throw new IllegalArgumentException("byte數(shù)組必須不為空,并且是8位!");
        }
        return (long) (((long) (arr[0] & 0xff) << 56) | ((long) (arr[1] & 0xff) << 48) | ((long) (arr[2] & 0xff) << 40)
                | ((long) (arr[3] & 0xff) << 32) | ((long) (arr[4] & 0xff) << 24)
                | ((long) (arr[5] & 0xff) << 16) | ((long) (arr[6] & 0xff) << 8) | ((long) (arr[7] & 0xff)));
    }

    /**
     * 將一個(gè)long數(shù)字轉(zhuǎn)換為8個(gè)byte數(shù)組組成的數(shù)組.
     */
    public static byte[] longToByte8(long sum) {
        byte[] arr = new byte[8];
        arr[0] = (byte) (sum >> 56);
        arr[1] = (byte) (sum >> 48);
        arr[2] = (byte) (sum >> 40);
        arr[3] = (byte) (sum >> 32);
        arr[4] = (byte) (sum >> 24);
        arr[5] = (byte) (sum >> 16);
        arr[6] = (byte) (sum >> 8);
        arr[7] = (byte) (sum & 0xff);
        return arr;
    }

    /**
     *
     * <pre>
     * 將int轉(zhuǎn)換為32位byte.
     * 實(shí)際上每個(gè)8位byte只存儲了一個(gè)0或1的數(shù)字
     * 比較浪費(fèi).
     * </pre>
     *
     * @param num
     * @return
     */
    public static byte[] intToByte32(int num) {
        byte[] arr = new byte[32];
        for (int i = 31; i >= 0; i--) {
            // &1 也可以改為num&0x01,表示取最地位數(shù)字.
            arr[i] = (byte) (num & 1);
            // 右移一位.
            num >>= 1;
        }
        return arr;
    }

    /**
     *
     * <pre>
     * 將長度為32的byte數(shù)組轉(zhuǎn)換為一個(gè)int類型值.
     * 每一個(gè)8位byte都只存儲了0或1的數(shù)字.
     * </pre>
     *
     * @param arr
     * @return
     */
    public static int byte32ToInt(byte[] arr) {
        if (arr == null || arr.length != 32) {
            throw new IllegalArgumentException("byte數(shù)組必須不為空,并且長度是32!");
        }
        int sum = 0;
        for (int i = 0; i < 32; ++i) {
            sum |= (arr[i] << (31 - i));
        }
        return sum;
    }

    /**
     *
     * <pre>
     * 將長度為64的byte數(shù)組轉(zhuǎn)換為一個(gè)long類型值.
     * 每一個(gè)8位byte都只存儲了0或1的數(shù)字.
     * </pre>
     *
     * @param arr
     * @return
     */
    public static long byte64ToLong(byte[] arr) {
        if (arr == null || arr.length != 64) {
            throw new IllegalArgumentException("byte數(shù)組必須不為空,并且長度是64!");
        }
        long sum = 0L;
        for (int i = 0; i < 64; ++i) {
            sum |= ((long) arr[i] << (63 - i));
        }
        return sum;
    }

    /**
     *
     * <pre>
     * 將一個(gè)long值轉(zhuǎn)換為長度為64的8位byte數(shù)組.
     * 每一個(gè)8位byte都只存儲了0或1的數(shù)字.
     * </pre>
     *
     * @param sum
     * @return
     */
    public static byte[] longToByte64(long sum) {
        byte[] arr = new byte[64];
        for (int i = 63; i >= 0; i--) {
            arr[i] = (byte) (sum & 1);
            sum >>= 1;
        }
        return arr;
    }

    /**
     * <pre>
     * 把byte[]轉(zhuǎn)換成16進(jìn)制進(jìn)制字符串
     * </pre>
     * @param src
     * @return
     */
    public static String bytesToHexString(byte[] src){
        StringBuilder stringBuilder = new StringBuilder("");
        if (src == null || src.length <= 0) {
            return null;
        }
        for (int i = 0; i < src.length; i++) {
            int v = src[i] & 0xFF;
            String hv = Integer.toHexString(v);
            if (hv.length() < 2) {
                stringBuilder.append(0);
            }
            stringBuilder.append(hv);
        }
        return stringBuilder.toString();
    }

    /**
     * <pre>
     * 把16進(jìn)制進(jìn)制字符串轉(zhuǎn)換成byte[]
     * </pre>
     * @param hexString
     * @return
     */
    public static byte[] hexStringToBytes(String hexString) {
        if (hexString == null || hexString.equals("")) {
            return null;
        }
        int length = hexString.length() / 2;
        char[] hexChars = hexString.toCharArray();
        byte[] d = new byte[length];
        for (int i = 0; i < length; i++) {
            int pos = i * 2;
            d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));
        }
        return d;
    }


    /**
     * 將16進(jìn)制字符轉(zhuǎn)換為字節(jié)
     * @param c
     * @return
     */
    private static byte charToByte(char c) {
        return (byte) "0123456789abcdef".indexOf(c);
    }

    /**
     * byte[]轉(zhuǎn)換成bit
     * @param bytes
     * @return
     */
    public static String bytesToBits(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for(byte b:bytes){
            sb.append(byteToBits(b));
        }
        return sb.toString();
    }

    /**
     * byte轉(zhuǎn)換成8位bit
     * @param b
     * @return
     */
    public static String byteToBits(byte b) {
        int z = b; z |= 256;
        String str = Integer.toBinaryString(z);
        int len = str.length();
        return str.substring(len-8, len);
    }

    /**
     * 計(jì)算校驗(yàn)和
     * @param bytes
     * @return
     */
    public static final int calculateCheckSum(byte[] bytes) {
        int sum = 0;
        for (byte b : bytes) {
            sum += (short)b;
        }
        return sum > 65535 ? (sum-65535) : sum;
    }
}

/**
 * @author: huangyibo
 * @Date: 2022/4/28 18:20
 * @Description: GZip 轉(zhuǎn)化的工具類
 */

public class GZipUtils {

    public final static int GZIP_MAGIC = 0x8b1f;

    private static final Logger logger = LoggerFactory.getLogger(GZipUtils.class);

    /**
     * 從輸入流中構(gòu)建消息
     * @param is 輸入流
     * @throws IOException 讀流失敗則拋出異常
     */
    public static byte[]  buildMsg(InputStream is) throws  Exception {
        byte[] messages=null;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        byte[] receiveBuf = new byte[1024];
        int len=0;
        while ((len = is.read(receiveBuf)) != -1) {
            try {
                baos.write(receiveBuf, 0, len);
            } catch (Exception e) {
                logger.error("Fail to do message check sum!", e);
                throw e;
            }
        }
        if (baos.size() == 0) { // 判斷是否有消息數(shù)??
            return null;
        }
        messages = baos.toByteArray();
        try {
            baos.close();
        } catch (IOException e) {
            logger.error("Fail to close byte array output stream!", e);
            throw e;
        }
        if(GZipUtils.checkIfGzip(messages)) {
            try {
                return GZipUtils.unGZip(messages);
            } catch (Exception e) {
                return messages;
            }
        }
        return messages;
    }

    /**
     * gZip解壓方法
     *
     * @throws Exception
     */
    public static byte[] unGZip(byte[] data) throws Exception {
        byte[] b = null;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        GZIPInputStream gzip =null;
        ByteArrayInputStream bis =null;
        try {
            bis = new ByteArrayInputStream(data);
            gzip = new GZIPInputStream(bis);
            byte[] buf = new byte[1024 * 1024];
            int num = -1;
            while ((num = gzip.read(buf, 0, buf.length)) != -1) {
                baos.write(buf, 0, num);
            }
            b = baos.toByteArray();
        } catch (Throwable ex) {
            throw new Exception("UNGzip the byte [] error,please check the data format", ex);
        }finally {
            closeStream(baos);
            closeStream(gzip);
            closeStream(bis);
        }
        return b;
    }

    /**
     * 關(guān)閉流
     * @param clo
     */
    private static void closeStream(Closeable clo) {
        if(null!=clo) {
            try {
                clo.close();
            } catch (Exception e) {
                logger.error("Fail to close Stream!", e);
            }
        }
    }

    /**
     * 數(shù)據(jù)壓縮
     *
     * @param is    輸入流
     * @param os    輸出流
     * @throws Exception
     */
    public static void compress(InputStream is, OutputStream os) throws Exception {
        GZIPOutputStream gos = new GZIPOutputStream(os);
        int count;
        byte[] data = new byte[1024];
        while ((count = is.read(data, 0, 1024)) != -1) {
            gos.write(data, 0, count);
        }
        gos.finish();
        gos.flush();
        gos.close();
    }

    /**
     * 數(shù)據(jù)壓縮
     *
     * @param data
     * @return
     * @throws Exception
     */
    public static byte[] compress(byte[] data) throws Exception {
        ByteArrayInputStream bais = new ByteArrayInputStream(data);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 壓縮
        compress(bais, baos);
        byte[] output = baos.toByteArray();
        baos.flush();
        baos.close();
        bais.close();
        return output;
    }

    private static int readUByte(byte by) {
        int b = by  & 0xFF;
        if (b <= -1 || b > 255) {
            return -1;
        }
        return b;
    }

    public static boolean checkIfGzip(byte[] bytes){
        if(null==bytes || bytes.length<3) {
            return false;
        }
        if(readUByte(bytes[2])!=8){
            return false;
        }
        int b =readUByte(bytes[0] );
        int b2 =readUByte(bytes[1] );
        if (b == -1 || b2 == -1) {
            return false;
        }
        int res=(b2<< 8) | b;
        if(res!= GZIP_MAGIC){
            return false;
        }
        return true;
    }


    private static int readUByte(InputStream in) throws IOException {
        int b = in.read();
        if (b == -1) {
            throw new EOFException();
        }
        if (b < -1 || b > 255) {
            throw new IOException( ".read() returned value out of range -1..255: " + b);
        }
        System.out.println(b);
        return b;
    }
    private static int readUShort(InputStream in) throws IOException {
        int b = readUByte(in);
        return (readUByte(in) << 8) | b;
    }
}

RSA、DSA總結(jié):

非對稱性加密還有很多,RSA和DSA是比較常用和常見的加密方式,安全性來講兩者差不多,DSA只是一種算法,和RSA不同之處在于它不能用作加密和解密,也不能進(jìn)行密鑰交換,只用于簽名,它比RSA要快很多,RSA啥都好,但是RSA算法的秘鑰很長,加密的計(jì)算量比較大,安全性較高,但是加密速度比較慢,所以RSA加密常用于少量的核心數(shù)據(jù)的加密。

四、ECC算法

4.1 簡介

橢圓加密算法(ECC)是一種公鑰加密算法,最初由 Koblitz 和 Miller 兩人于1985年提出,其數(shù)學(xué)基礎(chǔ)是利用橢圓曲線上的有理點(diǎn)構(gòu)成 Abel 加法群上橢圓離散對數(shù)的計(jì)算困難性。公鑰密碼體制根據(jù)其所依據(jù)的難題一般分為三類:大整數(shù)分解問題類、離散對數(shù)問題類、橢圓曲線類。有時(shí)也把橢圓曲線類歸為離散對數(shù)類。

ECC 的主要優(yōu)勢是在某些情況下它比其他的方法使用更小的密鑰 (比如 RSA),提供相當(dāng)?shù)幕蚋叩燃壍陌踩CC 的另一個(gè)優(yōu)勢是可以定義群之間的雙線性映射,基于 Weil 對或是 Tate 對;雙線性映射已經(jīng)在密碼學(xué)中發(fā)現(xiàn)了大量的應(yīng)用,例如基于身份的加密。不過一個(gè)缺點(diǎn)是加密和解密操作的實(shí)現(xiàn)比其他機(jī)制花費(fèi)的時(shí)間長。

ECC 被廣泛認(rèn)為是在給定密鑰長度的情況下,最強(qiáng)大的非對稱算法,因此在對帶寬要求十分緊的連接中會十分有用。

比特幣錢包公鑰的生成使用了橢圓曲線算法,通過橢圓曲線乘法可以從私鑰計(jì)算得到公鑰, 這是不可逆轉(zhuǎn)的過程。

4.2 優(yōu)勢

  • (1) 安全性高,有研究表示160位的橢圓密鑰與1024位的 RSA 密鑰安全性相同。

  • (2) 處理速度快,在私鑰的加密解密速度上,ECC 算法比 RSA、DSA 速度更快,存儲空間占用小,帶寬要求低。

4.3 例子

https://github.com/esxgx/easy-ecc

Java 中 Chipher、Signature、KeyPairGenerator、KeyAgreement、SecretKey 均不支持 ECC 算法。

4.4 ECC算法詳解:

http://www.itdecent.cn/p/58c1750c6f22

五、DH算法

5.1 簡介

DH,全稱為"Diffie-Hellman",它是一種確保共享 KEY 安全穿越不安全網(wǎng)絡(luò)的方法,也就是常說的密鑰一致協(xié)議。由公開密鑰密碼體制的奠基人 Diffie 和 Hellman 所提出的一種思想。簡單的說就是允許兩名用戶在公開媒體上交換信息以生成"一致"的、可以共享的密鑰。也就是由甲方產(chǎn)出一對密鑰 (公鑰、私鑰),乙方依照甲方公鑰產(chǎn)生乙方密鑰對 (公鑰、私鑰)。

以此為基線,作為數(shù)據(jù)傳輸保密基礎(chǔ),同時(shí)雙方使用同一種對稱加密算法構(gòu)建本地密鑰 (SecretKey) 對數(shù)據(jù)加密。這樣,在互通了本地密鑰 (SecretKey) 算法后,甲乙雙方公開自己的公鑰,使用對方的公鑰和剛才產(chǎn)生的私鑰加密數(shù)據(jù),同時(shí)可以使用對方的公鑰和自己的私鑰對數(shù)據(jù)解密。不單單是甲乙雙方兩方,可以擴(kuò)展為多方共享數(shù)據(jù)通訊,這樣就完成了網(wǎng)絡(luò)交互數(shù)據(jù)的安全通訊。

  • 5.2 例子

具體例子可以移步到這篇文章:非對稱密碼之DH密鑰交換算法

參考:
https://blog.csdn.net/u014294681/article/details/86705999

https://www.cnblogs.com/wangzxblog/p/13667634.html

https://www.cnblogs.com/taoxw/p/15837729.html

https://www.cnblogs.com/fangfan/p/4086662.html

https://www.cnblogs.com/utank/p/7877761.html

https://blog.csdn.net/m0_59133441/article/details/122686815

https://www.cnblogs.com/muliu/p/10875633.html

https://www.cnblogs.com/wf-zhang/p/14923279.html

http://www.itdecent.cn/p/7a927db713e4

https://blog.csdn.net/ljx1400052550/article/details/79587133

https://blog.csdn.net/yuanjian0814/article/details/109815473

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容