Java原生序列化和反序列化

学习路径借鉴:https://github.com/Drun1baby/JavaSecurityLearning

学习视频网站:https://www.bilibili.com/video/BV16h411z7o9?spm_id_from=333.788.videopod.episodes&vd_source=525a280615063349bad5e187f6bfeec3

学习文章:https://blog.csdn.net/mocas_wang/article/details/107621010

前言

感觉很久之前就开始说要学java,但是一直被零零碎碎的事打乱。开始写第一篇java笔记之前找了很多关于java安全的学习思路,但还是一头雾水,看两个学长的博客貌似都是从java反射开始学的,又看到都很推荐白日梦组长的b站教程,就打算从这个系列从开始的也就是java序列化和反序列化开始学。

先学习了一些java基础语法,能看懂代码了,就开始学习反序列化和序列化

什么是反序列化和序列化

Java序列化就是指把Java对象转换为字节学列的过程

Java反序列化就是指把字节序列恢复为Java对象的过程

为什么需要序列化和反序列化

当两个Java进程进行通信时,通过Java序列化和反序列化可以实现进程间的对象传送,也就是说发送方需要把这个Java对象转换为字节序列,然后在网络上传送,另外,接收方需要从字节序列中恢复出Java对象

常见用途:

  • 对象持久化:把对象保存到文件、数据库中
  • 网络传输:在远程调用(RMI、RPC)、消息队列中传输对象
  • 分布式系统:在不同 JVM 之间传递对象状态
  • 缓存:把对象存入 Redis、磁盘缓存等

序列化实现

说实话感觉前面就是一些了解,我也没学的很深入简单知道一下就好了,这里开始才是重点要学的。

只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列(不是的话则会抛出异常)

Serializable接口

SrializableJava提供的序列化接口,它是一个空接口

1
2
3
4
5
package java.io;

public interface Serializable{

}

定义了一个公共接口Serializable,并且这个接口没有任何方法,在java中成为标记接口

Serializable用来标识当前类可以被ObjectOutputStream序列化,以及被ObjectInputStream 反序列化

ObjectOutputStream

Java提供的对象序列化的工具类,专门用于把对象转换成字节流,并写出去。这个“写出去”听起来怪怪的但就是这个意思,就是将序列化后的字节利用FileOutputStream之类的函数写到文件、内存或者网络中。简言之,ObjectOutputStream就是负责把对象序列化成一串字节,然后塞进某个输出流,至于这个输出流具体是哪个就不由ObjectOutputStream决定。

ObjectInputStream

Java提供的对象反序列化工具类,专门用于将字节六还原成对象,并读回来。有了对之前“写出去”的理解,那这个“读回来”也就很简单了,可以把它理解成“写出去”的反义,ObjectInputStream就是从字节流中读取数据,并还原成 Java 对象。

writeObject

用于将对象的状态写入ObjectOutputStream,用于序列化

Serializable接口的基本使用

其实就是对ObjectOutputStreamObjectInputStream的使用。

通过ObjectOutputStream将需要序列化的数据写入到流中,并通过其包装FileOutStream将数据写入文件中或者包装ByteArrayOutStream 将数据写入到内存中。

通过ObjectInputStream将数据从磁盘FileInputStream或者内存ByteArrayInputStream读取处理啊后转化为指定的对象即可。

示例代码(对象有序列化接口)

Person类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.io.Serializable;

public class Person implements Serializable {

private String name;

private int age;

public Person(){

}
public Person(String name, int age){
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
",age=" + age +
'}';
}


}

Perosn类中实现了一个接口Serializable,如果没有这个接口则无法实现序列化

序列化

SerializerTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.*;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializeTest {
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);//ObjectOutputStream类中定义一个文件输出流
}

public static void main(String[] args) throws Exception{
Person person = new Person("aa",22);

serialize(person);//调用序列化方法,将对象变成字节写进ser.bin

System.out.println(person);

}
}

ObjectOutputStream类中定义一个文件输出流,写入序列化流头信息,准备吧对象变成字节,可以输出到一个文件中,这个文件就是通过FileOutputStream创建的ser.bin,调用其中的writeObject方法就可以把这个对象序列化——>这一步就是利用ObjectOutputStream达到将对象序列化成字节流并写出来的目的

throws IOException用于声明此处可能会抛出IO异常,注意这里要和“处理异常”区分,它不等于处理异常,也不阻止异常发生。

反序列化

UnserializeTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class UnserializeTest {
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois= new ObjectInputStream(new FileInputStream(Filename));//ObjectInputStream代表对象输入流
Object obj = ois.readObject();
return obj;
}

public static void main(String[] args) throws Exception{
Person person = (Person) unserialize("ser.bin");
System.out.println(person);
}
}

这里就是通过ObjectInputStream从文件中反序列化对象,它的readObejct()方法从一个源输入流中读取字节序列,实现反序列化

需要注意的是静态成员变量是不能被序列化,序列化时针对对象属性的,而静态成员变量时属于类的,序列化不能针对类

transient标识的对象成员变量不参与序列化且返回null

Person类中的name对象添加transient标识,则在反序列化中name返回的就是null

image-20260616161024951

这个是对于序列化的对象person实现了序列化接口,但是如果序列化的对象的父类没有实现序列化接口,那么在反序列化时是会调用对象的无参构造方法的。

示例代码(对象无序列化接口)

Animal类(未实现序列化接口的父类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Animal {
private String color;

public Animal() {//没有无参构造将会报错
System.out.println("调用 Animal 无参构造");
}

public Animal(String color) {
this.color = color;

System.out.println("调用 Animal 有 color 参数的构造");
}

@Override
public String toString() {
return "Animal{" +
"color='" + color + '\'' +
'}';
}
}

这个类就是不能被序列化,它就一个未实现Serializable接口的父类,它的子类被序列化时,必须通过Animal的无参构造方法创建父类部分。

BlackCat类(可执行序列化的子类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.io.Serializable;

public class BlackCat extends Animal implements Serializable {
private static final long serialVersionUID = 1L;
private String name;

//下面进行无参构造
public BlackCat() {
super();
System.out.println("调用黑猫的无参构造");
}

public BlackCat(String color, String name) {
super(color);
this.name = name;
System.out.println("调用黑猫有 color 参数的构造");
}

@Override
public String toString() {
return "BlackCat{" +
"name='" + name + '\'' +super.toString() +'\'' +
'}';
}
}

private static final long serialVersionUID = 1L;用于表示类的版本,防止java.io.InvalidClassException只要可能被序列化就应该写

1
2
3
4
public BlackCat() {
super();
System.out.println("调用黑猫的无参构造");
}

定义了无参构造,允许new BlackCat();,保证反序列化时Animal能被正确初始化

1
2
3
4
5
public BlackCat(String color, String name) {
super(color);
this.name = name;
System.out.println("调用黑猫有 color 参数的构造");
}

定义了有参构造,还未实现调用。

一开始写到这里的”有参构造“我还有点疑惑,怎么前面又是”无参构造“这里又是”有参构造“。事实上,这两个并不冲突,上面说的”无参构造“是在进行反序列化的时候调用父类的无参构造,而这里说的”有参构造“是指在new的时候调用的

但写到这里又有一个疑惑点就是这串代码中没有new怎么也叫有参构造,这里需要注意的就是此处只是定义了有参构造,还没到调用构造方法。

SuperMain类(测试类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.io.*;

public class SuperMain {
private static final String FILE_PATH = "./super.bin";

public static void main(String[] args) throws Exception {
serializeAnimal();
deserializeAnimal();
}

private static void serializeAnimal() throws Exception {
BlackCat black = new BlackCat("black", "我是黑猫");
System.out.println("序列化前:"+black.toString());
System.out.println("=================开始序列化================");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
oos.writeObject(black);
oos.flush();
oos.close();
}

private static void deserializeAnimal() throws Exception {
System.out.println("=================开始反序列化================");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
BlackCat black = (BlackCat) ois.readObject();
ois.close();
System.out.println(black);
}
}

序列化

1
BlackCat black = new BlackCat("black", "我是黑猫");

调用BlackCat()Animal(),调用了有参构造

反序列化

1
BlackCat black = (BlackCat) ois.readObject();

调用Animal()无参构造

image-20260616161028823

从执行结果可以看到,如果要序列化的对象black的父类Animal没有实现序列化接口,那么在反序列化时会调用对应的无参构造方法,用于重新初始化父类的属性。这里能直观看到额就是color反序列化后的值,因为Animal没有实例化接口,其对应的color属性就不会被序列化,因此反序列化得到的color的值就为null

ReadObject方法存在的安全问题

只要服务端接受的类中存在ReadObject方法,那么这个方法会自动调用。就是序列化的这个对象传到服务器上,如果服务器将其反序列化,那其中的代码一定会执行

可能的形式

  • 入口类的readObject直接调用危险方法
  • 入口类的参数中含可控类。该类存在危险方法,readObject方法触发时调用
  • 入口类参数中包含可控类,该类又调用其他危险方法的类,readObject方法触发时调用(这个就是通俗的讲类似于套娃,一个类里包含一个类,直到最后才触发到危险的方法)
  • 构造函数/静态代码块等类加载时隐式执行

例如,在之前的示例代码中插入命令执行

person类

在person类中加入命令执行代码calc,对他进行序列化,然后对他反序列化,在反序列化时就会执行readObject代码,然后就会执行calc代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class Person implements Serializable {

private transient String name;

private int age;

public Person(){

}
public Person(String name, int age){
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
",age=" + age +
'}';
}

//重写readObject方法
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException{
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");

}

}

private transient String name;这里transient修饰的变量name是不会被反序列化的

transient修饰的变量不会被反序列化,在对象反序列化时,该字段会被赋予默认值

  • 数值类型:0
  • boolean:false
  • 引用类型:null

Serialize类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.*;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializeTest {
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static void main(String[] args) throws Exception{
Person person = new Person("aa",22);

serialize(person);

//System.out.println(person);
serialize(person);

}
}

Unserialize类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class UnserializeTest {
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois= new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

public static void main(String[] args) throws Exception{
Person person = (Person) unserialize("ser.bin");
System.out.println(person);
}
}

image-20260616161032980

实现反序列化攻击的条件

  • 入口类重写readObject,调用常见的函数,参数类型广泛,最好JDK自带
  • 调用链,根据相同名称,相同类型不停调用
  • 执行类,执行攻击的地方(最重要的部分)

Java原生序列化和反序列化
https://colourful228.github.io/2026/06/16/Java序列化和反序列化/
作者
Colourful
发布于
2026年6月16日
更新于
2026年7月8日
许可协议