目录
为什么需要序列化?
序列化与反序列化基础案例
Serializable 接口
序列化对象
反序列化对象
Java 反序列化漏洞
readObject()
- 序列化:将对象的状态信息转换为可以存储或传输的形式的过程,即将对象转换为字节序列。
- 反序列化:是序列化的逆过程,即将字节序列恢复为对象。
为什么需要序列化?
- 对象持久化:将对象的状态保存到文件中,以便在程序终止运行后能够重新加载这些对象。
- 深拷贝:通过序列化一个对象到一个字节流,然后反序列化它到一个新的对象实例,可以实现对象的深拷贝。
- 网络传输:在网络中传输对象时,需要将对象序列化为字节序列,然后在接收端反序列化以恢复对象。
序列化与反序列化基础案例
Serializable 接口
Serializable 接口是java.io包中的一个标记接口(marker interface),它没有定义任何方法。当一个类实现了Serializable接口时,它表明这个类的对象可以被序列化。即如果一个类的对象需要序列化,则这个类必须实现Serializable
- import java.io.Serializable 用于将java.io包中的Serializable接口引入到当前类中。
- serialVersionUID 用于在序列化和反序列化过程中确保版本兼容性。
定义一个可序列化的类
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L; // 用于版本控制
private String name;
private int age;
// 构造函数
public User(String name, int age) {
this.name = name;
this.age = age;
}
// Getter 和 Setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
// toString 方法方便打印对象信息
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
序列化对象
将 User 类的对象序列化为一个文件
import java.io.FileOutputStream; //用于向文件写入数据
import java.io.IOException; //抛出异常
import java.io.ObjectOutputStream; //将对象序列化为字节序列并写入到输出流中
public class SerializeDemo {
public static void serialize(User user, String filePath) {
try (FileOutputStream fileOut = new FileOutputStream(filePath); //创建一个FileOutputStream对象fileOut,用于向指定的文件路径写入数据。
ObjectOutputStream out = new ObjectOutputStream(fileOut)) { //基于fileOut创建了一个ObjectOutputStream对象out,这个对象用于 将对象序列化为字节序列并写入到fileOut中。
out.writeObject(user); // 将User对象序列化到文件
System.out.printf("Serialized data is saved in %s%n", filePath);
} catch (IOException i) {
i.printStackTrace();
}
}
public static void main(String[] args) {
User user = new User("John Doe", 30);
serialize(user, "user.ser"); // 序列化User对象到user.ser文件
}
}
文件后缀 .ser 一种约定俗成的命名方式,有助于开发者识别文件的用途和内容。实际上,你可以使用任何扩展名来保存序列化后的数据,只要确保在反序列化时能够正确地指定文件路径即可。但是,为了保持代码的清晰性和易读性,建议使用像 .ser、.dat、.obj 等能够表明文件内容的扩展名。
Java 序列化将对象的状态信息转换为二进制格式的字节序列,这些字节序列被存储在文件中(如 user.ser
)。这些二进制数据具有以下特点:
- 非文本格式:内容是以二进制形式存储的,不是人类可读的文本。
- 包含对象状态:包含了序列化对象的所有非瞬态、非静态字段的当前值。
- 元数据:可能还包含了关于对象类的元数据信息,以便在反序列化时能够重新构造对象。
- 版本控制(可选):如果类定义了
serialVersionUID
,则这个版本号也会被序列化,用于版本兼容性检查。 - 平台依赖:虽然 Java 序列化是 Java 平台的一部分,但序列化数据的格式可能会受到 JVM 实现和版本的影响,因此在跨平台或跨版本时需要谨慎。
- 安全性问题:由于序列化机制涉及到 Java 的反射和类加载,因此可能会受到安全攻击,如反序列化漏洞。
- 性能影响:序列化/反序列化过程可能对性能有一定影响,特别是在处理大量数据或频繁进行序列化操作时。
反序列化对象
从文件中反序列化User对象
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class DeserializeDemo {
public static User deserialize(String filePath) {
User user = null;
try (FileInputStream fileIn = new FileInputStream(filePath);
ObjectInputStream in = new ObjectInputStream(fileIn)) {
user = (User) in.readObject(); // 从文件读取并反序列化User对象
} catch (IOException | ClassNotFoundException i) {
i.printStackTrace();
}
return user;
}
public static void main(String[] args) {
User user = deserialize("user.ser"); // 从user.ser文件反序列化User对象
if (user != null) {
System.out.println(user); // 打印反序列化后的User对象,输出 User{name='John Doe', age=30}
//User类重写了toString方法,以便能够打印出对象的详细信息
}
}
}
Java 反序列化漏洞
Java反序列化漏洞通常涉及将不可信的数据源(如用户输入或网络传输的数据)反序列化为Java对象时,如果反序列化的类包含了不安全的代码(如执行系统命令、访问敏感资源等),则可能引发严重的安全问题。
下面是一个简化的例子,包含了一个不安全的可序列化类和一个触发反序列化漏洞的客户端代码。
readObject()
在Java中,readObject(java.io.ObjectInputStream in) 方法是一个特殊的方法,它用于在对象反序列化过程中被自动调用。这个方法通常定义在实现了 java.io.Serializable 接口的类中,用于在对象从流中读取(即反序列化)时执行特定的操作或恢复对象的状态。
不安全的可序列化类
在这个 readObject 方法中,首先调用了 in.defaultReadObject(),这是非常重要的,因为它负责读取并恢复对象的字段值(即序列化过程中保存的字段状态)。如果不调用这个方法,对象的字段将不会被正确反序列化。
package demo;
import java.io.Serializable;
public class EvilObject implements Serializable {
private static final long serialVersionUID = 1L;
// 当反序列化时调用
private void readObject(java.io.ObjectInputStream in) throws java.io.IOException, ClassNotFoundException {
in.defaultReadObject();
// 不安全的代码:执行系统命令
try {
java.lang.Runtime.getRuntime().exec("calc"); // 在Windows上打开计算器
} catch (Exception e) {
e.printStackTrace();
}
}
}
序列化对象并存储在evil.ser 文件中
package demo;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
public class SerializeEvilObject {
public static void main(String[] args) {
try (FileOutputStream fileOut = new FileOutputStream("evil.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
EvilObject obj = new EvilObject();
out.writeObject(obj);
} catch (Exception e) {
e.printStackTrace();
}
}
}
当受害者运行DeserializeEvilObject类时,它会读取evil.ser文件并尝试反序列化数据。在这个过程中,EvilObject的readObject方法会被调用,从而执行calc命令
package demo;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
public class DeserializeEvilObject {
public static void main(String[] args) {
try (FileInputStream fileIn = new FileInputStream("evil.ser");
ObjectInputStream in = new ObjectInputStream(fileIn)) {
// 反序列化时,会调用EvilObject的readObject方法,从而执行calc命令
Object obj = in.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
}