【网络】加解密算法和示例

本文详细介绍了Java加密解密体系,涵盖三类引擎:加密引擎、生成器或转换器、证书或密钥对象。解析了主要算法和服务,如SecureRandom、MessageDigest、Cipher、MAC、KeyFactory等。并深入探讨了JCA使用Provider提供算法服务的方式,以及Key和KeySpec的概念和转换方法。同时,提供了RSA、AES、DES等加密算法的Java代码示例。

背景

本博客主要参考:

加密和编码的主要区别:编码是使用一种的算法对数据进行转换,编码后的数据只需要使用相同的算法发即可得到数据;加密是使用一种算法和密钥(key)对数据进行转换,加密后的数据仅知道加密算法是无法解密的,还需要知道密钥。

Java加密解密体系

Java Security Infrastructure

在这里插入图片描述
三类引擎:

  • 加密引擎,提供加密,解密,消息摘要服务
  • 生成器或转换器
  • 证书或密钥对象

主要算法和服务:

  • 生成随机数:SecureRandom
  • 消息摘要(hash):Message Digest
  • 签名:Signature, 使用密钥初始化,用于签名或验证数据
  • 加密工具:Cipher,使用密钥初始化,用于加密和解密数据
  • 消息认证编码:MAC,与MessageDigest类似,但是需要使用密钥初始化以保证消息的完整性
  • 密钥转换:KeyFactory, 将通用类型的Key转换成算法相关类型key,比如RsaKeySpec
  • 私钥转换(用于对称加密):SecretKeyFactory,将SecretKey转换为相关类型key(KeyFactory的子类)
  • 密钥对生成:KeyPairGenerator
  • 密钥生成:KeyGenerator
  • 密钥交换:KeyAgreement
  • 密钥和证书管理:KeyStore
  • 证书生成:CertificateFactory
  • 证书链:CertPathBuilder
  • 证书链校验:CertPathValidator
  • 证书仓库:CertStore,用于获取证书和CRL
    注:Genarator生成全新的数据,Factory基于已有数据创建数据
    在这里插入图片描述

JCA使用Provider来提供算法服务,程序中可以使用对应Provider的getInstance(算法名,[提供者名])获取服务,如下

    md = MessageDigest.getInstance("SHA-256");
    md = MessageDigest.getInstance("SHA-256", "ProviderC");

如果没有指定提供者名称,则会搜索所有的provider,直到返回第一个。

Key和KeySpec

Key的子类:PublicKey, PrivateKey, SecretKey(对称加密的key)
Key和KeySpec是密钥数据的两种表现形式。Cipher使用Key来初始化算法。
Key的数据是不透明的,但是KeySpec包含特定的get方法,能够获取key包含的参数。
转换:

PublicKey KeyFactory.generatePublic(KeySpec keySpec)
PrivateKey KeyFactory.generatePrivate(KeySpec keySpec)
KeySpec KeyFactory.getKeySpec(Key key, Class keySpec)

例子:

DSAPrivateKeySpec dsaPrivKeySpec = new DSAPrivateKeySpec(x, p, q, g);

KeyFactory keyFactory = KeyFactory.getInstance("DSA");
PrivateKey privKey = keyFactory.generatePrivate(dsaPrivKeySpec);

Signature sig = Signature.getInstance("SHA256withDSA");
sig.initSign(privKey);
sig.update(someData);
byte[] signature = sig.sign();

验证:

    // encodedPubKey是byte[],通过PublicKey.getEncoded()
    X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(encodedPubKey);

    KeyFactory keyFactory = KeyFactory.getInstance("DSA");
    PublicKey pubKey = keyFactory.generatePublic(pubKeySpec);

    Signature sig = Signature.getInstance("SHA256withDSA");
    sig.initVerify(pubKey);
    sig.update(data);
    sig.verify(signature);

SecretKey

创建SecretKey的两种方法:
1.使用Factory

    // Note the following bytes are not realistic secret key data
    // bytes but are simply supplied as an illustration of using data
    // bytes (key material) you already have to build a DESedeKeySpec.

    byte[] desEdeKeyData = getKeyData();
    DESedeKeySpec desEdeKeySpec = new DESedeKeySpec(desEdeKeyData);
    SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DESede");
    SecretKey secretKey = keyFactory.generateSecret(desEdeKeySpec);

2.SecretKeySpec
SecretKey只能从KeySpec生成

    byte[] aesKeyData = getKeyData();
    SecretKeySpec secretKey = new SecretKeySpec(aesKeyData, "AES");

KeyPairGenerator

KeyPairGenerator可生成PublicKey和PrivateKey
Key的生成与具体算法有关,所有算法都共享的参数:keysize和random
其他算法参数:AlgorithmParameterSpec

void initialize(int keysize, SecureRandom random)
void initialize(int keysize) // 使用系统提供的random
void initialize(AlgorithmParameterSpec params,
                SecureRandom random)

void initialize(AlgorithmParameterSpec params)

在这里插入图片描述

KeyGenerator

KeyGenerator可生成SecretKey,初始化方法与KeyPairGenerator相同

KeyAgreement

参与方使用init初始化私钥,并将公钥发送给对方,使用doPhase生成最终的密钥
在这里插入图片描述
初始化方法

    public void init(Key key);

    public void init(Key key, SecureRandom random);

    public void init(Key key, AlgorithmParameterSpec params);

    public void init(Key key, AlgorithmParameterSpec params,
                     SecureRandom random);

Phase

    public Key doPhase(Key key, boolean lastPhase);

KeyStore

keystore是存储密钥和证书的地方,
keystore位于:
系统级:${JAVA_HOME}/lib/ext/cacerts
用户级:${user.home}/.keystore目录下。

JDK有3个与keystore相关的工具:keytool,jarsignerpolicytool(GUI).
通过KeyStore.getInstance()获取keystore管理类,

keystore的类型:jks(默认),pkcs12(网络标准),jceks(3DES加密),`dks)

keystore中包含两种类型数据:1.私钥 2,被信任的证书

RSA

简介

参考链接:

RSA加密算法是以其创造者Rivest, Shamir & Adleman三人的首字母命名,是一种非对称加密。

RSA加密的构成:公钥, 私钥和加解密算法。
RSA的理论基础:验证大数是否是素数很容易,但要分解素数很困难。

算法思想:
1.将待加密的数据看成一个数字m
2.寻找两个大素数p和q
3.将p和q相乘,得到模n
4.找到两个指数e和d,使得 m^ed (mod n) = m (mod n)
5.加密密文 s=m^d (mod n)
6.解密密文 m=s^e (mod n)

e和d就是两个key,通过对消息m以e或d为指数进行指数运算,得到密文,再将密文与另外一个指数进行指数运算,即可得到解密后的数据。

实践者,一般e取较小的数字,比如65537,而d的计算,就是RSA算法的基础。为了计算d,必须知道p和q,但是p和q都是由创建者随机选取的大数,只有p和q的乘积是公开的,p和q本身是保密的,因此只有创建者能够知道d。

n称为模,e称为公开指数,d称为私有指数; (e,n)称为公钥,(d,n)称为密钥
通常为了使得数据随机分布,会在密文尾部加上随机的数据对齐到某个整数,同时避免密文过短导致 m^e < n,从而能够对密文进行开方解密得到消息。

Java代码示例

首先需要密钥对,包括选择公钥e(一般选取65537),和两个不同的随机大素数p和q, 然后计算模数和私钥d
JDK提供了KeyPairGenerator来生成密钥对, 流程:

  • 创建KeyPaireGenerator的实例
  • 初始化生成器实例,设置模数的位长度
  • 调用getKeyPair(),返回的对象包含getPublic()getPrivate()两个方法分别表示公钥和私钥

生成密钥对的方法:

KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);// 长度一般是1024或2048,折中考虑性能和安全性
KeyPair kp = kpg.genKeyPair();
Key publicKey = kp.getPublic();
Key privateKey = kp.getPrivate();

// 由于Key是通用的接口,要获取具体的信息(模数和指数),还需要进一步使用KeyFactory转换成RSAPublicSpec
KeyFactory fact = KeyFactory.getInstance("RSA");
RSAPublicKeySpec pub = fact.getKeySpec(kp.getPublic(),
  RSAPublicKeySpec.class);
RSAPrivateKeySpec priv = fact.getKeySpec(kp.getPrivate(),
  RSAPrivateKeySpec.class);

pub.getModulus() instanceof BigInteger
pub.getPublicExponent() instanceof BigInteger
pub.getPrivateExponent() instanceof BigInteger  

使用密钥对加密

// n和e是公钥的模和指数
RSAPublicKeySpec keySpec = new RSAPublicKeySpec(n, e);
KeyFactory fact = KeyFactory.getInstance("RSA");
PublicKey pubKey = fact.generatePublic(keySpec);

// byte[] data
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
byte[] cipherData = cipher.doFinal(data); // 加密后的数据

解密

// n和d是私钥的模和指数
RSAPrivateKeySpec keySpec = new RSAPrivateKeySpec(n, d);
KeyFactory fact = KeyFactory.getInstance("RSA");
PrivateKey privKey = fact.generatePrivate(keySpec);

// byte[] data
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privKey);
byte[] cipherData = cipher.doFinal(data); // 解密后的数据

AES

简介

参考链接:

AES是对称加密,AES加密算法的key无需特别选取。

Java代码示例

加密

byte[] key = //... secret sequence of bytes
byte[] dataToSend = ...

Cipher c = Cipher.getInstance("AES");
SecretKeySpec k =
  new SecretKeySpec(key, "AES");
c.init(Cipher.ENCRYPT_MODE, k);
byte[] encryptedData = c.doFinal(dataToSend);

解密

byte[] key = //... we know the secret!
byte[] encryptedData = //... received from Alice

Cipher c = Cipher.getInstance("AES");
SecretKeySpec k =
  new SecretKeySpec(key, "AES");
c.init(Cipher.DECRYPT_MODE, k);
byte[] data = c.doFinal(encryptedData);

// do something with data

其他代码

片段

private static final String key = "aesEncryptionKey";
private static final String initVector = "encryptionIntVec";

public static String encrypt(String value) {
	try {
		IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
		SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");

		Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
		cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);

		byte[] encrypted = cipher.doFinal(value.getBytes());
		return Base64.encodeBase64String(encrypted);
	} catch (Exception ex) {
		ex.printStackTrace();
	}
	return null;
}

解密

public static String decrypt(String encrypted) {
	try {
		IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
		SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");

		Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
		cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
		byte[] original = cipher.doFinal(Base64.decodeBase64(encrypted));

		return new String(original);
	} catch (Exception ex) {
		ex.printStackTrace();
	}

	return null;
}

DES

简介

参考:

Java代码示例

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.AlgorithmParameterSpec;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;

public class DESEncryptionExample {
	private static Cipher encryptCipher;
	private static Cipher decryptCipher;
	private static final byte[] iv = { 11, 22, 33, 44, 99, 88, 77, 66 };

	public static void main(String[] args) {
		String clearTextFile = "/Users/pankaj/source.txt";
		String cipherTextFile = "/Users/pankaj/cipher.txt";
		String clearTextNewFile = "/Users/pankaj/source-new.txt";

		try {
			// create SecretKey using KeyGenerator
			SecretKey key = KeyGenerator.getInstance("DES").generateKey();
			AlgorithmParameterSpec paramSpec = new IvParameterSpec(iv);

			// get Cipher instance and initiate in encrypt mode
			encryptCipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
			encryptCipher.init(Cipher.ENCRYPT_MODE, key, paramSpec);

			// get Cipher instance and initiate in decrypt mode
			decryptCipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
			decryptCipher.init(Cipher.DECRYPT_MODE, key, paramSpec);

			// method to encrypt clear text file to encrypted file
			encrypt(new FileInputStream(clearTextFile), new FileOutputStream(cipherTextFile));

			// method to decrypt encrypted file to clear text file
			decrypt(new FileInputStream(cipherTextFile), new FileOutputStream(clearTextNewFile));
			System.out.println("DONE");
		} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
				| InvalidAlgorithmParameterException | IOException e) {
			e.printStackTrace();
		}

	}

	private static void encrypt(InputStream is, OutputStream os) throws IOException {

		// create CipherOutputStream to encrypt the data using encryptCipher
		os = new CipherOutputStream(os, encryptCipher);
		writeData(is, os);
	}

	private static void decrypt(InputStream is, OutputStream os) throws IOException {

		// create CipherOutputStream to decrypt the data using decryptCipher
		is = new CipherInputStream(is, decryptCipher);
		writeData(is, os);
	}

	// utility method to read data from input stream and write to output stream
	private static void writeData(InputStream is, OutputStream os) throws IOException {
		byte[] buf = new byte[1024];
		int numRead = 0;
		// read and write operation
		while ((numRead = is.read(buf)) >= 0) {
			os.write(buf, 0, numRead);
		}
		os.close();
		is.close();
	}

Diffie-Hellman

介绍

参考:

Diffie-Hellman是一种密钥交换方法,这种算法比RSA先提出。
其基本思想如下:

  • 发起方选择一个大素数p和一个较小的数g,以及一个数a,生成A = g^a (mod p); 然后将p,g,A传送给接收方;
  • 接收方选择一个数b,生成B = g^b (mod p),将B传送给A
  • 则发送方的密钥为 keyA = B^a (mod p), 接收方的密钥为keyB = A ^ b(mod p), 并且keyA = keyB

注意,最终发送方和接收方都获取到了密钥,并且密钥相同;但是密钥并没有被传输。
注意,为了使得密钥不容易被猜测,a,b,p都必须非常大,g则无需很大,通常取2或者5。其中,只有ab是秘密的,其他数据都可以明文传输。
该加密算法的理论技术是下面的等式:
(g^a mod p)^b mod p = (g^b mod p)^a mod p
最终的密钥就是等式一边的结果。

Java代码示例

官方代码示例:

import java.io.*;
import java.math.BigInteger;
import java.security.*;
import java.security.spec.*;
import java.security.interfaces.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import javax.crypto.interfaces.*;
import com.sun.crypto.provider.SunJCE;

public class DHKeyAgreement2 {
    private DHKeyAgreement2() {}
    public static void main(String argv[]) throws Exception {
        
        /*
         * Alice creates her own DH key pair with 2048-bit key size
         */
        System.out.println("ALICE: Generate DH keypair ...");
        KeyPairGenerator aliceKpairGen = KeyPairGenerator.getInstance("DH");
        aliceKpairGen.initialize(2048);
        KeyPair aliceKpair = aliceKpairGen.generateKeyPair();
        
        // Alice creates and initializes her DH KeyAgreement object
        System.out.println("ALICE: Initialization ...");
        KeyAgreement aliceKeyAgree = KeyAgreement.getInstance("DH");
        aliceKeyAgree.init(aliceKpair.getPrivate());
        
        // Alice encodes her public key, and sends it over to Bob.
        byte[] alicePubKeyEnc = aliceKpair.getPublic().getEncoded();
        
        /*
         * Let's turn over to Bob. Bob has received Alice's public key
         * in encoded format.
         * He instantiates a DH public key from the encoded key material.
         */
        KeyFactory bobKeyFac = KeyFactory.getInstance("DH");
        X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(alicePubKeyEnc);

        PublicKey alicePubKey = bobKeyFac.generatePublic(x509KeySpec);

        /*
         * Bob gets the DH parameters associated with Alice's public key.
         * He must use the same parameters when he generates his own key
         * pair.
         */
        DHParameterSpec dhParamFromAlicePubKey = ((DHPublicKey)alicePubKey).getParams();

        // Bob creates his own DH key pair
        System.out.println("BOB: Generate DH keypair ...");
        KeyPairGenerator bobKpairGen = KeyPairGenerator.getInstance("DH");
        bobKpairGen.initialize(dhParamFromAlicePubKey);
        KeyPair bobKpair = bobKpairGen.generateKeyPair();

        // Bob creates and initializes his DH KeyAgreement object
        System.out.println("BOB: Initialization ...");
        KeyAgreement bobKeyAgree = KeyAgreement.getInstance("DH");
        bobKeyAgree.init(bobKpair.getPrivate());

        // Bob encodes his public key, and sends it over to Alice.
        byte[] bobPubKeyEnc = bobKpair.getPublic().getEncoded();

        /*
         * Alice uses Bob's public key for the first (and only) phase
         * of her version of the DH
         * protocol.
         * Before she can do so, she has to instantiate a DH public key
         * from Bob's encoded key material.
         */
        KeyFactory aliceKeyFac = KeyFactory.getInstance("DH");
        x509KeySpec = new X509EncodedKeySpec(bobPubKeyEnc);
        PublicKey bobPubKey = aliceKeyFac.generatePublic(x509KeySpec);
        System.out.println("ALICE: Execute PHASE1 ...");
        aliceKeyAgree.doPhase(bobPubKey, true);

        /*
         * Bob uses Alice's public key for the first (and only) phase
         * of his version of the DH
         * protocol.
         */
        System.out.println("BOB: Execute PHASE1 ...");
        bobKeyAgree.doPhase(alicePubKey, true);

        /*
         * At this stage, both Alice and Bob have completed the DH key
         * agreement protocol.
         * Both generate the (same) shared secret.
         */
        try {
            byte[] aliceSharedSecret = aliceKeyAgree.generateSecret();
            int aliceLen = aliceSharedSecret.length;
            byte[] bobSharedSecret = new byte[aliceLen];
            int bobLen;
        } catch (ShortBufferException e) {
            System.out.println(e.getMessage());
        }        // provide output buffer of required size
        bobLen = bobKeyAgree.generateSecret(bobSharedSecret, 0);
        System.out.println("Alice secret: " +
                toHexString(aliceSharedSecret));
        System.out.println("Bob secret: " +
                toHexString(bobSharedSecret));
        if (!java.util.Arrays.equals(aliceSharedSecret, bobSharedSecret))
            throw new Exception("Shared secrets differ");
        System.out.println("Shared secrets are the same");

        /*
         * Now let's create a SecretKey object using the shared secret
         * and use it for encryption. First, we generate SecretKeys for the
         * "AES" algorithm (based on the raw shared secret data) and
         * Then we use AES in CBC mode, which requires an initialization
         * vector (IV) parameter. Note that you have to use the same IV
         * for encryption and decryption: If you use a different IV for
         * decryption than you used for encryption, decryption will fail.
         *
         * If you do not specify an IV when you initialize the Cipher
         * object for encryption, the underlying implementation will generate
         * a random one, which you have to retrieve using the
         * javax.crypto.Cipher.getParameters() method, which returns an
         * instance of java.security.AlgorithmParameters. You need to transfer
         * the contents of that object (e.g., in encoded format, obtained via
         * the AlgorithmParameters.getEncoded() method) to the party who will
         * do the decryption. When initializing the Cipher for decryption,
         * the (reinstantiated) AlgorithmParameters object must be explicitly
         * passed to the Cipher.init() method.
         */
        System.out.println("Use shared secret as SecretKey object ...");
        SecretKeySpec bobAesKey = new SecretKeySpec(bobSharedSecret, 0, 16, "AES");
        SecretKeySpec aliceAesKey = new SecretKeySpec(aliceSharedSecret, 0, 16, "AES");

        /*
         * Bob encrypts, using AES in CBC mode
         */
        Cipher bobCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        bobCipher.init(Cipher.ENCRYPT_MODE, bobAesKey);
        byte[] cleartext = "This is just an example".getBytes();
        byte[] ciphertext = bobCipher.doFinal(cleartext);

        // Retrieve the parameter that was used, and transfer it to Alice in
        // encoded format
        byte[] encodedParams = bobCipher.getParameters().getEncoded();

        /*
         * Alice decrypts, using AES in CBC mode
         */

        // Instantiate AlgorithmParameters object from parameter encoding
        // obtained from Bob
        AlgorithmParameters aesParams = AlgorithmParameters.getInstance("AES");
        aesParams.init(encodedParams);
        Cipher aliceCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        aliceCipher.init(Cipher.DECRYPT_MODE, aliceAesKey, aesParams);
        byte[] recovered = aliceCipher.doFinal(ciphertext);
        if (!java.util.Arrays.equals(cleartext, recovered))
            throw new Exception("AES in CBC mode recovered text is " +
                    "different from cleartext");
        System.out.println("AES in CBC mode recovered text is "
                "same as cleartext");
    }

    /*
     * Converts a byte to hex digit and writes to the supplied buffer
     */
    private static void byte2hex(byte b, StringBuffer buf) {
        char[] hexChars = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
                '9', 'A', 'B', 'C', 'D', 'E', 'F' };
        int high = ((b & 0xf0) >> 4);
        int low = (b & 0x0f);
        buf.append(hexChars[high]);
        buf.append(hexChars[low]);
    }

    /*
     * Converts a byte array to hex string
     */
    private static String toHexString(byte[] block) {
        StringBuffer buf = new StringBuffer();
        int len = block.length;
        for (int i = 0; i < len; i++) {
            byte2hex(block[i], buf);
            if (i < len-1) {
                buf.append(":");
            }
        }
        return buf.toString();
    }
}
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.Security;

import javax.crypto.KeyAgreement;
import javax.crypto.spec.DHParameterSpec;

public class MainClass {
  private static BigInteger g512 = new BigInteger("1234567890", 16);

  private static BigInteger p512 = new BigInteger("1234567890", 16);

  public static void main(String[] args) throws Exception {
    Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());

    DHParameterSpec dhParams = new DHParameterSpec(p512, g512);
    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("DH", "BC");

    keyGen.initialize(dhParams, new SecureRandom());

    KeyAgreement aKeyAgree = KeyAgreement.getInstance("DH", "BC");
    KeyPair aPair = keyGen.generateKeyPair();
    KeyAgreement bKeyAgree = KeyAgreement.getInstance("DH", "BC");
    KeyPair bPair = keyGen.generateKeyPair();

    aKeyAgree.init(aPair.getPrivate());
    bKeyAgree.init(bPair.getPrivate());

    aKeyAgree.doPhase(bPair.getPublic(), true);
    bKeyAgree.doPhase(aPair.getPublic(), true);

    MessageDigest hash = MessageDigest.getInstance("SHA1", "BC");
    System.out.println(new String(hash.digest(aKeyAgree.generateSecret())));
    System.out.println(new String(hash.digest(bKeyAgree.generateSecret())));
  }
}

EC Diffie-Hellman算法

EC Diffie-Hellman算法,在安全领域一般表示为ECDH或者ECHDE,其中后缀E只是表示公钥是使用恒定的数字还是临时生成的数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值