对字符、文本进行编码
计算机最常见的用途之一就是处理文字内容,人类识别文字内容依靠的是人类的语言系统,以及语言和文字之间建立的映射关系,人们看到的文字不管用什么字体书写,只要能正确的识别并阅读,就会转化为语言交给大脑去做进一步处理。同一本小说比如鲁迅的《狂人日记》不管使用的宋体还是楷体,也无论是简体中文还是繁体汉字,人们阅读后都能看到那一幕幕封建礼教吃人的画面。
计算机在处理文字的时候,即便是目前最厉害的AI程序,也无法像人类一样直接理解文字符号的意义,因此计算机在处理文本内容时使用了一种更容易达成的方式来把复杂的问题分解为简单问题。
现在全世界范围内人类还在使用的绝大多数语言都能转化为对应的文本
将文本拆解为由若干字符组成的字符串
对每一个字符进行编号
通过记录一串数字编号来将复杂的图形文字排布顺序记录下来
计算机存储文本或者通过网络通信传输文本时只需要保存和传输文本对应的数字编号。
从计算机中读取文本内容时,读取数字编号,并找出数字编号对应的字符翻译成人能看懂的图形符号。
上面的过程就是对文字信息的编码和解码的过程,举个更具体的例子
如果将英文进行编码解码,英文共有26个字母组成,从a到z进行编号,a是1号,b是2号,c是3号,……,z就是26号,再加上一个空格编号为0。
如果将文本bless the motherland for an early reunification
按刚才的编号进行编码,就会得到一串数字组成的序列2 12 5 19 19 0 20 8 5 0 13 15 20 8 5 18 12 1 14 4 0 6 15 18 0 1 14 0 5 1 18 12 25 0 18 5 21 14 9 6 9 3 1 20 9 15 14
上面看到的这些数字是十进制显示的,实际上我们在计算机中打印内存中的数据,往往会使用16进制显示,十六进制显示的编码看起来像这样\x2 \xc \x5 \x13 \x13 \x0 ……
斜杠x后面跟一个数值表示这是使用16进制记录的数值。
通过网络将这一串数字发送给目标设备,目标设备只要用同样的编码方式反向操作把对应的数字替换成字母表中的字母就能得到我们传输给他的信息bless the motherland for an early reunification。
通过对文本编码,网络传输和存储信息都将节省大量的空间,这也是为什么使用微信打字发送消息很快,但是同样的信息如果是手写字体拍成图片传输就会慢很多的原因。
当然对文本编码的好处远不止这些,计算机本就是用来数学计算的工具,一旦将信息编码为数字,计算机cpu内的各种指令集就能对信息进行进一步的处理,比如压缩,加密等,后面我们将会用python代码来演示这些例子。
常见的文本编码方式
上一个例子中我们只对26个英文字母和空格进行了编码,且没有区分大小写,因此编码后的数字序列中只会出现0到26共27个数字编号。如果需要区分大小写,至少还需要增加26个编号,也就是53个编号。
理论上,每个人在使用计算机处理数据的时候都可以根据需要处理数据的特点定义一种编码方式(这里说的编码不仅限于文本,包括图像、声音等多媒体数据也可以进行编码)。
然而工程师在实际开发软件的过程中考虑到兼容问题,大部分时候都不会去自己发明一套编码,而是使用一些已经被广泛接纳的标准编码方式来处理信息。
ASCII编码
ASCII((American Standard Code for Information Interchange): 美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。它是最通用的信息交换标准,并等同于国际标准ISO/IEC 646。ASCII第一次以规范标准的类型发表是在1967年,最后一次更新则是在1986年,到目前为止共定义了128个字符。
因为编码后的字符保存为数字序列,数值可以比较大小,因此英文文本也就可以按对应的ASCII码大小进行排序。
在Java中,查看字符的ASCII码非常简单,直接将字符类型强转为int类型即可,代如下码
public class AsciiDemo {
public static void main(String[] args) {
System.out.println((int) '0');
}
}
常用文字编码
由于ASCII编码只有一个字节,1个字节编码拉丁文字母和常见英文标点符号还是够用的,但涉及到拉丁语以外的语言1个字节就不够了,因此各厂商和一些标准化组织纷纷推出了各自的编码标准用与特定的场景。
Latin1
Latin1是ISO-8859-1的别名,有些环境下写作Latin-1。ISO-8859-1编码是单字节编码,向下兼容ASCII,其编码范围是0x00-0xFF,0x00-0x7F之间完全和ASCII一致,0x80-0x9F之间是控制字符,0xA0-0xFF之间是文字符号。常用于url编码。
中文汉字编码
GB2312-80GB 2312或GB2312-80是中国国家标准简体中文字符集,全称《信息交换用汉字编码字符集·基本集》,又称 GB 0,由中国国家标准总局发布,1981 年 5 月 1 日实施。GB2312编码通行于中国大陆;新加坡等地也采用此编码。中国大陆几乎所有的中文系统和国际化的软件都支持GB2312。GB2312标准共收录6763个汉字,其中一级汉字3755个,二级汉字3008个;同时收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个字符。对于人名、古汉语等方面出现的罕用字,GB2312不能处理,GBK即汉字内码扩展规范,K为(扩展)中“扩”字的声母。英文全称Chinese Internal Code Specification。GBK共收入21886个汉字和图形符号,包括:GB2312中的全部汉字、非汉字符号。BIG5中的全部汉字。与ISO10646相应的国家标准GB13000中的其它CJK汉字,以上合计20902个汉字。其它汉字、部首、符号,共计984个。GBK向下与GB2312完全兼容,向上支持ISO10646国际标准,支持用户自己定义字符。GB18030GB 18030,全称:国家标准GB 18030-2005《信息技术中文编码字符集》,是中华人民共和国现时最新的内码字集,是GB18030-2000《信息技术信息交换用汉字编码字符集基本集的扩充》的修订版。GB18030与GB 2312-1980和GBK兼容,共收录汉字70244个。最多可定义161万个字符。支持中国国内少数民族的文字。
Unicode(统一码)
统一码,也叫万国码、单一码(Unicode)是计算机科学领域里的一项业界标准,包括字符集、编码方案等。Unicode是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。Unicode是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。Unicode用数字0-0x10FFFF来映射这些字符,最多可以容纳1114112个字符,或者说有1114112个码位。码位就是可以分配给字符的数字。UTF-8、UTF-16、UTF-32都是将数字转换到程序数据的编码方案。
Base64
Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,Base64就是一种基于64个可打印字符来表示二进制数据的方法。
在网络传输和应用程序读取数据时,不止文本,我们还可以将多种其他类型数据编码为Base64数据,比如程序中使用的图片。
文件压缩与解压缩
在Java中,文件的压缩与解压缩,主要依赖三个类来完成,ZipInputStream、ZipOutputStream、ZipEntry
import java.io.*;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
/**
* <p> 压缩与解压缩。</p>
*
* @author zxc
* 2022/2/17
*/
public class ZipDemo {
public static void main(String[] args) throws IOException {
//目标文件
File destFile = new File("D:\\myFile.zip\\");
ZipDemo zipDemo = new ZipDemo();
//压缩文件
//List<File> list = new ArrayList<>();
//list.add(new File("d:\\1.txt"));
//list.add(new File("d:\\2.txt"));
//zipDemo.zip(list, destFile);
//解压缩文件
zipDemo.unzip(destFile, new File("d:\\myunzip"));
}
/**
* 压缩文件方法
*
* @param fileList 需要压缩的文件列表
* @param destFile 目标文件
* @throws IOException 异常信息
*/
public void zip(List<File> fileList, File destFile) throws IOException {
if (fileList == null || fileList.isEmpty()) {
return;
}
//zip输出流对象
ZipOutputStream zipOutputStream = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(destFile)));
//设置压缩等级,level (0-9)
zipOutputStream.setLevel(5);
//遍历文件列表
for (File file : fileList) {
//zip文件条目对象
ZipEntry zipEntry = new ZipEntry(file.getName());
//将文件条目对象放入zip流中
zipOutputStream.putNextEntry(zipEntry);
//将文件信息写入zip流中
FileInputStream fileInputStream = new FileInputStream(file);
byte[] bytes = new byte[1024];
int len = 0;
while ((len = fileInputStream.read(bytes)) != -1) {
zipOutputStream.write(bytes, 0, len);
}
zipOutputStream.closeEntry();
fileInputStream.close();
}
zipOutputStream.close();
}
/**
* 解压缩zip文件
*
* @param zipFile 待解压的文件
* @param destFile 解压到哪个目录
*/
public void unzip(File zipFile, File destFile) {
//目标文件必须是一个目录
if (destFile.isFile()) {
return;
}
//文件夹不存在要先创建文件夹
if (!destFile.exists()) {
destFile.mkdirs();
}
//创建一个zip读取流,在try中直接写会自动关闭流对象,无需手动关闭
try (ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(new FileInputStream(zipFile)))) {
ZipEntry zipEntry;
//遍历读取文件条目,读取下一个 ZIP文件条目并将流定位在条目数据的开头。
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
String name = zipEntry.getName();
//构建一个新文件对象,创建在destFile目录下
File f = new File(destFile, name);
//将文件条目数据写入到新的文件对象中,
FileOutputStream fileOutputStream = new FileOutputStream(f, false);
byte[] bytes = new byte[1024];
int len = 0;
while ((len = zipInputStream.read(bytes)) != -1) {
fileOutputStream.write(bytes, 0, len);
}
zipInputStream.closeEntry();
fileOutputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用Java对文本进行编码
以Base64加密为例
import java.util.Base64;
import java.util.Base64.Encoder;
/**
* <p> Base64编码解码。</p>
*
* @author zxc
* 2022/2/17
*/
public class Base64Demo {
public static void main(String[] args) {
String message = "hello world";
System.out.println("源字符串:" + message);
Base64Demo base64Demo = new Base64Demo();
String encode = base64Demo.encode(message);
System.out.println("编码之后的字符串:" + encode);
String decode = base64Demo.decode(encode);
System.out.println("解码之后的字符串:" + decode);
}
/**
* 使用base64编码字符串
*
* @param message {@link String}待编码的字符串
* @return {@link String}编码后字符串
*/
public String encode(String message) {
Encoder encoder = Base64.getEncoder();
String encodeString = encoder.encodeToString(message.getBytes());
return encodeString;
}
/**
* 解码base64字符串
*
* @param encodeString {@link String}编码之后的字符串
* @return {@link String}解码后的字符串
*/
public String decode(String encodeString) {
Base64.Decoder decoder = Base64.getDecoder();
byte[] decode = decoder.decode(encodeString);
return new String(decode);
}
}
运行main方法之后,控制台中输出了一下字符串
源字符串:hello world
编码之后的字符串:aGVsbG8gd29ybGQ=
解码之后的字符串:hello world
说明编码和解码程序是没有问题的。
密码学支持
在日常工作中经常需要涉及到对数据的加密、解密以及数据验证等操作,Java提供了一些类来完成加密解密功能。
哈希函数
哈希函数通常用于验证数据指纹,同一个文件或数据,使用同一种哈希函数得到的哈希值一定相同,如果发现对某文件两次哈希得到的结果不一样,就证明文件数据有被篡改过。
MessageDigest
MessageDigest类提供了常用的哈希函数包括SHA-1,SHA-224,SHA-256,SHA-384,SHA-512,MD5算法等。
MD5
import com.sun.org.apache.xerces.internal.impl.dv.util.HexBin;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* <p> 加密解密。</p>
*
* @author zxc
* 2022/2/17
*/
public class MessageDigestDemo {
public static void main(String[] args) throws NoSuchAlgorithmException {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] digest = messageDigest.digest("hello world".getBytes());
String encode = HexBin.encode(digest);
System.out.println(encode);
}
}
运行结果为
5EB63BBBE01EEED093CB22BB8F5ACDC3
SHA-256
import com.sun.org.apache.xerces.internal.impl.dv.util.HexBin;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* <p> 加密解密。</p>
*
* @author zxc
* 2022/2/17
*/
public class MessageDigestDemo {
public static void main(String[] args) throws NoSuchAlgorithmException {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
byte[] digest = messageDigest.digest("hello world".getBytes());
String encode = HexBin.encode(digest);
System.out.println(encode);
}
}
运行结果为
B94D27B9934D3E08A52E52D7DA7DABFAC484EFE37A5380EE9088F7ACE2EFCDE9
加密与解密
数据加密是最常见的数据安全保护手段,加密函数和哈希函数不一样,哈希函数不可逆,我们无法通过哈希值还原被哈希的数据。但是被加密的数据需要能解密还原内容,否则加密也就失去了意义。
加密前的数据称为“明文”,被加密后的数据称为“密文”。
在实际进行加密操作过程中,我们需要使用经验证安全可靠的加密算法,并且还需要生成加密所需的密钥。明文数据通过加密算法使用对应的密钥来对数据进行加密,从而得到密文。
即便知道了密文加密使用的加密算法,没有密钥的情况下,也无法在有限的时间内破解加密数据获取原文。
AES加密、解密数据
AES 加密的参数及其条件
密钥:加密的时候用秘钥,解密的时候需要同样的密钥才能解出来,密钥必须是16位字节或者24位字节或者32位字节(因为python3的字符串是unicode编码,需要encode才可以转换成字节型数据)
明文:需要加密的内容,字节长度需要是16位的倍数
模式:AES加密常用的有ECB和CBC模式
iv偏移量:这个参数在ECB模式下不需要,在CBC模式下需要
AES ECB模式
注意:sun.misc.BASE64Encoder在JDK8之后的版本中,已经由java.util.Base64.getEncoder()代替,sun.misc.BASE64Decoder被java.util.Base64.getDecoder()代替。
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
/**
* <p>AES ECB模式例子 。</p>
*
* @author zxc
* 2022/2/17
*/
public class AESECBDemo {
private static final int KEY_LENGTH_16 = 16;
private static final int KEY_LENGTH_32 = 32;
private static final String AES_ECB_PKCS_5_PADDING = "AES/ECB/PKCS5Padding";
private static final String AES = "AES";
public static void main(String[] args) throws Exception {
//加密key长度需要为16位或32位
String key = "11112222333344441111222233334444";
//待加密内容
String content = "hello world";
System.out.println("加密前的内容:" + content);
AESECBDemo aesEncodeDemo = new AESECBDemo();
String encrypted = aesEncodeDemo.encryptByECB(key, content);
System.out.println("加密后的内容:" + encrypted);
String decrypt = aesEncodeDemo.decryptByECB(key, encrypted);
System.out.println("解密后的内容:" + decrypt);
}
/**
* AES ECB模式加密
*
* @param key 加密的秘钥
* @param content 待加密内容
* @return 加密后的内容
* @throws Exception 异常信息
*/
public String encryptByECB(String key, String content) throws Exception {
checkKey(key);
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), AES);
//AES/ECB/PKCS5Padding 格式为 "算法/模式/补码方式"
Cipher cipher = Cipher.getInstance(AES_ECB_PKCS_5_PADDING);
//设置加密模式,加密的key
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
//加密
byte[] bytes = cipher.doFinal(content.getBytes());
//base64编码进行二次加密
return new BASE64Encoder().encode(bytes);
}
/**
* 检查key是否合法
*
* @param key {@link String}秘钥信息
* @throws Exception 异常信息
*/
private void checkKey(String key) throws Exception {
if (key == null || key.length() != KEY_LENGTH_16 && key.length() != KEY_LENGTH_32) {
throw new Exception("加密秘钥不正确");
}
}
/**
* AES ECB模式解密
*
* @param key 加密的秘钥
* @param encrypt 加密后的内容
* @return 解密后的内容
* @throws Exception 异常信息
*/
public String decryptByECB(String key, String encrypt) throws Exception {
checkKey(key);
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), AES);
//AES/ECB/PKCS5Padding 格式为 "算法/模式/补码方式"
Cipher cipher = Cipher.getInstance(AES_ECB_PKCS_5_PADDING);
//设置为解密模式,解密的key
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
//base64解密
byte[] decodeBuffer = new BASE64Decoder().decodeBuffer(encrypt);
//aes解密
byte[] bytes = cipher.doFinal(decodeBuffer);
return new String(bytes);
}
}
输出内容如下
加密前的内容:hello world
加密后的内容:nzsEN6YiSOmh2RQgFTLJqQ==
解密后的内容:hello world
AES CBC模式
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
/**
* <p> AES CBC模式例子。</p>
*
* @author zxc
* 2022/2/17
*/
public class AESCBCDemo {
private static final int KEY_LENGTH_16 = 16;
private static final int KEY_LENGTH_32 = 32;
private static final String AES = "AES";
private static final String AES_CBC_NO_PADDING = "AES/CBC/NoPadding";
public static void main(String[] args) throws Exception {
//加密key长度需要为16位或32位
String key = "1111222233334444";
//偏移矢量,必须为16位
String iv = "5555666677778888";
//待加密内容
String content = "hello world";
System.out.println("加密前的内容:" + content);
AESCBCDemo aescbcDemo = new AESCBCDemo();
String encrypted = aescbcDemo.encryptByCBC(key, content, iv);
System.out.println("加密后的内容:" + encrypted);
String decrypt = aescbcDemo.decryptByCBC(key, encrypted, iv);
System.out.println("解密后的内容:" + decrypt);
}
/**
* AES ECB模式加密
*
* @param key 加密的秘钥
* @param content 待加密内容
* @param iv 偏移矢量
* @return 加密后的内容
* @throws Exception 异常信息
*/
public String encryptByCBC(String key, String content, String iv) throws Exception {
checkKey(key);
checkIV(iv);
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), AES);
//AES/ECB/PKCS5Padding 格式为 "算法/模式/补码方式"
Cipher cipher = Cipher.getInstance(AES_CBC_NO_PADDING);
//加密内容长度必须要为blockSize的整数倍
int blockSize = cipher.getBlockSize();
int contentLength = content.getBytes().length;
if (contentLength % blockSize != 0) {
contentLength = contentLength + (blockSize - (contentLength % blockSize));
}
byte[] newBytes = new byte[contentLength];
System.arraycopy(content.getBytes(), 0, newBytes, 0, content.getBytes().length);
//偏移矢量
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes());
//设置加密模式,加密的key,偏移矢量
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
//加密
byte[] bytes = cipher.doFinal(newBytes);
//base64编码进行二次加密
return new BASE64Encoder().encode(bytes);
}
/**
* 检查key是否合法
*
* @param key {@link String}秘钥信息
* @throws Exception 异常信息
*/
private void checkKey(String key) throws Exception {
if (key == null || key.length() != KEY_LENGTH_16 && key.length() != KEY_LENGTH_32) {
throw new Exception("加密秘钥不正确");
}
}
/**
* 检查偏移矢量是否合法
*
* @param iv {@link String} 偏移矢量
* @throws Exception 异常信息
*/
private void checkIV(String iv) throws Exception {
if (iv == null || iv.length() != KEY_LENGTH_16) {
throw new Exception("偏移矢量不正确,必须为16位");
}
}
/**
* AES ECB模式解密
*
* @param key 加密的秘钥
* @param encrypt 加密后的内容
* @param iv 偏移矢量
* @return 解密后的内容
* @throws Exception 异常信息
*/
public String decryptByCBC(String key, String encrypt, String iv) throws Exception {
checkKey(key);
checkIV(iv);
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), AES);
//AES/ECB/PKCS5Padding 格式为 "算法/模式/补码方式"
Cipher cipher = Cipher.getInstance(AES_CBC_NO_PADDING);
//偏移矢量
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes());
//设置为解密模式,解密的key,偏移矢量
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
//base64解密
byte[] decodeBuffer = new BASE64Decoder().decodeBuffer(encrypt);
//aes解密
byte[] bytes = cipher.doFinal(decodeBuffer);
return new String(bytes);
}
}