目录
一.网络编程基础
1. 概述
计算机网络是指两台或更多的计算机组成的网络,在同一个网络中,任意两台计算机都可以直接通信,因为所有计算机都需要遵循同一种网络协议。
那什么是互联网呢?互联网是网络的网络(internet
),即把很多计算机网络连接起来,形成一个全球统一的互联网。对某个特定的计算机网络来说,它可能使用网络协议ABC
,而另一个计算机网络可能使用网络协议XYZ
。如果计算机网络各自的通讯协议不统一,就没法把不同的网络连接起来形成互联网。因此,为了把计算机网络接入互联网,就必须使用TCP/IP
协议。
TCP/IP
协议泛指互联网协议,其中最重要的两个协议是TCP协议
和IP协议
。只有使用TCP/IP协议
的计算机才能够联入互联网,使用其他网络协议(例如NetBIOS
、AppleTalk
协议等)是无法联入互联网的。
中国四大主流网络体系是Chinanet
CERNET
cstnet
CHINAGBN
。
Chinanet
是邮电部门经营管理的基于Internet
网络技术的中国公用计算机互联网,是国际计算机互联网(Internet
)的一部分,是中国的Internet
骨干网。CERNET
中国教育和科研计算机网CERNET
是由国家投资建设,教育部负责管理,清华大学等高等学校承担建设和管理运行的全国性学术计算机互联网络。cstnet
1994年中国科学技术网CSTNET
首次实现和Internet
直接连接,同时建立了中国最高域名服务器,标志着中国正式接入Internet
。ChinaGBN
(China Golden Bridge Network)也称做中国国家公用经济信息通信网。它是中国国民经济信息化的基础设施,是建立金桥工程的业务网,支持金关、金税、金卡等“金”字头工程的应用。
2. IP地址
在互联网中,一个IP
地址用于唯一标识一个网络接口(Network Interface
)。一台联入互联网的计算机肯定有一个IP
地址,但也可能有多个IP
地址(多个网卡)。
IP
地址分为IPv4
和IPv6
两种。IPv4
采用32
位地址,类似101.202.99.12
,而IPv6
采用128
位地址,类似2001:0DA8:100A:0000:0000:1020:F2F3:1428
。IPv4
地址总共有232
个(大约42亿),而IPv6
地址则总共有2128
个(大约340万亿亿亿亿),IPv4
的地址目前已耗尽,而IPv6
的地址是根本用不完的。
IP地址又分为公网IP地址
和内网IP地址
。公网IP地址可以直接被访问,内网IP
地址只能在内网访问。内网IP
地址类似于:
-
192.168.x.x
10.x.x.x
有一个特殊的IP地址,称之为本机地址,它总是127.0.0.1
。
1707762444 = 0x65ca630c
= 65 ca 63 0c
= 101.202.99.12
每台计算机都需要正确配置IP地址
和子网掩码
,根据这两个就可以计算网络号,如果两台计算机计算出的网络号相同,说明两台计算机在同一个网络,可以直接通信。如果两台计算机计算出的网络号不同,那么两台计算机不在同一个网络,不能直接通信,它们之间必须通过路由器或者交换机这样的网络设备间接通信,我们把这种设备称为网关
。
网关
的作用就是连接多个网络,负责把来自一个网络的数据包发到另一个网络,这个过程叫路由。所以,一台计算机的一个网卡会有3个关键配置:
-
IP
地址,例如:10.0.2.15
- 子网掩码,例如:
255.255.255.0
- 网关的IP地址,例如:
10.0.2.2
有一个特殊的本机域名localhost
,它对应的IP地址总是本机地址127.0.0.1
。
在Windows
操作系统中,可以通过ipconfig
命令查看本地主机的IP
地址:
InetAddress localIPAddress = InetAddress.getLocalHost();
System.out.println("本地主机IP:" + localIPAddress.getHostAddress());
System.out.println("本地主机名称:" + localIPAddress.getHostName());
如果想检查当前主机与目标主机之间的网络是否通畅,可以使用ping
命令来进行测试:
在Java
中,如果需要测试网络是否通畅,可以使用Runtime
对象exec()
执行ping
命令:
Process process = Runtime.getRuntime().exec("ping 192.168.254.162");
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line = null;
while((line = reader.readLine()) != null) {
System.out.println(line);
}
3. 域名
因为直接记忆IP地址非常困难,所以我们通常使用域名
访问某个特定的服务。域名解析服务器DNS
负责把域名
翻译成对应的IP
,客户端再根据IP地址访问服务器。
用nslookup
可以查看域名对应的IP地址:
4. 网络模型
由于计算机网络从底层的传输到高层的软件设计十分复杂,要合理地设计计算机网络模型,必须采用分层模型,每一层负责处理自己的操作。OSI
(Open System Interconnect)网络模型是ISO
组织定义的一个计算机互联的标准模型,注意它只是一个定义,目的是为了简化网络各层的操作,提供标准接口便于实现和维护。这个模型从上到下依次是:
- 应用层,提供应用程序之间的通信;
- 表示层:处理数据格式,加解密等等;
- 会话层:负责建立和维护会话;
- 传输层:负责提供端到端的可靠传输;
- 网络层:负责根据目标地址选择路由来传输数据;
- 数据链路层和物理层:负责把数据进行分片并且真正通过物理网络传输,例如,无线网、光纤等。
互联网实际使用的TCP/IP
模型并不是对应到OSI
的7层模型
,而是大致对应OSI
的5层模型
:
5. 常用协议
IP协议
是一个分组交换协议,它不保证可靠传输。而TCP协议
是传输控制协议,它是面向连接
的协议,支持可靠传输
和双向通信
。TCP协议
是建立在IP协议之上的,简单地说,IP协议只负责发数据包,不保证顺序和正确性,而TCP协议负责控制数据包传输,它在传输数据之前需要先建立连接,建立连接后才能传输数据,传输完后还需要断开连接。TCP协议之所以能保证数据的可靠传输,是通过接收确认
、超时重传
这些机制实现的。并且,TCP协议允许双向通信,即通信双方可以同时发送和接收数据。
TCP协议也是应用最广泛的协议,许多高级协议都是建立在TCP协议之上的,例如HTTP
、SMTP
等。
UDP协议
(User Datagram Protocol)是一种数据报文协议,它是无连接协议
,不保证可靠传输
。因为UDP
协议在通信前不需要建立连接,因此它的传输效率比TCP
高,而且UDP
协议比TCP
协议要简单得多。选择UDP
协议时,传输的数据通常是能容忍丢失的,例如,一些语音视频通信的应用会选择UDP
协议。
6. 小结
计算机网络: 软件体系: cs Client/server bs Browser/server 网络编程三要素 1.IP地址: ip:IPV4:32位地址长度,每八位一组,分成四组 x-0~255 ip:IPV6:128位地址长度,每十六位一组,分成八组 x-0~65535 2001:0DA8::1020:F2F3:1428 2.端口号:应用程序在设备中的唯一标识,端口号由二个字节0~65535,0-1023端口号(被广大服务商占用),1024以上可用 3.协议: TCP/UDP: TCP:面向连接的协议 UDP:无连接的协议
二.TCP编程
1. 什么是Socket?
在开发网络应用程序的时候,会遇到Socket
这个概念。Socket
是一个抽象概念,一个应用程序通过一个Socket
来建立一个远程连接,而Socket
内部通过TCP/IP
协议把数据传输到网络。
┌───────────┐ ┌───────────┐
│Application│ │Application│
├───────────┤ ├───────────┤
│ Socket │ │ Socket │
├───────────┤ ├───────────┤
│ TCP │ │ TCP │
├───────────┤ ┌──────┐ ┌──────┐ ├───────────┤
│ IP │<────>│Router│<─────>│Router│<────>│ IP │
└───────────┘ └──────┘ └──────┘ └───────────┘
Socket
、TCP
和部分IP
的功能都是由操作系统提供的,不同的编程语言只是提供了对操作系统调用的简单的封装。例如:Java
提供的几个Socket
相关的类就封装了操作系统提供的接口:ServerSocket
类、Socket
类。
为什么需要Socket
进行网络通信?因为仅仅通过IP
地址进行通信是不够的,同一台计算机同一时间会运行多个网络应用程序,例如浏览器、QQ、邮件客户端等。当操作系统接收到一个数据包的时候,如果只有IP
地址,它没法判断应该发给哪个应用程序,所以,操作系统抽象出Socket
接口,每个应用程序需要各自对应到不同的Socket
,数据包才能根据Socket
正确地发到对应的应用程序。
一个Socket
就是由IP
地址和端口号(范围是0~65535
)组成,可以把Socket
简单理解为IP
地址+端口号。端口号总是由操作系统分配,它是一个0
~65535
之间的数字,其中,小于1024
的端口属于特权端口,需要管理员权限,大于1024
的端口可以由任意用户的应用程序打开。
所以,如果需要与指定主机进行通信,完整的通信地址是由一个IP
地址+端口号组成:
101.202.99.2:1201
101.202.99.2:1304
101.202.99.2:15000
使用Socket
进行网络编程时,本质上就是两个进程之间的网络通信。其中一个进程必须充当服务器端,它会主动监听某个指定的端口,另一个进程必须充当客户端,它必须主动连接服务器的IP
地址和指定端口,如果连接成功,服务器端和客户端就成功地建立了一个TCP
连接,双方后续就可以随时发送和接收数据。
因此,当Socket
连接成功地在服务器端和客户端之间建立后:
- 对服务器端来说:它的
Socket
是指定的IP
地址和指定的端口号; - 对客户端来说:它的
Socket
是它所在计算机的IP
地址和一个由操作系统分配的随机端口号。
2. 服务器端
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(6666); // 监听指定端口
System.out.println("server is running...");
while (true) {
Socket sock = ss.accept();
// 使用Socket流进行网络通信
// ...
System.out.println("connected from " + sock.getRemoteSocketAddress());
}
}
}
服务器端通过下述代码,在指定端口6666
监听。这里我们没有指定IP
地址,表示在计算机的所有网络接口上进行监听。
ServerSocket ss = new ServerSocket(6666);
如果ServerSocket
监听成功,我们就使用一个无限循环来处理客户端的连接,注意到代码ss.accept()
表示每当有新的客户端连接进来后,就返回一个Socket
实例,这个Socket
实例就是用来和刚连接的客户端进行通信的。
while (true) {
Socket sock = ss.accept();
System.out.println("connected from " + sock.getRemoteSocketAddress());
}
如果没有客户端连接进来,accept()
方法会阻塞并一直等待。如果有多个客户端同时连接进来,ServerSocket
会把连接扔到队列里,然后一个一个处理。对于Java
程序而言,只需要通过循环不断调用accept()
就可以获取新的连接。
3. 客户端
相比服务器端,客户端程序就要简单很多。一个典型的客户端程序如下:
public class Client {
public static void main(String[] args) throws IOException {
// 连接指定服务器和端口
Socket sock = new Socket("localhost", 6666);
// 使用Socket流进行网络通信
// ...
// 关闭
sock.close();
System.out.println("disconnected.");
}
}
客户端程序通过下述代码,连接到服务器端,注意上述代码的服务器地址是"localhost
",表示本机地址,端口号是6666
。如果连接成功,将返回一个Socket
实例,用于后续通信。
Socket sock = new Socket("localhost", 6666);
4. Socket流
当Socket
连接创建成功后,无论是服务器端,还是客户端,我们都使用Socket
实例进行网络通信。因为TCP
是一种基于流的协议,因此,Java
标准库使用InputStream
和OutputStream
来封装Socket
的数据流,这样我们使用Socket
的流,和普通IO
流类似:
// 用于读取网络数据:
InputStream in = sock.getInputStream();
// 用于写入网络数据:
OutputStream out = sock.getOutputStream();
写入网络数据时,必须要调用flush()方法。如果不调用flush(),我们很可能会发现,客户端和服务器都收不到数据,这并不是Java标准库的设计问题,而是我们以流的形式写入数据的时候,并不是一写入就立刻发送到网络,而是先写入内存缓冲区,直到缓冲区满了以后,才会一次性真正发送到网络,这样设计的目的是为了提高传输效率。如果缓冲区的数据很少,而我们又想强制把这些数据发送到网络,就必须调用flush()强制把缓冲区数据发送出去。
5. 小结
使用Java进行TCP
编程时,需要使用Socket
模型:
-
- 服务器端用
ServerSocket
监听指定端口; - 客户端使用
Socket(InetAddress, port)
连接服务器; - 服务器端用
accept()
接收连接并返回Socket
实例; - 双方通过
Socket
打开InputStream
/OutputStream
读写数据; - 服务器端通常使用多线程同时处理多个客户端连接,利用线程池可大幅提升效率;
flush()
方法用于强制输出缓冲区到网络。
- 服务器端用
三.UDP编程
1. 概述
和TCP
编程相比,UDP
编程就简单得多,因为UDP
没有创建连接,数据包也是一次收发一个,所以没有流的概念。
在Java
中使用UDP
编程,仍然需要使用Socket
,因为应用程序在使用UDP
时必须指定网络接口(IP地址
)和端口号。注意:UDP
端口和TCP
端口虽然都使用0
~65535
,但他们是两套独立的端口,即一个应用程序用TCP
协议占用了端口1234
,不影响另一个应用程序用UDP
协议占用端口1234
。
2. 服务器端
在服务器端,使用UDP
也需要监听指定的端口。Java提供了DatagramSocket
来实现这个功能,代码如下:
DatagramSocket ds = new DatagramSocket(6666); // 监听指定端口
while (true) { // 无限循环
// 数据缓冲区:
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
ds.receive(packet); // 收取一个UDP数据包
// 收取到的数据存储在buffer中,由packet.getOffset(), packet.getLength()指定起始位置和长度
// 将其按UTF-8编码转换为String:
String s = new String(packet.getData(), packet.getOffset(), packet.getLength(), StandardCharsets.UTF_8);
// 发送数据:
byte[] data = "ACK".getBytes(StandardCharsets.UTF_8);
packet.setData(data);
ds.send(packet);
}
服务器端首先使用如下语句在指定的端口监听UDP
数据包:
DatagramSocket ds = new DatagramSocket(6666);
如果没有其他应用程序占据这个端口,那么代表监听成功。为了能够反复处理数据,我们使用一个死循环来处理收到的UDP
数据包:
while (true) { // 死循环
}
要接收一个UDP
数据包,需要准备一个byte[]
缓冲区,并通过DatagramPacket
实现接收:
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
ds.receive(packet);
假设我们收取到的是一个String
,那么,通过DatagramPacket
返回的packet.getOffset()
和packet.getLength()
确定数据在缓冲区的起止位置:
String s = new String(packet.getData(), packet.getOffset(), packet.getLength(), StandardCharsets.UTF_8);
当服务器收到一个DatagramPacket
后,通常必须立刻回复一个或多个UDP
包,因为客户端地址在DatagramPacket
中,每次收到的DatagramPacket
可能是不同的客户端,如果不回复,客户端就收不到任何UDP
包。
发送UDP
包也是通过DatagramPacket
实现的:
byte[] data = ...
packet.setData(data);
ds.send(packet);
3. 客户端
和服务器端相比,客户端使用UDP
时,只需要直接向服务器端发送UDP
包,然后接收返回的UDP
包:
DatagramSocket ds = new DatagramSocket();
ds.setSoTimeout(1000);
ds.connect(InetAddress.getByName("localhost"), 6666); // 连接指定服务器和端口
// 发送:
byte[] data = "Hello".getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length);
ds.send(packet);
// 接收:
byte[] buffer = new byte[1024];
packet = new DatagramPacket(buffer, buffer.length);
ds.receive(packet);
String resp = new String(packet.getData(), packet.getOffset(), packet.getLength());
ds.disconnect();
客户端打开一个DatagramSocket
使用以下代码:
DatagramSocket ds = new DatagramSocket();
ds.setSoTimeout(1000);
ds.connect(InetAddress.getByName("localhost"), 6666);
4. 小结
- 使用
UDP
协议通信时,服务器和客户端双方无需建立连接; - 服务器端用
DatagramSocket(port)
监听端口; - 客户端使用
DatagramSocket.connect()
指定远程地址和端口; - 双方通过
receive()
和send()
读写数据; DatagramSocket
没有IO
流接口,数据被直接写入byte[]
缓冲区;
案例:
从磁盘拿出一张照片,客户端发出去,服务端接收,并保存。返回给客户端一个上传成功。(判断下大小2mb,并且必须.png结尾)
Server
package homework;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;
import java.util.UUID;
public class Server {
public static void main(String[] args) {
try (ServerSocket ss = new ServerSocket(9999)) {
System.out.println("服务器已启动,等待客户端连接...");
while (true) {
Socket socket = ss.accept();
String uuid = UUID.randomUUID().toString().substring(0, 4);
try (OutputStream os = new FileOutputStream(uuid + ".png");
InputStream is = socket.getInputStream();
OutputStream sos = socket.getOutputStream()
) {
// System.out.println( uuid + ".png");
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
System.out.println("Arrays.toString(buffer)");
os.write(buffer, 0, len);
}
// os.flush(); // 确保数据写入
sos.write("上传成功".getBytes());
// System.out.println("文件接收完成:" + uuid + ".png");
}
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Client
package homework;
import java.io.*;
import java.net.Socket;
public class client1 {
public static void main(String[] args) throws IOException {
String path = "D:\\30f.jpg";
InputStream in = new FileInputStream(path);
if (path.endsWith(".png") && in.available() < 1024 * 1024 * 2) {
try (
Socket s = new Socket("127.0.0.1", 9999);
OutputStream os = s.getOutputStream();
) {
System.out.println("已连接到服务器");
// 不能大于2mb
byte[] b = new byte[1024];
int len;
while ((len = in.read(b)) != -1) {
os.write(b, 0, len);
}
s.shutdownOutput();
// s.shutdownOutput();
InputStream is = s.getInputStream();
byte[] bytes = new byte[1024];
int len1;
len1 = is.read(bytes);
System.out.println(new String(bytes, 0, len1));
} catch (Exception e) {
e.printStackTrace();
}
} else {
System.out.println("文件格式错误或文件大小要小于2M");
}
}
}
四.加密算法
1. 什么是编码?
ASCII
码就是一种编码,字母A
的编码是十六进制的0x41
,字母B
是0x42
,以此类推:
字母
A
B
C
D
…
因为ASCII
编码最多只能有127
个字符,要想对更多的文字进行编码,就需要用占用2
个字节的Unicode
或者3
个字节的UTF-8
。例如:中文的"中"字使用Unicode
编码就是0x4e2d
,UTF-8
编码是0xe4b8ad
。
汉字 |
Unicode编码 |
UTF-8编码 |
中 |
0x4e2d |
0xe4b8ad |
文 |
0x6587 |
0xe69687 |
编 |
0x7f16 |
0xe7bc96 |
码 |
0x7801 |
0xe7a081 |
2. Base64编码
URL
编码是对字符进行编码,表示成%xx
的形式,而Base64
编码是对二进制数据进行编码,表示成文本格式。
Base64
编码可以把任意长度的二进制数据变为纯文本,并且纯文本内容中且只包含指定字符内容:A
~Z
、a
~z
、0
~9
、+
、/
、=
。它的原理是把3
字节的二进制数据按6bit
一组,用4
个整数表示,然后查表,把整数用索引对应到字符,得到编码后的字符串。
6
位整数的范围总是0
~63
,所以,能用64
个字符表示:字符A
~Z
对应索引0
~25
,字符a
~z
对应索引26
~51
,字符0
~9
对应索引52
~61
,最后两个索引62
、63
分别用字符+
和/
表示。
举个例子:3
个byte
数据分别是e4
、b8
、ad
,按6bit
分组得到十六进制39
、0b
、22
和2d
,分别对应十进制57
、11
、34
、45
,通过索引计算结果为5Lit4
┌───────────────┬───────────────┬───────────────┐
│ e4 │ b8 │ ad │
└───────────────┴───────────────┴───────────────┘
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│1│1│1│0│0│1│0│0│1│0│1│1│1│0│0│0│1│0│1│0│1│1│0│1│二进制
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
┌───────────┬───────────┬───────────┬───────────┐
│ 39 │ 0b │ 22 │ 2d │十六进制
└───────────┴───────────┴───────────┴───────────┘
┌───────────┬───────────┬───────────┬───────────┐
│ 57 │ 11 │ 34 │ 45 │十进制
└───────────┴───────────┴───────────┴───────────┘
┌───────────┬───────────┬───────────┬───────────┐
│ 5 │ L │ i │ t │十进制
└───────────┴───────────┴───────────┴───────────┘
在Java
中,二进制数据就是byte[]
数组。Java
标准库提供了Base64
来对byte[]
数组进行编解码:
public class Main {
public static void main(String[] args) {
byte[] input = new byte[] { (byte) 0xe4, (byte) 0xb8, (byte) 0xad };
String b64encoded = Base64.getEncoder().encodeToString(input);
System.out.println(b64encoded);
}
}
编码后得到字符串结果:5Lit
。要对这个字符使用Base64
解码,仍然用Base64
这个类:
public class Main {
public static void main(String[] args) {
byte[] output = Base64.getDecoder().decode("5Lit");
System.out.println(Arrays.toString(output)); // [-28, -72, -83]
}
}
因为标准的Base64
编码会出现+
、/
和=
,所以不适合把Base64
编码后的字符串放到URL
中。一种针对URL
的Base64
编码可以在URL
中使用的Base64
编码,它仅仅是把+
变成-
,/
变成_
:
public class Main {
public static void main(String[] args) {
// 原始字节内容
byte[] input = new byte[] { 0x01, 0x02, 0x7f, 0x00 };
// 分别使用两种方式进行编码
String b64Encode = Base64.getEncoder().encodeToString(input);
String b64UrlEncoded = Base64.getUrlEncoder().encodeToString(input);
// 替换“+、/和=”
System.out.println(b64Encode);
System.out.println(b64UrlEncoded);
// 分别使用两种方式进行重新解码
byte[] output1 = Base64.getDecoder().decode(b64Encode);
byte[] output2 = Base64.getUrlDecoder().decode(b64UrlEncoded);
// 结果完全一致
System.out.println(Arrays.toString(output1));
System.out.println(Arrays.toString(output2));
}
}
Base64
编码的目的是把二进制数据变成文本格式,这样在很多文本中就可以处理二进制数据。例如,电子邮件协议就是文本协议,如果要在电子邮件中添加一个二进制文件,就可以用Base64
编码,然后以文本的形式传送。
Base64
编码的缺点是传输效率会降低,因为它把原始数据的长度增加了1/3。和URL
编码一样,Base64
编码是一种编码算法,不是加密算法。
如果把Base64
的64
个字符编码表换成32
个、48
个或者58
个,就可以使用Base32
编码,Base48
编码和Base58
编码。字符越少,编码的效率就会越低。
3. 小结
URL
编码和Base64
编码都是编码算法,它们不是加密算法;
URL
编码的目的是把任意文本数据编码为%
前缀表示的文本,便于浏览器和服务器处理;
Base64
编码的目的是把任意二进制数据编码为文本,但编码后数据量会增加1/3.
五.哈希算法
1. 概述
哈希算法(Hash
)又称摘要算法(Digest
),它的作用是:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。
哈希算法最重要的特点就是:
- 相同的输入一定得到相同的输出;
- 不同的输入大概率得到不同的输出。
所以,哈希算法的目的:为了验证原始数据是否被篡改。
Java
字符串的hashCode()
就是一个哈希算法,它的输入是任意字符串,输出是固定的4
字节int
整数:
"hello".hashCode(); // 0x5e918d2
"hello, java".hashCode(); // 0x7a9d88e8
"hello, bob".hashCode(); // 0xa0dbae2f
两个相同的字符串永远会计算出相同的hashCode
,否则基于hashCode
定位的HashMap
就无法正常工作。这也是为什么当我们自定义一个class
时,覆写equals()
方法时我们必须正确覆写hashCode()
方法。
2. 哈希碰撞
哈希碰撞是指:两个不同的输入得到了相同的输出。
例如:
"AaAaAa".hashCode(); // 0x7460e8c0
"BBAaBB".hashCode(); // 0x7460e8c0
"通话".hashCode(); // 0x11ff03
"重地".hashCode(); // 0x11ff03
碰撞能不能避免?答案是不能。碰撞是一定会出现的,因为输出的字节长度是固定的,String
的hashCode()
输出是4
字节整数,最多只有4294967296
种输出,但输入的数据长度是不固定的,有无数种输入。所以,哈希算法是把一个无限的输入集合映射到一个有限的输出集合,必然会产生碰撞。
碰撞不可怕,我们担心的不是碰撞,而是碰撞的概率,因为碰撞概率的高低关系到哈希算法的安全性。一个安全的哈希算法必须满足:
- 碰撞概率低;
- 不能猜测输出。
不能猜测输出是指:输入的任意一个bit
的变化会造成输出完全不同,这样就很难从输出反推输入(只能依靠暴力穷举)。
假设一种哈希算法有如下规律:
hashA("java001") = "123456"
hashA("java002") = "123457"
hashA("java003") = "123458"
那么很容易从输出123459
反推输入,这种哈希算法就不安全。安全的哈希算法从输出是看不出任何规律的:
hashB("java001") = "123456"
hashB("java002") = "580271"
hashB("java003") = ???
3. 常用哈希算法
哈希算法,根据碰撞概率,哈希算法的输出长度越长,就越难产生碰撞,也就越安全。
常用的哈希算法有:
算法 |
输出长度(位) |
输出长度(字节) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Java
标准库提供了常用的哈希算法,通过统一的接口进行调用。以MD5
算法为例,看看如何对输入内容计算哈希:
import java.security.MessageDigest;
public class main {
public static void main(String[] args) {
// 创建一个MessageDigest实例:
MessageDigest md = MessageDigest.getInstance("MD5");
// 反复调用update输入数据:
md.update("Hello".getBytes("UTF-8"));
md.update("World".getBytes("UTF-8"));
// 16 bytes: 68e109f0f40ca72a15e05cc22786f8e6
byte[] results = md.digest();
StringBuilder sb = new StringBuilder();
for(byte bite : results) {
sb.append(String.format("%02x", bite));
}
System.out.println(sb.toString());
}
}
使用MessageDigest
时,我们首先根据哈希算法获取一个MessageDigest
实例,然后,反复调用update(byte[])
输入数据。当输入结束后,调用digest()
方法获得byte[]
数组表示的摘要,最后,把它转换为十六进制的字符串。
运行上述代码,可以得到输入HelloWorld
的MD5
是68e109f0f40ca72a15e05cc22786f8e6
。
4. 哈希算法的用途
4.1. 校验下载文件
因为相同的输入永远会得到相同的输出,因此,如果输入被修改了,得到的输出就会不同。我们在网站上下载软件的时候,经常看到下载页显示的MD5哈希值:
如何判断下载到本地的软件是原始的、未经篡改的文件?我们只需要自己计算一下本地文件的哈希值,再与官网公开的哈希值对比,如果相同,说明文件下载正确,否则,说明文件已被篡改。
4.2. 存储用户密码
哈希算法的另一个重要用途是存储用户口令。如果直接将用户的原始口令存放到数据库中,会产生极大的安全风险:
- 数据库管理员能够看到用户明文口令;
- 数据库数据一旦泄漏,黑客即可获取用户明文口令。
username |
password |
bob |
123456789 |
alice |
sdfsdfsdf |
tim |
justdoit |
不存储用户的原始口令,那么如何对用户进行认证?方法是存储用户口令的哈希,例如,MD5
。在用户输入原始口令后,系统计算用户输入的原始口令的MD5
并与数据库存储的MD5
对比,如果一致,说明口令正确,否则,口令错误。
因此,数据库存储用户名和口令的表内容应该像下面这样:
username |
password |
bob |
25f9e794323b453885f5181f1b624d0b |
alice |
73a90acaae2b1ccc0e969709665bc62f |
tim |
19f9f30bd097d4c066d758fb01b75032 |
这样一来,数据库管理员看不到用户的原始口令。即使数据库泄漏,黑客也无法拿到用户的原始口令。想要拿到用户的原始口令,必须用暴力穷举的方法,一个口令一个口令地试,直到某个口令计算的MD5
恰好等于指定值。
使用哈希口令时,还要注意防止彩虹表攻击。
什么是彩虹表呢?上面讲到了,如果只拿到MD5
,从MD5
反推明文口令,只能使用暴力穷举的方法。然而黑客并不笨,暴力穷举会消耗大量的算力和时间。但是,如果有一个预先计算好的常用口令和它们的MD5
的对照表,这个表就是彩虹表。如果用户使用了常用口令,黑客从MD5
一下就能反查到原始口令:
常用口令 |
MD5 |
hello123 |
f30aa7a662c728b7407c54ae6bfd27d1 |
12345678 |
25d55ad283aa400af464c76d713c07ad |
passw0rd |
bed128365216c019988915ed3add75fb |
19700101 |
570da6d5277a646f6552b8832012f5dc |
… |
… |
wbjxxmy |
11d7a82f45f6a176fd9d5c100ccab40a |
这就是为什么不要使用常用密码,以及不要使用生日作为密码的原因。
当然,我们也可以采取特殊措施来抵御彩虹表攻击:对每个口令额外添加随机数,这个方法称之为加盐(salt
):
digest = md5(salt + inputPassword)
经过加盐处理的数据库表,内容如下:
username |
salt |
password |
bob |
H1r0a |
a5022319ff4c56955e22a74abcc2c210 |
alice |
7$p2w |
e5de688c99e961ed6e560b972dab8b6a |
tim |
z5Sk9 |
1eee304b92dc0d105904e7ab58fd2f64 |
5. SHA-1
SHA-1
也是一种哈希算法,它的输出是160 bits
,即20
字节。SHA-1
是由美国国家安全局开发的,SHA
算法实际上是一个系列,包括SHA-0
(已废弃)、SHA-1
、SHA-256
、SHA-512
等。
在Java
中使用SHA-1
,和MD5
完全一样,只需要把算法名称改为"SHA-1
":
import java.security.MessageDigest;
public class main {
public static void main(String[] args) {
// 创建一个MessageDigest实例:
MessageDigest md = MessageDigest.getInstance("SHA-1");
// 反复调用update输入数据:
md.update("Hello".getBytes("UTF-8"));
md.update("World".getBytes("UTF-8"));
// 20 bytes: db8ac1c259eb89d4a131b253bacfca5f319d54f2
byte[] results = md.digest();
StringBuilder sb = new StringBuilder();
for(byte bite : results) {
sb.append(String.format("%02x", bite));
}
System.out.println(sb.toString());
}
}
类似的,计算SHA-256
,我们需要传入名称"SHA-256
",计算SHA-512
,我们需要传入名称"SHA-512
"。Java
标准库支持的所有哈希算法可以在这里查到。
6. 小结
- 哈希算法可用于验证数据完整性,具有防篡改检测的功能;
- 常用的哈希算法有
MD5
、SHA-1
等; - 用哈希存储口令时要考虑彩虹表攻击。
例题:
从本地获取一张图片,对这个图片进行md加密
package jiamisuanfa;
import java.io.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.UUID;
public class Demo06 {
public static void main(String[] args) throws IOException, NoSuchAlgorithmException {
//题目:
// 从本地获取一张图片,对这个图片进行md加密
InputStream is = new FileInputStream(new File("D:\\30f.jpg"));
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] message = new byte[1024];
String salt = UUID.randomUUID().toString().substring(0,6);
while (is.read(message) != -1) {
md.update(message);
}
byte[] bytes1 = md.digest();
System.out.println("没有加颜值的字符数组:"+ Arrays.toString(bytes1));
System.out.println("没有颜值加密后的字符串:"+ byteToHex(bytes1));
md.update(salt.getBytes());
byte[] bytes = md.digest();
// System.out.println("未加密:"+ Arrays.toString(message));
System.out.println("加了颜值后的字符数组:"+ Arrays.toString(bytes));
System.out.println("有了颜值加密后的字符串:"+ byteToHex(bytes));
}
public static String byteToHex(byte[] bytes)
{
StringBuilder sb = new StringBuilder();
for (byte b:bytes){
sb.append(String.format("%02x",b));
}
return sb.toString();
}
}
扩展:添加盐值解决彩虹表攻击
package jiamisuanfa;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class Demo05 {
// 此次的盐值信息为:f2c01d
// 加密后的结果为:[-60,35,-131,2,-124,1,1,1......]
// 加密后的字符串为:c456888es6w6778ewr2
// 加密后长度为:..
// 添加盐值解决彩虹表攻击
public static void main(String[] args) throws NoSuchAlgorithmException {
// 1,创建MessageDigest实例
MessageDigest md = MessageDigest.getInstance("MD5");
// 2.使用md5进行信息加密
byte[] message = "我本将心像明月".getBytes();
md.update(message);
// 3,添加盐值,随机值
// String uuid = UUID.randomUUID().toString().substring(0,6);
// System.out.println("此次的盐值信息为:"+uuid);
// md.update(uuid.getBytes());
md.update("f2c01d".getBytes());
// 3.进行加密
byte[] bytes = md.digest();
System.out.println("加密后的字节数组:"+ Arrays.toString(bytes));
System.out.println("加密后的字符串:"+ byteToHex(bytes));
System.out.println("加密后的字符串长度:"+ byteToHex(bytes).length());
}
public static String byteToHex(byte[] bytes)
{
StringBuilder sb = new StringBuilder();
for (byte b:bytes){
sb.append(String.format("%02x",b));
}
return sb.toString();
}
}
RipeMD160,SHA-1,MD5 创建,统一封装成工具类:
package jiamisuanfa;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.util.Arrays;
import java.util.UUID;
public class HashTools {
// 创建信息摘要对象-成员变量
private static MessageDigest md;
private HashTools() {
}
// md5
public static String md5(String message) throws NoSuchAlgorithmException {
// 创建信息摘要对象
md = MessageDigest.getInstance("MD5");
return handle(message);
}
// sha-1
public static String sha1(String message) throws NoSuchAlgorithmException {
md = MessageDigest.getInstance("SHA-1");
return handle(message);
}
public static String ripeMD168(String message) throws NoSuchAlgorithmException {
// 安全注册中心
// 注册BouncyCastle提供的通知类对象BouncyCastleProvider
Security.addProvider(new BouncyCastleProvider());
md = MessageDigest.getInstance("RipeMD160");
return handle(message);
}
private static String handle(String message) {
byte[] bytes = message.getBytes();
md.update(bytes);
// 添加颜值
md.update(UUID.randomUUID().toString().substring(0,6).getBytes());
// 加密操作
byte[] bytes1 = md.digest();
// 转字符串
return byteToHex(bytes1);
}
public static String byteToHex(byte[] bytes){
StringBuilder sb = new StringBuilder();
for (byte b:bytes){
// 将每一个字节数字转成2位的16进制的字符串数字
sb.append(String.format("%02x",b));
}
return sb.toString();
}
public static byte[] stringTobytes(String message){
byte[] bytes = new byte[message.length()/2];
for (int i = 0; i < message.length(); i=i+2){
String subString = message.substring(i,i+2);
byte b = (byte) Integer.parseInt(subString,16);
bytes[i/2] = b;
}
return bytes;
}
}
package jiamisuanfa;
import java.security.NoSuchAlgorithmException;
public class Demo07 {
public static void main(String[] args) throws NoSuchAlgorithmException {
String message = "我本将心赵明月";
String md5 = HashTools.md5(message);
System.out.println("md5加密后的字符串:"+md5);
System.out.println(md5.length());// 16*2
String sha1 = HashTools.sha1(message);
System.out.println("sha1加密后的字符串:"+sha1);
String ripeMD168 = HashTools.ripeMD168(message);
System.out.println("ripeMD160加密后的字符串:"+ripeMD168);
System.out.println(ripeMD168.length());// 20*2
}
}
六.Hmac算法
1. 概述
在前面讲到哈希算法时,我们说,存储用户的哈希口令时,要加盐存储,目的就在于抵御彩虹表攻击。我们回顾一下哈希算法:digest = hash(input)
正是因为相同的输入会产生相同的输出,我们加盐的目的就在于,使得输入有所变化:
digest = hash(salt + input)
这个salt
可以看作是一个额外的“认证码”,同样的输入,不同的认证码,会产生不同的输出。因此,要验证输出的哈希,必须同时提供“认证码”。
Hmac
算法就是一种基于密钥的消息认证码算法,它的全称是Hash-based Message Authentication Code
,是一种更安全的消息摘要算法。
Hmac
算法总是和某种哈希算法配合起来用的。例如,我们使用MD5
算法,对应的就是Hmac MD5
算法,它相当于“加盐”的MD5
:HmacMD5 ≈ md5(secure_random_key, input)
因此,HmacMD5
可以看作带有一个安全的key
的MD5
。使用HmacMD5
而不是用MD5
加salt
,有如下好处:
HmacMD5
使用的key
长度是64
字节,更安全;Hmac
是标准算法,同样适用于SHA-1
等其他哈希算法;Hmac
输出和原有的哈希算法长度一致。
可见,Hmac
本质上就是把key
混入摘要的算法。验证此哈希时,除了原始的输入数据,还要提供key
。为了保证安全,我们不会自己指定key
,而是通过Java
标准库的KeyGenerator
生成一个安全的随机的key
。
下面是使用HmacMD5
的参考代码:
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
public class main {
public static void main(String[] args) throws NoSuchAlgorithmException, IllegalStateException, UnsupportedEncodingException, InvalidKeyException {
// 获取HmacMD5秘钥生成器
KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5");
// 产生秘钥
SecretKey secreKey = keyGen.generateKey();
// 打印随机生成的秘钥:
byte[] keyArray = secreKey.getEncoded();
StringBuilder key = new StringBuilder();
for(byte bite:keyArray) {
key.append(String.format("%02x", bite));
}
System.out.println(key);
// 使用HmacMD5加密
Mac mac = Mac.getInstance("HmacMD5");
mac.init(secreKey); // 初始化秘钥
mac.update("HelloWorld".getBytes("UTF-8"));
byte[] resultArray = mac.doFinal();
StringBuilder result = new StringBuilder();
for(byte bite:resultArray) {
result.append(String.format("%02x", bite));
}
System.out.println(result);
}
}
和MD5
相比,使用HmacMD5
的步骤是:
- 通过名称
HmacMD5
获取KeyGenerator
实例; - 通过
KeyGenerator
创建一个SecretKey
实例; - 通过名称
HmacMD5
获取Mac
实例; - 用
SecretKey
初始化Mac实例; - 对
Mac
实例反复调用update(byte[])
输入数据; - 调用
Mac
实例的doFinal()
获取最终的哈希值。
我们可以用Hmac
算法取代原有的自定义的加盐算法,因此,存储用户名和口令的数据库结构如下:
username |
secret_key (64 bytes) |
password |
bob |
a8c06e05f92e...5e16 |
7e0387872a57c85ef6dddbaa12f376de |
alice |
e6a343693985...f4be |
c1f929ac2552642b302e739bc0cdbaac |
tim |
f27a973dfdc0...6003 |
af57651c3a8a73303515804d4af43790 |
有了Hmac
计算的哈希和SecretKey
,我们想要验证怎么办?这时,SecretKey
不能从KeyGenerator
生成,而是从一个byte[]
数组恢复:
// 原始密码
String password = "nhmyzgq";
// 通过"秘钥的字节数组",恢复秘钥
byte[] keyByteArray = {126, 49, 110, 126, -79, -5, 66, 34, -122, 123, 107, -63, 106, 100, -28, 67, 19, 23, 1, 23, 47, 63, 47, 109, 123, -111, -27, -121, 103, -11, 106, -26, 110, -27, 107, 40, 19, -8, 57, 20, -46, -98, -82, 102, -104, 96, 87, -16, 93, -107, 25, -56, -113, 12, -49, 96, 6, -78, -31, -17, 100, 19, -61, -58};
// 恢复秘钥
SecretKey key = new SecretKeySpec(keyByteArray,"HmacMD5");
// 加密
Mac mac = Mac.getInstance("HmacMD5");
mac.init(key);
mac.update(password.getBytes());
byte[] resultByteArray = mac.doFinal();
StringBuilder resultStr = new StringBuilder();
for(byte b : resultByteArray) {
resultStr.append(String.format("%02x", b));
}
System.out.println("加密结果:" + resultStr);
2. 小结
Hmac
算法是一种标准的基于密钥的哈希算法,可以配合MD5
、SHA-1
等哈希算法,计算的摘要长度和原摘要算法长度相同。
案例:
登录加密,和二次登录,获取key验证
package jiamisuanfa;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class Demo08 {
/*
密钥字节信息:[19, -3, 97, -104, -71, 103, -63, -72, -81, -62, 124, -104, -93, -19, -36, -53, -60, -88, -128, 23, 60, 99, -90, 74, -57, -81, 21, 56, -68, 110, 127, -95, -51, -21, -96, -85, -72, -23, -118, -49, -50, 122, -71, 52, -49, 94, 6, -89, 14, 46, -11, -107, 127, 115, 90, 54, -90, -98, -96, -60, 41, -83, -81, 6]
密钥字符串:13fd6198b967c1b8afc27c98a3eddccbc4a880173c63a64ac7af1538bc6e7fa1cdeba0abb8e98acfce7ab934cf5e06a70e2ef5957f735a36a69ea0c429adaf06
密钥字节信息长度:64
加密后的字节数组:[-24, 18, -16, 11, -83, -19, 35, 50, 4, 104, 109, 51, -91, -43, 16, -64]
加密后的字符串:e812f00baded233204686d33a5d510c0
加密后的字符串长度:16
*/
public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {
// // 产生密钥生成器对象
// KeyGenerator generator = KeyGenerator.getInstance("HmacMD5");
// // 通过密钥生成器生成密钥
// SecretKey key = generator.generateKey();
// // 获取密钥字节信息
// byte[] keyBytes = key.getEncoded();
// System.out.println("密钥字节信息:"+ Arrays.toString(keyBytes));
// System.out.println("密钥字符串:"+HashTools.byteToHex(keyBytes));
// System.out.println("密钥字节信息长度:"+ keyBytes.length);
// // 获取加密对象
// Mac mas = Mac.getInstance("HmacMD5");
// // 初始化key值
// mas.init(key);
// // 提供需要进行加密的信息添加进来
// mas.update("我本将心像明月".getBytes());
// // 加密操作
// byte[] bytes = mas.doFinal();
// System.out.println("加密后的字节数组:"+ Arrays.toString(bytes));
// System.out.println("加密后的字符串:"+ HashTools.byteToHex(bytes));
// System.out.println("加密后的字符串长度:"+ bytes.length);
// 登录
// 二次登录
// 密钥字符串转字节数组操作
String str = "13fd6198b967c1b8afc27c98a3eddccbc4a880173c63a64ac7af1538bc6e7fa1cdeba0abb8e98acfce7ab934cf5e06a70e2ef5957f735a36a69ea0c429adaf06";
byte[] keyBytes = HashTools.stringTobytes(str);
System.out.println("密钥字节信息:"+ Arrays.toString(keyBytes));
// 2.还原密钥key
SecretKey key = new SecretKeySpec(keyBytes, "HmacMD5");
// 再次加密
Mac mas = Mac.getInstance("HmacMD5");
// 初始化key值
mas.init(key);
// 提供需要进行加密的信息添加进来
mas.update("我本将心像明月".getBytes());
// 加密操作
byte[] bytes = mas.doFinal();
System.out.println("加密后的字节数组:"+ Arrays.toString(bytes));
System.out.println("加密后的字符串:"+ HashTools.byteToHex(bytes));
System.out.println("加密后的字符串长度:"+ bytes.length);
}
}
七.对称加密算法
1. 概述
对称加密算法就是传统的用一个秘钥进行加密和解密。例如,我们常用的WinZIP
和WinRAR
对压缩包的加密和解密,就是使用对称加密算法.
算法 |
密钥长度 |
工作模式 |
填充模式 |
DES |
56/64 |
ECB/CBC/PCBC/CTR/... |
NoPadding/PKCS5Padding/... |
AES |
128/192/256 |
ECB/CBC/PCBC/CTR/... |
NoPadding/PKCS5Padding/PKCS7Padding/... |
IDEA |
128 |
ECB |
PKCS5Padding/PKCS7Padding/... |
密钥长度直接决定加密强度,而工作模式和填充模式可以看成是对称加密算法的参数和格式选择。Java
标准库提供的算法实现并不包括所有的工作模式和所有填充模式。
最后,值得注意的是,DES
算法由于密钥过短,可以在短时间内被暴力破解,所以现在已经不安全了。
2. 使用AES加密
AES
算法是目前应用最广泛的加密算法。比较常见的工作模式是ECB
和CBC
。
2.1. ECB模式
ECB
模式是最简单的AES
加密模式,它需要一个固定长度的密钥,固定的明文会生成固定的密文。
我们先用ECB
模式加密并解密:
package jiamisuanfa;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class Demo09 {
public static void main(String[] args) throws IllegalBlockSizeException, NoSuchPaddingException, BadPaddingException, NoSuchAlgorithmException, InvalidKeyException {
// 加密
byte[] input = "我本将心像明月".getBytes();
byte[] keys = "1234567890abcdef".getBytes();
byte[] encodeBytes = encodeMessage(input, keys);
System.out.println("加密后的字符串:"+ HashTools.byteToHex(encodeBytes));
System.out.println("加密后的字符串长度:"+ encodeBytes.length);
System.out.println("加密后的结果:"+ Arrays.toString(encodeBytes));
// 解密
byte[] decodeBytes = decodeMessage(encodeBytes, keys);
System.out.println("解密后的字符串:"+ new String(decodeBytes));
System.out.println("解密后的字符串长度:"+ decodeBytes.length);
System.out.println("解密后的结果:"+ Arrays.toString(decodeBytes));
}
public static byte[] encodeMessage(byte[] input, byte[] keys) throws IllegalBlockSizeException, BadPaddingException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException {
// 1.读取对称加密对象,设置加密算法,工作模式,填充模式
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
// 2.加密准备密钥信息
SecretKeySpec key = new SecretKeySpec(keys, "AES");
// 3.初始化加密对象
cipher.init(Cipher.ENCRYPT_MODE, key);
// 4.执行加密操作
byte[] bytes = cipher.doFinal(input);
return bytes;
}
public static byte[] decodeMessage(byte[] input, byte[] keys) throws IllegalBlockSizeException, BadPaddingException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException {
// 1.读取对称加密对象,设置加密算法,工作模式,填充模式
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
// 2.加密准备密钥信息
SecretKeySpec key = new SecretKeySpec(keys, "AES");
// 3.初始化加密对象
cipher.init(Cipher.DECRYPT_MODE, key);
// 4.执行加密操作
byte[] bytes = cipher.doFinal(input);
return bytes;
}
}
Java
标准库提供的对称加密接口非常简单,使用时按以下步骤编写代码:
- 根据算法名称/工作模式/填充模式获取
Cipher
实例; - 根据算法名称初始化一个
SecretKey
实例,密钥必须是指定长度; - 使用
SerectKey
初始化Cipher
实例,并设置加密或解密模式; - 传入明文或密文,获得密文或明文。
2.2. CBC模式
ECB
模式是最简单的AES
加密模式,这种一对一的加密方式会导致安全性降低。所以,更好的方式是通过CBC
模式,它需要一个随机数作为IV
参数,这样对于同一份明文,每次生成的密文都不同:
package jiamisuanfa;
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
public class Demo11 {
public static void main(String[] args) throws InvalidAlgorithmParameterException, IllegalBlockSizeException, NoSuchPaddingException, BadPaddingException, NoSuchAlgorithmException, InvalidKeyException {
byte[] input = "我本将心像明月".getBytes();
byte[] keys = "1234567890ebadfj".getBytes();
byte[] encode = encodeMessage(input, keys);
System.out.println("加密后的信息为:"+ Arrays.toString( encode));
byte[] decode = decodeMessage(encode, keys);
System.out.println("解密后的信息为:"+ new String(decode));
}
private static byte[] encodeMessage(byte[] message,byte[] key) throws IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {
// 1.获取Cipher对象
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// 2.还原key
SecretKey keys = new SecretKeySpec(key, "AES");
// 3.准备iv偏移
SecureRandom sr = SecureRandom.getInstanceStrong();
byte[] bytes = sr.generateSeed(16);
IvParameterSpec iv = new IvParameterSpec(bytes);
// 4.初始化操作,设置加密模式,设置key,设置偏移量
cipher.init(Cipher.ENCRYPT_MODE, keys, iv);
// 5.加密
byte[] encode = cipher.doFinal(message);
return addByte(encode, bytes);
}
private static byte[] addByte(byte[] encode, byte[] iv) {
byte[] bytes = new byte[encode.length+iv.length];
System.arraycopy(encode,0,bytes,0,encode.length);
System.arraycopy(iv,0,bytes,encode.length,iv.length);
return bytes;
}
private static byte[] decodeMessage(byte[] decodeMessage,byte[] key) throws InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
// 1.获取Cipher对象
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// 2.还原key
SecretKey keys = new SecretKeySpec(key, "AES");
// 3.还原iv值
byte[] bytes = Arrays.copyOfRange(decodeMessage, decodeMessage.length-16, decodeMessage.length);
IvParameterSpec iv = new IvParameterSpec(bytes);
// 4.初始化操作,设置加密模式,设置key,设置偏移量
cipher.init(Cipher.DECRYPT_MODE, keys, iv);
// 5.解密
byte[] bytes1 = Arrays.copyOf(decodeMessage, decodeMessage.length-16);
return bytes1;
}
}
3. 小结
- 对称加密算法使用同一个密钥进行加密和解密,常用算法有
DES
、AES
和IDEA
等; - 密钥长度由算法设计决定,
AES
的密钥长度是128
/192
/256
位; - 使用对称加密算法需要指定算法名称、工作模式和填充模式。