序列化和反序列化概念

  • Java 序列化是指把 Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,如将 Java 对象转为一个 bin 文件或将一个 Java 对象转为一个 JSON 字符串,都是为了保存和不同服务之间对象的传输。
  • Java 反序列化是指把字节序列恢复为 Java 对象的过程。

序列化的应用场景

  • 序列化是指把一个 Java 对象变成二进制内容,本质上就是一个 byte[]数组。
  • 为什么要把 Java 对象序列化呢?因为序列化后可以把 byte[]保存到文件中,或者把 byte[]通过网络传输到远程,这样,就相当于把 Java对象存储到文件或者通过网络传输出去了。
  • 有序列化,就有反序列化,即把一个二进制内容(也就是 byte[]数组)变回 Java对象。有了反序列化,保存到文件中的 byte[]数组又可以“变回” Java对象,或者从网络上读取 byte[]并把它“变回”Java 对象。

序列化的过程

对象的序列化

一个对象要能序列化,必须实现一个特殊 java.io.Serializable接口,该接口是空接口,无需要实现的相应方法,它的定义如下:

1
2
public interface Serializable {
}

下面以序列化 Use 类为例:

1
2
3
4
5
6
7
8
public class User implements Serializable {
private String nickname;
private String username;
private String password;

// 省略无参和有参构造方法
// 省略 getter 和 setter
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class SerializeTest {
public static void main(String[] args) throws IOException {
// 1. 实例化对象并设置属性
User user = new User();
user.setNickname("熊先生");
user.setUsername("x1ongsec");
user.setPassword("123456");

// 2. 序列化对象到文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"));
oos.writeObject(user);
}
}

运行以上代码会生成 user.ser文件。

对象的反序列化

ObjectOutputStream相反,ObjectInputStream负责从一个字节流读取 Java对象。

1
2
3
4
5
6
7
public class UnserializeTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"));
Object obj = ois.readObject();
System.out.println(obj);
}
}

反序列化的核心原理

Java 序列化数据通常以 AC ED 00 05开头,其中 AC ED表示是 Java序列化文件的专属标识,00 05则表示序列化协议的版本号。

反序列化过程中的关键步骤:

  1. 类加载与校验:JVM 尝试加载字节流中指定的类
  2. 对象实例化:创建对象实例但不调用构造函数
  3. 属性恢复:按顺序恢复对象的属性值
  4. 自定义反序列化:如果类实现了自定义的 readObject()方法,则调用此方法

以下为演示过程:

序列化机制在还原对象时,会自动触发对象内部的特殊方法(如 readObject),而攻击者可通过构造恶意的序列化数据,让这些方法执行非预期的危险操作。

在被反序列化的类中定义私有的 readObject方法有且仅有一个参数,即 ObjectInputStream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class User implements Serializable { 
private String nickname;
private String username;
private String password;

// 该方法必须是无返回值、有且仅有一个参数ObjectInputStream
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
Runtime.getRuntime().exec("open -a Calculator");
}

// 省略无参和有参构造方法
// 省略 getter 和 setter
}

先对 User 类序列化获得相应的序列化二进制文件,接着进行反序列化。

1
2
3
4
5
6
7
public class UnserializeTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"));
Object obj = ois.readObject();
System.out.println(obj);
}
}

运行即可执行被反序列化类中的 readObject()方法。