网络编程-加密算法

发布于:2025-08-02 ⋅ 阅读:(8) ⋅ 点赞:(0)

目录

一.网络编程基础

1. 概述

2. IP地址

 3. 域名

 4. 网络模型

5. 常用协议

6. 小结

二.TCP编程

1. 什么是Socket?

2. 服务器端

3. 客户端

4. Socket流

5. 小结

三.UDP编程

1. 概述

2. 服务器端

3. 客户端

4. 小结

案例:

四.加密算法

1. 什么是编码?

2. Base64编码

3. 小结

五.哈希算法

1. 概述

2. 哈希碰撞

3. 常用哈希算法

4. 哈希算法的用途

4.1. 校验下载文件

4.2. 存储用户密码

5. SHA-1

6. 小结

例题:

六.Hmac算法

1. 概述

2. 小结

案例:

登录加密,和二次登录,获取key验证

七.对称加密算法

1. 概述

2. 使用AES加密

2.1. ECB模式

2.2. CBC模式

3. 小结


一.网络编程基础

1. 概述

        计算机网络是指两台或更多的计算机组成的网络,在同一个网络中,任意两台计算机都可以直接通信,因为所有计算机都需要遵循同一种网络协议。

        那什么是互联网呢?互联网是网络的网络(internet),即把很多计算机网络连接起来,形成一个全球统一的互联网。对某个特定的计算机网络来说,它可能使用网络协议ABC,而另一个计算机网络可能使用网络协议XYZ。如果计算机网络各自的通讯协议不统一,就没法把不同的网络连接起来形成互联网。因此,为了把计算机网络接入互联网,就必须使用TCP/IP协议。

   TCP/IP协议泛指互联网协议,其中最重要的两个协议是TCP协议IP协议。只有使用TCP/IP协议的计算机才能够联入互联网,使用其他网络协议(例如NetBIOSAppleTalk协议等)是无法联入互联网的。

中国四大主流网络体系ChinanetCERNET cstnetCHINAGBN

  1. Chinanet是邮电部门经营管理的基于Internet网络技术的中国公用计算机互联网,是国际计算机互联网(Internet)的一部分,是中国的Internet骨干网。
  2. CERNET中国教育和科研计算机网CERNET是由国家投资建设,教育部负责管理,清华大学等高等学校承担建设和管理运行的全国性学术计算机互联网络。
  3. cstnet 1994年中国科学技术网CSTNET首次实现和Internet直接连接,同时建立了中国最高域名服务器,标志着中国正式接入Internet
  4. ChinaGBN(China Golden Bridge Network)也称做中国国家公用经济信息通信网。它是中国国民经济信息化的基础设施,是建立金桥工程的业务网,支持金关、金税、金卡等“金”字头工程的应用。

2. IP地址

        在互联网中,一个IP地址用于唯一标识一个网络接口(Network Interface)。一台联入互联网的计算机肯定有一个IP地址,但也可能有多个IP地址(多个网卡)。

  IP地址分为IPv4IPv6两种。IPv4采用32位地址,类似101.202.99.12,而IPv6采用128位地址,类似2001:0DA8:100A:0000:0000:1020:F2F3:1428IPv4地址总共有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. 网络模型

由于计算机网络从底层的传输到高层的软件设计十分复杂,要合理地设计计算机网络模型,必须采用分层模型,每一层负责处理自己的操作。OSIOpen System Interconnect)网络模型是ISO组织定义的一个计算机互联的标准模型,注意它只是一个定义,目的是为了简化网络各层的操作,提供标准接口便于实现和维护。这个模型从上到下依次是:

  • 应用层,提供应用程序之间的通信;
  • 表示层:处理数据格式,加解密等等;
  • 会话层:负责建立和维护会话;
  • 传输层:负责提供端到端的可靠传输;
  • 网络层:负责根据目标地址选择路由来传输数据;
  • 数据链路层和物理层:负责把数据进行分片并且真正通过物理网络传输,例如,无线网、光纤等。

互联网实际使用的TCP/IP模型并不是对应到OSI7层模型,而是大致对应OSI5层模型

5. 常用协议

   IP协议是一个分组交换协议,它不保证可靠传输。而TCP协议传输控制协议,它是面向连接的协议,支持可靠传输双向通信TCP协议是建立在IP协议之上的,简单地说,IP协议只负责发数据包,不保证顺序和正确性,而TCP协议负责控制数据包传输,它在传输数据之前需要先建立连接,建立连接后才能传输数据,传输完后还需要断开连接。TCP协议之所以能保证数据的可靠传输,是通过接收确认超时重传这些机制实现的。并且,TCP协议允许双向通信,即通信双方可以同时发送和接收数据。

        TCP协议也是应用最广泛的协议,许多高级协议都是建立在TCP协议之上的,例如HTTPSMTP等。

  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     │
└───────────┘      └──────┘       └──────┘      └───────────┘

SocketTCP和部分IP的功能都是由操作系统提供的,不同的编程语言只是提供了对操作系统调用的简单的封装。例如:Java提供的几个Socket相关的类就封装了操作系统提供的接口:ServerSocket类、Socket类。

为什么需要Socket进行网络通信?因为仅仅通过IP地址进行通信是不够的,同一台计算机同一时间会运行多个网络应用程序,例如浏览器、QQ、邮件客户端等。当操作系统接收到一个数据包的时候,如果只有IP地址,它没法判断应该发给哪个应用程序,所以,操作系统抽象出Socket接口,每个应用程序需要各自对应到不同的Socket,数据包才能根据Socket正确地发到对应的应用程序。

一个Socket就是由IP地址和端口号(范围是0~65535)组成,可以把Socket简单理解为IP地址+端口号。端口号总是由操作系统分配,它是一个065535之间的数字,其中,小于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标准库使用InputStreamOutputStream来封装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,字母B0x42,以此类推:

字母
A
B
C
D
…

         因为ASCII编码最多只能有127个字符,要想对更多的文字进行编码,就需要用占用2个字节的Unicode或者3个字节的UTF-8。例如:中文的"中"字使用Unicode编码就是0x4e2dUTF-8编码是0xe4b8ad

 

汉字

Unicode编码

UTF-8编码

0x4e2d

0xe4b8ad

0x6587

0xe69687

0x7f16

0xe7bc96

0x7801

0xe7a081

2. Base64编码

URL编码是对字符进行编码,表示成%xx的形式,而Base64编码是对二进制数据进行编码,表示成文本格式。

Base64编码可以把任意长度的二进制数据变为纯文本,并且纯文本内容中且只包含指定字符内容:A~Za~z0~9+/=。它的原理是把3字节的二进制数据按6bit一组,用4个整数表示,然后查表,把整数用索引对应到字符,得到编码后的字符串。

6位整数的范围总是0~63,所以,能用64个字符表示:字符A~Z对应索引0~25,字符a~z对应索引26~51,字符0~9对应索引52~61,最后两个索引6263分别用字符+/表示。

举个例子3byte数据分别是e4b8ad,按6bit分组得到十六进制390b222d,分别对应十进制57113445,通过索引计算结果为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中。一种针对URLBase64编码可以在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编码是一种编码算法,不是加密算法。

如果把Base6464个字符编码表换成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

碰撞能不能避免?答案是不能。碰撞是一定会出现的,因为输出的字节长度是固定的,StringhashCode()输出是4字节整数,最多只有4294967296种输出,但输入的数据长度是不固定的,有无数种输入。所以,哈希算法是把一个无限的输入集合映射到一个有限的输出集合,必然会产生碰撞。

碰撞不可怕,我们担心的不是碰撞,而是碰撞的概率,因为碰撞概率的高低关系到哈希算法的安全性。一个安全的哈希算法必须满足:

  • 碰撞概率低;
  • 不能猜测输出。

不能猜测输出是指:输入的任意一个bit的变化会造成输出完全不同,这样就很难从输出反推输入(只能依靠暴力穷举)。

假设一种哈希算法有如下规律:

hashA("java001") = "123456"
hashA("java002") = "123457"
hashA("java003") = "123458"

那么很容易从输出123459反推输入,这种哈希算法就不安全。安全的哈希算法从输出是看不出任何规律的:

hashB("java001") = "123456"
hashB("java002") = "580271"
hashB("java003") = ???

3. 常用哈希算法

哈希算法,根据碰撞概率,哈希算法的输出长度越长,就越难产生碰撞,也就越安全。

常用的哈希算法有:

算法

输出长度(位)

输出长度(字节)

MD5

128 bits

16 bytes

SHA-1

160 bits

20 bytes

RipeMD-160

160 bits

20 bytes

SHA-256

256 bits

32 bytes

SHA-512

512 bits

64 bytes

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[]数组表示的摘要,最后,把它转换为十六进制的字符串。

运行上述代码,可以得到输入HelloWorldMD568e109f0f40ca72a15e05cc22786f8e6


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-1SHA-256SHA-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. 小结

  • 哈希算法可用于验证数据完整性,具有防篡改检测的功能;
  • 常用的哈希算法有MD5SHA-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算法,它相当于“加盐”的MD5HmacMD5 ≈ md5(secure_random_key, input)

因此,HmacMD5可以看作带有一个安全的keyMD5。使用HmacMD5而不是用MD5salt,有如下好处:

  • 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的步骤是:

  1. 通过名称HmacMD5获取KeyGenerator实例;
  2. 通过KeyGenerator创建一个SecretKey实例;
  3. 通过名称HmacMD5获取Mac实例;
  4. SecretKey初始化Mac实例;
  5. Mac实例反复调用update(byte[])输入数据;
  6. 调用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算法是一种标准的基于密钥的哈希算法,可以配合MD5SHA-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. 概述

对称加密算法就是传统的用一个秘钥进行加密和解密。例如,我们常用的WinZIPWinRAR对压缩包的加密和解密,就是使用对称加密算法.

算法

密钥长度

工作模式

填充模式

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算法是目前应用最广泛的加密算法。比较常见的工作模式是ECBCBC

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标准库提供的对称加密接口非常简单,使用时按以下步骤编写代码:

  1. 根据算法名称/工作模式/填充模式获取Cipher实例;
  2. 根据算法名称初始化一个SecretKey实例,密钥必须是指定长度;
  3. 使用SerectKey初始化Cipher实例,并设置加密或解密模式;
  4. 传入明文或密文,获得密文或明文。
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. 小结

  • 对称加密算法使用同一个密钥进行加密和解密,常用算法有DESAESIDEA等;
  • 密钥长度由算法设计决定,AES的密钥长度是128/192/256位;
  • 使用对称加密算法需要指定算法名称、工作模式和填充模式。