本文最后更新于 2026-07-08T16:59:23+08:00
学习路径借鉴: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接口
Srializable是Java提供的序列化接口,它是一个空接口
1 2 3 4 5
| package java.io;
public interface Serializable{ }
|
定义了一个公共接口Serializable,并且这个接口没有任何方法,在java中成为标记接口
Serializable用来标识当前类可以被ObjectOutputStream序列化,以及被ObjectInputStream 反序列化
ObjectOutputStream
Java提供的对象序列化的工具类,专门用于把对象转换成字节流,并写出去。这个“写出去”听起来怪怪的但就是这个意思,就是将序列化后的字节利用FileOutputStream之类的函数写到文件、内存或者网络中。简言之,ObjectOutputStream就是负责把对象序列化成一串字节,然后塞进某个输出流,至于这个输出流具体是哪个就不由ObjectOutputStream决定。
Java提供的对象反序列化工具类,专门用于将字节六还原成对象,并读回来。有了对之前“写出去”的理解,那这个“读回来”也就很简单了,可以把它理解成“写出去”的反义,ObjectInputStream就是从字节流中读取数据,并还原成 Java 对象。
writeObject
用于将对象的状态写入ObjectOutputStream,用于序列化
Serializable接口的基本使用
其实就是对ObjectOutputStream和ObjectInputStream的使用。
通过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); }
public static void main(String[] args) throws Exception{ Person person = new Person("aa",22);
serialize(person);
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)); 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

这个是对于序列化的对象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()无参构造

从执行结果可以看到,如果要序列化的对象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 + '}'; } 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);
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); } }
|

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