文章目录
前言
本文包括:java序列化和反序列化的基础知识和案例演示,案例包括转化为文件字符输出流并保存成文件的方式和通过ByteArrayOutputStream保存为字节数组的方式进行序列化和反序列化,使用Binary工具查看序列化后的文件和数据含义解释。
一、基础知识
1.什么是序列化和反序列化?
Java序列化是指把Java对象转换为字节序列的过程,通过ObjectOutputStream 类中的的writeObject方法可以实现序列化。
Java反序列化是指把字节序列恢复为Java对象的过程,通过ObjectInputStream类的readObject方法进行反序列化。序列化和反序列化的过程都是基于字节流来完成的。
需注意的点有:
- 要进行序列化和反序列化则该类必须继承自java.io.Serializable接口(该类的全部属性也必须继承自Serializable接口),否则会抛出NotSerializableException报错,如下图所示。
- 而且所有非transient关键字修饰的属性必须是可序列化的。
- 类对象序列化之后不一定要保存成文件,也可以通过ByteArrayOutputStream保存为字节数组。
- 反序列化之后返回的数据类型为Object类型,如果要转化为序列化之前的类,需要进行强制类型转化。

2.为什么要序列化
因为java对象不只是存储在内存中,它还需要在网络中进行传输,而序列化就是将java对象的状态信息转换为一串由二进制字节组成的数组,这样就能让对象进行持久化存储或者在网络中传输了,以后有需要的时候也可以直接通过反序列化得到这个对象。
3.ObjectOutputStream 类
java.io.ObjectOutputStream 类的作用就是将Java对象的原始数据类型写出到文件中,以实现对象的持久存储。
一个对象要想序列化,必须满足以下两个条件:
- 该类必须实现 java.io.Serializable 接口,Serializable 是一个标记接口,不实现此接口的类将不会
进行序列化或反序列化,会抛出 NotSerializableException 。 - 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态 的,使用 transient 关键字修饰。
4.ObjectInputStream 类
在Java反序列化中,会调用被反序列化的ObjectInputStream类的readObject方法,当readObject方法被重写不当时就会产生反序列漏洞。
5.Java序列化算法
- 所有保存到磁盘的对象都有一个序列化编码号,可以自定义编写如: private static final long serialVersionUID = -3066949856415001911L;
- 当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。
- 如果此对象已经序列化过,则直接输出编号即可。
6.java类中serialVersionUID的作用
serialVersionUID适用于java序列化机制。简单来说,JAVA序列化的机制是通过判断类的serialVersionUID来验证的版本一致的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较。如果相同说明是一致的,可以进行反序列化,否则会出现反序列化版本一致的异常,即是InvalidCastException。
二、序列化和反序列化案例一
1)先创建一个java的包并在里面创建一个可序列化的User类,注意要实现Serializable接口。本文环境为:idea 2021.3版本,jdk为8。
package serializable;
import java.io.Serializable;
public class User implements Serializable {
private String name;
public void setName(String name){
this.name = name;
}
public String getName(){
return name;
}
}

2)编写测试代码,测试序列化和反序列化。
package serializable;
import java.io.*;
public class test {
public static void main(String[] args) throws IOException,ClassNotFoundException{
User user = new User();
user.setName("Serial");
// 测试序列化
// 创建文件字符输出流,保存到文件 本案例会生成serializable.dat文件
FileOutputStream fileOutputStream = new FileOutputStream("D:/notes/java/practise/java_serializable/src/serializable/file/serializable.dat");
// 将对象写入ObjectOutputStream中,再转化到FileOutputStream并输出到文件中
ObjectOutputStream oos = new ObjectOutputStream(fileOutputStream);
// 调用writeObject方法,序列化对象到文件serializable.dat中
oos.writeObject(user);
// 关闭流,否则再次write时容易出现文件空白的情况
oos.close();
// 从文件反序列化对象
// 创建一个ObjectInputStream输入流,读取文件serializable.dat的磁盘数据
// 创建一个FIleInutputStream
FileInputStream fileInputStream = new FileInputStream("D:/notes/java/practise/java_serializable/src/serializable/file/serializable.dat");
// 将FileInputStream封装到ObjectInputStream中,还需要进行一下类型转换,默认是Object类型
ObjectInputStream ois = new ObjectInputStream(fileInputStream);
// 使用ObjectInputStream中的readObject读取一个对象
User newUser = (User) ois.readObject();
// 关闭流,否则再次write时容易出现文件空白的情况
ois.close();
// 输出
System.out.println(newUser.getName());
}
}

3)使用Binary 查看生成的序列化文件,可以看到存储的对象数值Serial,开头的 AC ED 00 05 为序列化内容的特征。Binary工具可在以下链接下载:https://pc.qq.com/detail/5/detail_90225.html。
| 十六进制数 | 数据含义解释 |
|---|---|
| AC ED | 申明使用序列化协议 |
| 00 05 | 序列化协议版本 |
| 73 | 这是一个新对象 |
| 72 | 这里开始一个新的Class |
| 00 17 | Class名字为23个字节 |
| 02 | 标记号 |
| 00 04 | 包含域的个数:4 |

三、序列化和反序列化案例二
1)创建一个Employee.java文件,有一个成员变量被 transient 瞬态修饰,不能被反序列化。
package serializable;
import java.io.*;
public class Employee implements Serializable{
public String name;
public String address;
public int ages;
public transient int age;// transient瞬态修饰成员,不会被序列化
}

2)创建一个SerializeDemo.java文件进行序列化生成了一个ser.dat文件,将Employee对象写入到了ser.dat文件中。
package serializable;
import java.io.*;
public class SerializeDemo {
public static void main(String[] args) throws IOException {
Employee employee = new Employee();
employee.name = "zhangsan";
employee.age = 20;
employee.ages = 40;
employee.address = "shenzhen";
// 创建文件字符输出流并保存成文件 路径为将要生成的文件所在的路径,
// 本案例会生成ser.dat文件
// 将对象写入ObjectOutputStream中,再转化到FileOutputStream并输出到文件中
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("D:/notes/java/practise/java_serializable/src/serializable/file/ser.dat"));
// 写出对象
outputStream.writeObject(employee);
// 释放资源,关闭流,否则再次write时容易出现文件空白的情况
outputStream.close();
}
}

3)使用Binary工具查看生成的序列化文件。

4)进行反序列化从二进制文件中提取出对象。
package serializable;
import java.io.*;
public class DeserializeDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 创建反序列化流,创建一个ObjectInputStream输入流,读取文件ser.dat的磁盘数据
FileInputStream fileInputStream = new FileInputStream("D:/notes/java/practise/java_serializable/src/serializable/file/ser.dat");
ObjectInputStream inputStream = new ObjectInputStream(fileInputStream);
// 使用ObjectInputStream中的readObject读取一个对象
Object o = inputStream.readObject();
// 释放资源,关闭流,否则再次write时容易出现文件空白的情况
inputStream.close();
System.out.println(o);
}
}

四、序列化和反序列化案例三
1)之前都是文件的输入流输出流并且是引用的方式,这回写一个字节数组的案例,全都在一个文件中。创建一个Serialize的java文件。
package serializable;
import java.io.*;
public class Serialize implements Serializable {
// 必须实现Serializable接口,表明当前类可以被序列化。
// serialVersionUID不写的话,idea会自动生成,赋予每个类不同的序列化UID
private static final long serialVersionUID = -3066949856415001911L;
public String name;
public int age;
public transient String address;
// 序列化方式一
private void writeObject (ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
s.writeObject("This is the first way");
}
// 反序列化方式一
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
String s1 = (String) s.readObject();
System.out.println(s1);
}
// 序列化和反序列化
/* 过程就是:利用构造函数实例化Serialize类并赋值了name和age属性到serialize对,address因为有transient修饰,所以不会反序列化
再通过ObjectOutputStream将serialize对象序列化输出字节流到了byteArrayOutputStream
并且控制台输出了一下byteArrayOutputStream
然后通过ObjectInputStream与ByteArrayInputStream反序列化了byteArrayOutputStream字节流取到了之前序列化的对象
最后输出了之前赋值的name与age两个属性,并且address属性一直都没有被序列化
*/
public static void main(String[] args) throws IOException, ClassNotFoundException {
Serialize serialize = new Serialize();
serialize.name = "hanmeimei";
serialize.age = 20;
serialize.address = "asdf";
// 创建序列化流
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
// 写出对象
objectOutputStream.writeObject(serialize);
// 释放资源
objectOutputStream.close();
// 打印输出
System.out.println(byteArrayOutputStream);
// 创建反序列化流
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Serialize o = (Serialize) objectInputStream.readObject();
System.out.println( o.name+ "\n" + o.age );
}
}

2)在上面的案例方式一中我们直接使用了writeObject方法,这个特殊点就是Java允许我们在序列化对象的时候插入一些自定义数据,并且在反序列化的时候能够使用 readObject 进行读取。如果自定义了writeObject方法和readObject方法,java会在Serialize对象默认序列化之后又增加写入了一串字符串的序列化数据,并且默认反序列化之后会把这个字符串读出来打印到控制台,这也是方式一种没有打印输出语句却打印出来了的原因。
3)Java中ByteArrayOutputStream类的toByteArray()方法用于创建一个新分配的字节数组。新分配的字节数组的大小等于这个输出流的当前大小。此方法将缓冲区的有效内容复制到其中。
五、反序列化漏洞的基本原理和案例
1)在Java反序列化中,会调用被反序列化的readObject方法,当readObject方法被重写不当时产生漏洞。类似于PHP反序列化时会自动执行__wakeup方法一样。
2)一个小案例演示,实际环境中不会这么简单仅起演示作用。
package serializable;
import java.io.*;
public class Deserialization_1 { public static void main(String args[]) throws Exception{
// 序列化
// 定义mo对象
MyObject mo = new MyObject();
mo.name = "hello world";
// 创建一个包含对象进行反序列化信息的”object”数据文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/notes/java/practise/java_serializable/src/serializable/file/object.dat"));
// writeObject()方法将mo对象写入object文件
oos.writeObject(mo);
oos.close();
// 创建反序列化流,创建一个ObjectInputStream输入流,读取文件object.dat的磁盘数据
FileInputStream fileInputStream = new FileInputStream("D:/notes/java/practise/java_serializable/src/serializable/file/object.dat");
ObjectInputStream ois = new ObjectInputStream(fileInputStream);
// 使用ObjectInputStream中的readObject从磁盘中读取MyObject对象的值给objectFromDisk
MyObject objectFromDisk = (MyObject)ois.readObject();
// 打印name的值
System.out.println(objectFromDisk.name);
// 关闭流
ois.close();
}
public static class MyObject implements Serializable {
public String name;
//重写readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
//执行默认的readObject()方法
in.defaultReadObject();
//执行打开计算器程序命令
Runtime.getRuntime().exec("calc.exe");
}
}
}

3)上面案例造成反序列化漏洞就是因为重写了readObject方法,然后执行了 Runtime.getRuntime().exec(),defaultReadObject方法为ObjectInputStream中执行readObject后的默认执行方法。
本文详细介绍了Java序列化和反序列化的概念、应用场景、相关类的使用,以及如何通过ObjectOutputStream和ObjectInputStream进行操作。同时,文章通过多个案例展示了序列化到文件和字节数组的过程,并探讨了反序列化漏洞的原理。此外,还提醒了序列化时transient关键字的作用和序列化ID(serialVersionUID)的影响。
1052

被折叠的 条评论
为什么被折叠?



