Java安全之安全加密算法
共 18587字,需浏览 38分钟
·
2020-11-02 05:06
点击上方蓝色字体,选择“标星公众号”
优质文章,第一时间送达
作者 | nice_0e3
来源 | urlify.cn/UZRjqe
前言
本篇文来谈谈关于常见的一些加密算法,其实在此之前,对算法的了解并不是太多。了解的层次只是基于加密算法的一些应用上。也来浅谈一下加密算法在安全领域中的作用。写本篇文也是基于算法的应用和实现,也是我的基本原则,能用就行。
1 、算法体制
在加密算法里面大致分为四大类:对称加密算法、非对称加密算法、散列函数、组合加密。
对称加密:指的是加密的密钥和解密的密钥相同。
非对称加密:指的是加密的密钥和加密的密钥不同。分为公钥和私钥。
散列函数:主要用于验证数据的完整性,长度不受限制,hash值容易计算,散列运算过程不可逆如:MD5、SHA
2 、散列算法
Hash,一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
MD5
md5的运用范围也比较广,比如在一些数据库存储密码的时候会去使用到该算法去进行加密存储。当然也可以看到网上的一些md5破解的。但是他们都是基于彩虹表去跑,使用字典进行md5加密,然后把加密的值进行对比,从而判断该md5值的明文。
代码实例:
package com.demo;
import com.sun.org.apache.xerces.internal.impl.dv.util.HexBin;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MD5demo {
private static String name = "xiaoming";
public static void main(String[] args) throws NoSuchAlgorithmException {
MessageDigest md5 = MessageDigest.getInstance("MD5");//设置要返回的摘要算法对象
byte[] digest = md5.digest(name.getBytes()); //计算hash值
System.out.println(HexBin.encode(digest));//输出hash结果
}
}
SHA
安全散列算法(英语:Secure Hash Algorithm,缩写为SHA)是一个密码散列函数家族,是FIPS所认证的安全散列算法。能计算出一个数字消息所对应到的,长度固定的字符串(又称消息摘要)的算法。且若输入的消息不同,它们对应到不同字符串的机率很高。
SHA家族的五个算法,分别是SHA-1、SHA-224、SHA-256、SHA-384,和SHA-512
下面是一张对照图。
代码实例:
package com.demo;
import com.sun.org.apache.xerces.internal.impl.dv.util.HexBin;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class jdksha1 {
private static String name = "xiaoming";
public static void main(String[] args) throws NoSuchAlgorithmException {
MessageDigest sha = MessageDigest.getInstance("SHA");
byte[] digest = sha.digest(name.getBytes());
System.out.println(HexBin.encode(digest));
}
}
3 、对称加密算法
对称加密算法,他的加解密密钥都是一样的。而对称加密的算法又分两大类,一种是每次对明文中的字节使用密钥进行加密,而第二种是对一组明文使用密钥进行加密。
先来看看分组加密的工作模式。
分组加密的工作模式
1. ECB:电子密码本(最常用的,每次加密均产生独立的密文分组,并且对其他的密文分组不会产生影响,也就是相同的明文加密后产生相同的密文)
2. CBC:密文链接(常用的,明文加密前需要先和前面的密文进行异或运算,也就是相同的明文加密后产生不同的密文)
除了这两种常用的工作模式,还有:
3. CFB:密文反馈
4. OFB:输出反馈
5. CTR:计数器
分组密码填充方式
1. NoPadding:无填充
2. PKCS5Padding:
3. ISO10126Padding
常用的加密方式DES、3DES、AES。可以来对比一下,这几个算法的区别。
BASE64
因为BASE64的加密解密算法是公开的,所以加密数据是没有任何安全性可言。先来看看API文档中提供的BASE64加密的类。除了在JDK中内置的也有一些第三方类会提供BASE64加密的类。
代码实例:
package com.demo;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import java.io.IOException;
public class base64demo {
public static void main(String[] args) throws IOException {
String name = "xiaoming";
BASE64Encoder encoder = new BASE64Encoder//实例化BASE64Encoder对象
String encode = encoder.encode(name.getBytes());//调用encode进行加密
System.out.println(encode);
BASE64Decoder base64Decoder = new BASE64Decoder();//实例化BASE64Decoder对象
byte[] bytes = base64Decoder.decodeBuffer(encode);//进行解密
System.out.println(bytes);
String s = new String(bytes);
System.out.println(s);
}
}
BASE64的加解密比较简单,在这里就不细说了。
DES
DES的算法其实已经被公开了,其实是不太安全的。
代码实例:
package com.demo;
import com.sun.org.apache.xerces.internal.impl.dv.util.HexBin;
import javax.crypto.*;
import javax.crypto.spec.DESKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
public class desdemo {
private static String name = "xiaoming";
public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException, NoSuchPaddingException, BadPaddingException, IllegalBlockSizeException {
//1.生成密钥
KeyGenerator des = KeyGenerator.getInstance("DES");//设置获取des的密钥
des.init(56);//初始化密钥生成器,设置为56长度的密钥
SecretKey secretKey = des.generateKey();//获取密钥
// System.out.println(secretKey);
byte[] deskey = secretKey.getEncoded(); //获取密钥的byte数组
//2.密钥转换
DESKeySpec desKeySpec = new DESKeySpec(deskey);
SecretKeyFactory des1 = SecretKeyFactory.getInstance("DES");
SecretKey secretKey1 = des1.generateSecret(desKeySpec);
// System.out.println(secretKey1);
//3.加密
Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");//选择算法、工作模式和填充方式
cipher.init(Cipher.ENCRYPT_MODE,secretKey1);//选择加密模式和密钥进行加密
byte[] res = cipher.doFinal(name.getBytes());//加密数据
// System.out.println(res);
// String s = new String(res);
System.out.println(HexBin.encode(res));//输出加密后结果
//4.解密
cipher.init(Cipher.DECRYPT_MODE,secretKey1);//选择密钥,并使用解密模式
byte[] bytes = cipher.doFinal(res);//解密加密后的结果
String s1 = new String(bytes);
System.out.println(s1);
}
}
3DES
3DES也被叫做DESede,也就是基于DES三次加密。其实也有四层和两层DES,但是平时中用的最多的还是3DES。
上面的图是他的一个算法的说明图。
其实3DES和DES在代码中的实现方式很相似。但是要注意的一点是密钥长度要选择正确,否则会报错。或者是可以使用SecureRandom()
类,让他根据选择的加密算法使用默认的密钥长度。
代码实例:
package com.demo;
import com.sun.org.apache.xerces.internal.impl.dv.util.HexBin;
import javax.crypto.*;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.DESedeKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
public class jdk3des {
private static String name = "xiaoming";
public static void main(String[] args) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException, BadPaddingException, IllegalBlockSizeException {
//1.生成密钥
KeyGenerator des = KeyGenerator.getInstance("DESede");//设置获取des的密钥
// des.init(168);//初始化密钥生成器,设置为168长度的密钥
des.init(new SecureRandom());//根据加密算法使用默认密钥长度
SecretKey secretKey = des.generateKey();//获取密钥
// System.out.println(secretKey);
byte[] deskey = secretKey.getEncoded(); //获取密钥的byte数组
//2.密钥转换
DESedeKeySpec desKeySpec = new DESedeKeySpec(deskey);
SecretKeyFactory des1 = SecretKeyFactory.getInstance("DESede");
SecretKey secretKey1 = des1.generateSecret(desKeySpec);
// System.out.println(secretKey1);
//3.加密
Cipher cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");//选择算法、工作模式和填充方式
cipher.init(Cipher.ENCRYPT_MODE,secretKey1);//选择加密模式和密钥进行加密
byte[] res = cipher.doFinal(name.getBytes());//加密数据
// System.out.println(res);
// String s = new String(res);
System.out.println(HexBin.encode(res));//输出加密后结果
//4.解密
cipher.init(Cipher.DECRYPT_MODE,secretKey1);//选择密钥,并使用解密模式
byte[] bytes = cipher.doFinal(res);//解密加密后的结果
String s1 = new String(bytes);
System.out.println(s1);
}
}
AES
AES是目前用的最多的对称加密算法,一般用于移动通讯系统加密或者是基于SSH协议的软件
代码实例:
package com.demo;
import com.sun.org.apache.xerces.internal.impl.dv.util.HexBin;
import com.sun.xml.internal.messaging.saaj.util.Base64;
import sun.misc.BASE64Encoder;
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
public class aesdemo {
private static String name = "xiaoming";
public static void main(String[] args) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
//1.获取密钥
KeyGenerator aes = KeyGenerator.getInstance("AES");
aes.init(128);
SecretKey secretKey = aes.generateKey();
byte[] encoded = secretKey.getEncoded();
//2.密钥转换
Key key = new SecretKeySpec(encoded, "AES");
//3.加密
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE,key);
byte[] bytes = cipher.doFinal(name.getBytes());
System.out.println(new BASE64Encoder().encode(bytes));//加密完成后,再使用base64进行加密
//4.解密
cipher.init(Cipher.DECRYPT_MODE,key);
byte[] bytes1 = cipher.doFinal(bytes);
System.out.println(new String(bytes1));
}
}
下面来放一张基于AES加解密的流程图。
PBE
PBE算法(Password Based Encryption,基于口令加密)是一种基于口令的加密算法,其特点是使用口令代替了密钥,而口令由用户自己掌管,采用随机数杂凑多重加密等方法保证数据的安全性。PBE算法在加密过程中并不是直接使用口令来加密,而是加密的密钥由口令生成,这个功能由PBE算法中的KDF函数完成。KDF函数的实现过程为:将用户输入的口令首先通过“盐”(salt)的扰乱产生准密钥,再将准密钥经过散列函数多次迭代后生成最终加密密钥,密钥生成后,PBE算法再选用对称加密算法对数据进行加密,可以选择DES、3DES、RC5等对称加密算法
以上这张图是PBE算法的实现方式
代码实例:
package com.demo;
import com.sun.xml.internal.messaging.saaj.util.Base64;
import sun.misc.BASE64Encoder;
import javax.crypto.*;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
public class pbedemo {
private static String name = "xiaoming";
public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, BadPaddingException, IllegalBlockSizeException {
//1.初始化盐
SecureRandom secureRandom = new SecureRandom();
byte[] salt = secureRandom.generateSeed(8);
//2. 获取密钥
String password = "123456";
PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray());
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBEWITHMD5andDES");
Key key = factory.generateSecret(pbeKeySpec);
//3.加密
PBEParameterSpec pbeParameterSpec = new PBEParameterSpec(salt, 100);
Cipher cipher = Cipher.getInstance("PBEWITHMD5andDES");
cipher.init(Cipher.ENCRYPT_MODE,key,pbeParameterSpec);
byte[] bytes = cipher.doFinal(name.getBytes());
System.out.println(new BASE64Encoder().encode(bytes));
//4.解密
cipher.init(Cipher.DECRYPT_MODE,key,pbeParameterSpec);
byte[] res = cipher.doFinal(bytes);
System.out.println(new String(res));
}
}
4 、非对称加密算法
非对称加密算法需要两个密钥:公钥和私钥。公钥]与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。
DH
1976年非对称加密算法思想被提出,但是当时并没有给出具体算法和方案,因为当时没有研究出单向函数
(也就是信息摘要算法还没出现),但是IEEE的期刊中给出了通信时双方如何通过信息交换协商密钥
的算法,也就是DH算法,通过该算法双方可以协商对称加密的密钥。
package com.demo;
import com.sun.org.apache.xerces.internal.impl.dv.util.HexBin;
import javax.crypto.*;
import javax.crypto.interfaces.DHPrivateKey;
import javax.crypto.interfaces.DHPublicKey;
import javax.crypto.spec.DHParameterSpec;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
public class dhdemo {
private static String name = "xiaoming";
public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidAlgorithmParameterException, InvalidKeyException, NoSuchPaddingException, BadPaddingException, IllegalBlockSizeException {
// 1.初始化发送方密钥
KeyPairGenerator sendKeyPairGenerator = KeyPairGenerator.getInstance("DH");
sendKeyPairGenerator.initialize(512);
KeyPair sendKeyPair = sendKeyPairGenerator.generateKeyPair();
byte[] sendPublicKeyEnc = sendKeyPair.getPublic().getEncoded();//生成发送方公钥,发送给接收方(网络、文件...)
// 2.初始化接收方密钥:
KeyFactory receiverKeyFactory = KeyFactory.getInstance("DH");
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(sendPublicKeyEnc);
PublicKey receivePublicKey = receiverKeyFactory.generatePublic(x509EncodedKeySpec);
DHParameterSpec dhParameterSpec = ((DHPublicKey)receivePublicKey).getParams();
KeyPairGenerator receiverKeyPairGenerator = KeyPairGenerator.getInstance("DH");
receiverKeyPairGenerator.initialize(dhParameterSpec);
KeyPair receiverKeyPair = receiverKeyPairGenerator.generateKeyPair();
PrivateKey receiverPrivateKey = receiverKeyPair.getPrivate();
byte[] receiverPublicKeyEnc = receiverKeyPair.getPublic().getEncoded();
// 3.构建接收方密钥:
KeyAgreement receiverKeyAgreement = KeyAgreement.getInstance("DH");
receiverKeyAgreement.init(receiverPrivateKey);
receiverKeyAgreement.doPhase(receivePublicKey, true);
SecretKey receiverDESKey = receiverKeyAgreement.generateSecret("DES");//接收方Key
// 4.构建发送方密钥:
KeyFactory sendKeyFactory = KeyFactory.getInstance("DH");
x509EncodedKeySpec = new X509EncodedKeySpec(receiverPublicKeyEnc);
PublicKey sendPublicKey = sendKeyFactory.generatePublic(x509EncodedKeySpec);
KeyAgreement sendKeyAgreement = KeyAgreement.getInstance("DH");
sendKeyAgreement.init(sendKeyPair.getPrivate());
sendKeyAgreement.doPhase(sendPublicKey, true);
SecretKey sendDESKey = sendKeyAgreement.generateSecret("DES");//发送方Key
// 5.发送方加密:
Cipher sendCipher = Cipher.getInstance("DES");
sendCipher.init(Cipher.ENCRYPT_MODE, sendDESKey);
byte[] sendResult = sendCipher.doFinal(name.getBytes());
System.out.println("sendResult :"+ HexBin.encode(sendResult));
// 6.接收方解密:
Cipher receiverCipher = Cipher.getInstance("DES");
receiverCipher.init(Cipher.DECRYPT_MODE, receiverDESKey);
byte[] receiverResult = receiverCipher.doFinal(sendResult);
System.out.println("receiverResult : "+new String (receiverResult));
}
}
RSA
RSA公开密钥密码体制是一种使用不同的加密密钥与解密密钥,“由已知加密密钥推导出解密密钥在计算上是不可行的”密码体制。RSA算法实现了公钥加密、私钥解密 和私钥解密、公钥加密的一个机制。
也就是说使用公钥来进行加密,想要解密获取明文内容,就必须使用对应的私钥来进行解密。而在其中私钥中的内容其实是比较长的,而公钥里面的内容会偏短一些。因为私钥一般会储存在本地,而公钥会放到各大互联网上,公钥比私钥内容短也是为了方便传输和存储。
代码示例:
package com.demo;
import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;
import sun.misc.BASE64Encoder;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
public class rsademo {
private static String name = "xiaoming";
public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
// 1.初始化密钥
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");//设置获取RSA的密钥
keyPairGenerator.initialize(512);//设置密钥长度
KeyPair keyPair = keyPairGenerator.generateKeyPair();//生成密钥对
RSAPublicKey rsaPublic =(RSAPublicKey) keyPair.getPublic();//获取公钥
RSAPrivateKey rsaPrivate = (RSAPrivateKey)keyPair.getPrivate();//获取私钥
// System.out.println(new BASE64Encoder().encode(rsaPublic.getEncoded()));//输出公钥
System.out.println("");
// System.out.println(new BASE64Encoder().encode(rsaPrivate.getEncoded()));//输出私钥
// 2.私钥加密、公钥解密
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(rsaPrivate.getEncoded());//对密钥进行编码处理
KeyFactory rsa = KeyFactory.getInstance("RSA");//设置获取RSA的密钥
PrivateKey privateKey = rsa.generatePrivate(pkcs8EncodedKeySpec);//从提供的密钥规范生成私钥对象。
// 2.1私钥加密
Cipher encodecipher = Cipher.getInstance("RSA");//设置获取RSA的密钥
encodecipher.init(Cipher.ENCRYPT_MODE,privateKey);//设置为加密类型并传入私钥进行加密
byte[] res = encodecipher.doFinal(name.getBytes());//对内容进行加密
System.out.println(new BASE64Encoder().encode(res));
// 2.2公钥解密
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(rsaPublic.getEncoded());//对公钥进行编码处理
rsa = KeyFactory.getInstance("RSA");//设置获取RSA的密钥
PublicKey publicKey = rsa.generatePublic(x509EncodedKeySpec);//从提供的密钥规范生成公钥对象。
Cipher decodecipher = Cipher.getInstance("RSA");//设置获取RSA的密钥
decodecipher.init(Cipher.DECRYPT_MODE,publicKey);//设置解密类型并传入公钥进行解密
byte[] decoderes = decodecipher.doFinal(res);//对内容进行解密
System.out.println(new String(decoderes));
}
}
下面再来看个公钥加密私钥解密的代码
package com.demo;
import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;
import sun.misc.BASE64Encoder;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
public class rsademo {
private static String name = "xiaoming";
public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
// 1.初始化密钥
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");//设置获取RSA的密钥
keyPairGenerator.initialize(512);//设置密钥长度
KeyPair keyPair = keyPairGenerator.generateKeyPair();//生成密钥对
RSAPublicKey rsaPublic =(RSAPublicKey) keyPair.getPublic();//获取公钥
RSAPrivateKey rsaPrivate = (RSAPrivateKey)keyPair.getPrivate();//获取私钥
// System.out.println(new BASE64Encoder().encode(rsaPublic.getEncoded()));//输出公钥
System.out.println("");
// 3.公钥加密、私钥解密
// 3.1公钥加密
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(rsaPublic.getEncoded());
KeyFactory encodersa = KeyFactory.getInstance("RSA");
PublicKey publicKey = encodersa.generatePublic(x509EncodedKeySpec);
Cipher encodecipher = Cipher.getInstance("RSA");
encodecipher.init(Cipher.ENCRYPT_MODE,publicKey);
byte[] encoderes = encodecipher.doFinal(name.getBytes());
System.out.println(new BASE64Encoder().encode(encoderes));
// 3.2私钥解密
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(rsaPrivate.getEncoded());
KeyFactory decodersa = KeyFactory.getInstance("RSA");
PrivateKey privateKey = decodersa.generatePrivate(pkcs8EncodedKeySpec);
Cipher decodecipher = Cipher.getInstance("RSA");
decodecipher.init(Cipher.DECRYPT_MODE,privateKey);
byte[] decoderes = decodecipher.doFinal(encoderes);
System.out.println(new String(decoderes));
}
}
在代码中其实可以看到用到了x509EncodedKeySpec
和pkcs8EncodedKeySpec
的类,其实这两个类一个用于对私钥进行编码的,一个是对私钥进行编码。
5 、结尾
在安全中其实加密算法显得格外的重要,举个例子,比如说冰蝎,为什么就能过一些设备呢?其实分析过冰蝎的,或者是看过冰蝎作者大大的文章的应该会知道,冰蝎在流量传输中也是做了加密的。使用的是AES加密。首先是将客户端发送的命令转换成字节码,然后使用aes进行加密,然后在客户端也就是webshell获取密钥进行解密得到字节码,最后就是使用defineClass来动态加载字节码进行执行。
但是在后面各大厂商的设备随着各大厂商设备的不断升级,冰蝎2还是被杀了。这是因为在冰蝎aes传输密钥的时候被识别出来了。那么我们如果再对该密钥进行更狠一点加密呢?虽然说也能实现,步骤繁琐一些。但是这还是会产生一些流量。在冰蝎3中的做法是去除了握手的过程,直接写死在Webshell中的字符串作为AES密钥解密流量包。
后面打算基于冰蝎进行一个重写,并使用不同的算法来实现加密。
粉丝福利:实战springboot+CAS单点登录系统视频教程免费领取
???
?长按上方微信二维码 2 秒 即可获取资料
感谢点赞支持下哈