JDK序列化机制及源码解读一:Serializable和Externalizable

前言:

为了Java反序列化漏洞打下深厚基础,特意学习总结了Java序列化机制及源码解读系列,便于分享和日后巩固。

主要记录以下四个方面:

1、Serializable和Externalizable,序列化接口

2、ObjectOutputStream对象输出流深入解读

3、ObjectInputStream对象输入流深入解读

4、拓展知识学习

初识序列化

首先认识下序列化。序列化就是把对象转换成字节流,便于保存在内存、文件、数据库中;反序列化即逆过程,把字节流还原成对象。Java序列化时会序列化该对象的类信息、属性及属性值,不能序列化对象的方法。

image-20200604162949988

先看下序列化用途:

(1)永久性保存对象,保存对象的字节序列到本地文件或者数据库中

(2)通过序列化以字节流的形式使对象在网络中进行传递和接收

(3)通过序列化在进程间传递对象

一些知识铺垫:

1、标记接口:Java中的标记接口(Marker Interface),又称标签接口(Tag Interface),具体是不包含任何方法的接口。在Java中,标记接口主要有以下两种目的:一是建立一个公共的父接口。比如EventListener接口,一个由几十个其它接口扩展的Java API,当一个接口继承了EventListener接口,JVM就知道该接口将要被用于一个事件的代理方案。同样的,你可以使用一个标记接口来建立一组接口的父接口。二是向一个类添加数据类型。这种情况是标记接口最初的目的,实现标记接口的类不需要定义任何接口方法(因为标记接口根本就没有方法),但是该类通过Java的多态性可以变成一个接口类型。

标记接口因为没有任何方法所以其本身并不“工作”,顾名思义,它只是将类标记为特定类型。在一些代码中可以检查标记是否存在,并根据这些信息进行一些操作,例如可以通过if (instance instanceof MyMarkerInterface) {...}来进行判断某个对象是否是标记类的实例化,然后再去进行一些操作。

如何序列化和反序列化?

一般序列化至少需要以下步骤:

  1. 创建一个类实现Serializable接口,并设置serialVersionUID(最好设置下,默认可不设置。设置方法可以百度IDEA设置serialVersionUID)
  2. 创建一个对象输出流ObjectOutputStream
  3. 调用对象输出流的writeObject方法把对象转换成字节流输出

一般反序列化至少需要以下步骤:

  1. 创建一个对象输入流ObjectInputStream
  2. 调用对象输入流的readObject方法把字节流转为对象

实例:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import java.io.*;

//要序列化的类必须实现Serializable
class CommandExec implements Serializable{
// 设置serialVersionUID
private static final long serialVersionUID = -6211228684695072792L;

//任意方法,可忽略
public void exec() {
String cmd = "calc";

try {
Process process = Runtime.getRuntime().exec(cmd);
InputStream is = process.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);

for(String content = br.readLine(); content != null; content = br.readLine()) {
System.out.println(content);
}
} catch (IOException var7) {
var7.printStackTrace();
}

}
}

/**
* 序列化测试:序列化和反序列化CommandExec实例
*/
public class Main {
public static void main(String[] args) throws Exception {
/* 序列化对象:由对象转为字节流 */
CommandExec commandExec1 = new CommandExec();
//创建 ObjectOutputStream 对象输出流,最终输出流到文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("e:\\test.txt")));
//写入对象到输出流
oos.writeObject(commandExec1);
System.out.println("对象序列化成功!");
oos.flush();
oos.close();

/* 反序列化:由字节流转为对象 */
// 创建 ObjectInputStream 对象输入流,从文件读取流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("e:\\test.txt")));
//读取对象
CommandExec commandExec2 = (CommandExec) ois.readObject();
System.out.println("对象反序列化成功!");
//调用对象的方法
commandExec2.exec();
ois.close();

}
}

执行结果:

image-20210629210351251

test.txt文件:

image-20210629210525001

除了java.io.Serializable,JDK还提供了另外一种原生序列化接口java.io.Externalizable,接下来深入学习这两种接口。

Serializable序列化接口

java.io.Serializable是一个标记接口,仅用于标识类的可序列化,它没有任何接口需要去实现。

这里拓展下:标记接口因为没有任何方法所以其本身并不“工作”,顾名思义,它只是将类标记为特定类型。在一些代码中可以检查标记是否存在,并根据这些信息进行一些操作,例如可以通过if (instance instanceof MyMarkerInterface) {...}来进行判断某个对象是否是标记类的实例化,然后再去进行一些操作。

重要知识点:

  1. java.io.Serializable仅用于标识类的可序列化,它没有任何方法或字段
  2. 序列化运行时将每个可序列化的类与称为serialVersionUID的版本号相关联, 因为可能这个类可能存在新旧版本, 所以使用该标记来标明。如果收发方的这个类版本不一致, 在反序列化 (deserialization) 的时候就会抛出InvalidClassException异常。其修饰符是static final long。注意写代码时可以用IDEA来自动生成serialVersionUID
  3. 只要实现了Serializable,它的所有子类都是可序列化的,子类会直接父类的继承writeObject和readObject,注意反序列化时不会调用父类的构造器
  4. 父类不可序列化,子类声明该接口,要想序列化父类信息子类必须重写writeObject和readObject方法,将可访问的父类信息序列化(原因:对象反序列化时,如果父类未实现序列化接口,则反序列出的对象会再次调用父类的构造函数来完成属于父类那部分内容的初始化)

Externalizable序列化接口

Externalizable接口是继承于Serializable接口的. 它仅定义了两个方法分别用于控制序列化和反序列化过程。通过Externalizable我们可以控制哪些对象及属性能够序列化,并且能够控制其按照代码出现顺序进行序列化。

1
2
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;。

实例:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import java.io.*;
import java.util.Date;

//https://blog.csdn.net/liwenshui322/article/details/47145191
//通过实现Externalizable接口练习控制对象序列化和反序列
/*首先,我们在序列化对象的时候,由于这个类实现了Externalizable 接口,在writeExternal()方法里定义了哪些属性可以序列化,
哪些不可以序列化,所以,对象在经过这里就把规定能被序列化的序列化保存文件,不能序列化的不处理,
然后在反序列的时候自动调用readExternal()方法,根据序列顺序挨个读取进行反序列,并自动封装成对象返回,然后在测试类接收,就完成了反序列
*/
public class TestExternalizable {
public static void main(String[] args) throws IOException, ClassNotFoundException {
TestExternalizable testExternalizable = new TestExternalizable();
testExternalizable.objToStream();
testExternalizable.streamToObj();


}

//序列化
public void objToStream() throws IOException {
MyExternalizable myExternalizable = new MyExternalizable(1,2,"3");
// .ser是Java序列化文件标准后缀
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("E:\\1.ser"));
objectOutputStream.writeObject(myExternalizable);
objectOutputStream.close();
}

//反序列化
public void streamToObj() throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("E:\\1.ser"));
MyExternalizable myExternalizable = (MyExternalizable) objectInputStream.readObject();
System.out.println(myExternalizable.toString());
objectInputStream.close();
}
}

class MyExternalizable implements Externalizable {
public int a = 0;
public int b = 0;
public String c = null;

public MyExternalizable(int a, int b, String c) {
this.a = a;
this.b = b;
this.c = c;
}

public MyExternalizable() {
}

//当序列化对象时,该方法自动调用
@Override
public void writeExternal(ObjectOutput out) throws IOException {
System.out.println("序列化...");
//可以在序列化时写非自身的属性
Date d = new Date();
out.writeObject(d);
//只序列化a、c属性
out.writeObject(a);
out.writeObject(c);
}

//当反序列化为对象时,该方法自动调用.反序列化会按照序列化顺序来
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
//依次反序列化
System.out.println("反序列化...");
Date d = (Date) in.readObject();
System.out.println(d.getTime());
//只反序列化a、c属性
this.a = (Integer) in.readObject();
this.c = (String) in.readObject();
}

@Override
public String toString() {
return "a:"+ a +" b:" + b + " c:" + c;
}
}

执行结果:

image-20210629212035118

参考链接:

https://www.jianshu.com/p/729c15e14d76
https://blog.csdn.net/liwenshui322/article/details/47145191
https://blog.csdn.net/weixin_30485291/article/details/98134295
https://www.cnblogs.com/youxin/archive/2013/06/04/3116304.html