Java序列化和反序列化(一)
一.基础
1.什么是序列化和反序列化
序列化:对象 -> 字符串
反序列化:字符串 -> 对象
2.为什么要进行序列化和反序列化
序列化和反序列化的设计就是用来传输数据的
当两个进程通信的时候,可以通过序列化反序列化来进行传输
3.序列化的好处
(1)能够实现数据的持久化,通过序列化可以把数据永久保存在硬盘上,也可理解为通过序列化将数据保存在文件中。
(2)利用序列化实现远程通信,在网络中进行传输对象的字节序列
4.序列化与反序列化的场景
(1)想把内存中的对象保存到一个文件中或者是数据库中
(2)用套接字在网络上进行传输
(3)通过RMI传输对象的时候
5.几种创建的序列化和反序列化
XML$SOAP
JSON
Protobuf
二.代码示范
类文件:Person.java
package src; 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 "src.Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
序列化文件:SerializationTest.java
package src; import java.io.FileOutputStream;//文件输出流 import java.io.IOException;//用于声明可能会抛出IOException的方法。当一个方法可能会引发输入/输出异常时,可以使用throws IOException来通知调用该方法的其他部分,让它们做出相应的异常处理。 import java.io.ObjectOutput; import java.io.ObjectOutputStream;//将对象以二进制形式写入输出流。它可以将对象序列化成字节流,用于在网络中传输或保存到文件中。 public class SerializationTest { 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); System.out.println(person); serialize(person); } }
反序列化文件:UnserializeTest.java
package src; 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);//反序列化 } }
代码讲解
运行代码
Run SerializationTest.java

Run UnserializationTest.java

SerializationTest.java
我们将代码进行了封装,将序列化功能封装进了 serialize这个方法里面,在序列化当中,我们通过这个FileOutputStream输出流对象,将序列化的对象输出到ser.bin当中。再调用 oos 的writeObject方法,将对象进行序列化操作。
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));//输出流对象 oos.writeObject(obj);//序列化
UnserializationTest.java
进行反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename)); Object obj = ois.readObject();
Serializable 接口

(1)序列化类的属性没有实现Serializable,那么在序列化时就会报错
只有实现 了Serializable或者 Externalizable接口的类的对象才能被序列化为字节序列。(不是则会抛出异常)
Serializable 接口是 Java 提供的序列化接口,它是一个空接口,所以其实我们不需要实现什么。
public interface Serializable { }
Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。如果我们此处将 Serializable 接口删除掉的话,会导致如下结果。

(2)在序列化过程中,它的父类如果没有实现序列化接口,那么将无需提供无参构造函数来重新创建对象。
(3)一个实现Serializable接口的子类也是可以被序列化的。
(4)静态成员变量是不能被序列化
序列化是针对对象属性的,而静态成员变量是属于类的。
(5)transient 标识的对象成员变量不参与序列化
这里我们可以动手实操一下,将 Person.java中的name加上transient的类型标识。加完之后再跑我们的序列化与反序列化的两个程序,修改过程与运行结果如图所示。


三.序列化的安全问题
1.引子
序列化和反序列化中有两个重要的方法————writeObject和readObject
这两个方法可以经过开发者重写,一般序列化的重写都是由于下面的场景诞生的。
举个例子,MyList 这个类定义了一个 arr 数组属性,初始化的数组长度为 100。在实际序列化时如果让 arr 属性参与序列化的话,那么长度为 100 的数组都会被序列化下来,但是我在数组中可能只存放 30 个数组而已,这明显是不可理的,所以这里就要自定义序列化过程啦,具体的做法是重写以下两个 private 方法:
private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException
只要服务端反序列化数据,客户端传递类的readObject中代码会自动执行,基于攻击者在服务器上运行代码的能力。
所以从根本上讲,Java反序列化的漏洞与readObject有关。
2.可能存在的漏洞形式
(1)入口类的readObject直接调用危险方法
这种情况呢,在实际开发场景中并不是特别常见,我们还是跟着代码来走一遍,写一段弹计算器的代码,文件 ———— "Person.Java"
package src; 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 "src.Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } public void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException{ ois.defaultReadObject();//调用默认机制,以恢复对象的非静态和非瞬态(非 transient 修饰)字段 Runtime.getRuntime().exec("calc");//在操作系统上执行外部命令。 } }
先运行序列化程序 ———— "SerializationTest.java",再运行反序列化程序 ———— "UnserializeTest.java"
这时就会弹出计算器,也就是calc.exe
(2)入口参数中包含可控类,该类有危险方法,readObject时调用
(3)入口参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用
(4)构造函数/静态代码块等加载时隐式执行
四.Java反射
1.Java反射定义
对于任意一个类,都能够得到这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
其实在Java中定义的一个类本身也是一个对象,即java.lang.Class类的实例,这个实例称为类对象
-
类对象表示正在运行的 Java 应用程序中的类和接口
-
类对象没有公共构造方法,由 Java 虚拟机自动构造
-
类对象用于提供类本身的信息,比如有几种构造方法, 有多少属性,有哪些普通方法
要得到类的方法和属性,首先就要得到该类对象
2.获取类对象
假设现在有一个User类
package reflection; public class User { private String name; public User(String name) { this.name=name; } public void setName(String name) { this.name=name; } public String getName() { return name; } }
要获取该类的对象一般有三种方法
class.for.name("reflection.User") User.class new User().getClass()
最常用的是第一种,通过一个字符串即类的全路径名就可以得到类对象,另外两种方法依赖项太强
3.利用类对象创建对象
与new直接创建对象不同,反射是先拿到类对象,然后通过类对象获取构造器对象,再通过构造器对象创建一个对象
package reflection; import java.lang.reflect.*; public class CreateObject { public static void main(String[] args) throws Exception{ Class UserClass = Class.forName("reflection.User"); Constructor constructor = UserClass.getConstructor(String.class); User user = (User) constructor.newInstance("iu"); System.out.println(user.getName()); } }
| 方法 | 说明 |
|---|---|
| getConstructor(Class...<?> parameterTypes) | 获得该类中与参数类型匹配的公有构造方法 |
| getConstructors() | 获得该类的所有公有构造方法 |
| getDeclaredConstructor(Class...<?> parameterTypes) | 获得该类中与参数类型匹配的构造方法 |
| getDeclaredConstructors() | 获得该类所有构造方法 |
4.通过反射调用方法
package reflection; import java.lang.reflect.*; public class CallMethod { public static void main(String[] args) throws Exception{ Class UserClass = Class.forName("reflection.User"); Constructor constructor = UserClass.getConstructor(String.class); User user = (User) constructor.newInstance("iu"); Method method = UserClass.getDeclaredMethod("setName", String.class); method.invoke(user, "lizhien"); System.out.println(user.getName()); } }
| 方法 | 说明 |
|---|---|
| getMethod(String name, Class...<?> parameterTypes) | 获得该类某个公有的方法 |
| getMethods() | 获得该类所有公有的方法 |
| getDeclaredMethod(String name, Class...<?> parameterTypes) | 获得该类某个方法 |
| getDeclaredMethods() | 获得该类所有方法 |
5.通过反射访问属性
package reflection; import java.lang.reflect.*; public class AccessAttribute { public static void main(String[] args) throws Exception{ Class UserClass = Class.forName("reflection.User"); Constructor constructor = UserClass.getConstructor(String.class); User user = (User) constructor.newInstance("iu"); Field field = UserClass.getDeclaredField("name"); field.setAccessible(true);// name是私有属性,需要先设置可访问 field.set(user, "lizhien"); System.out.println(user.getName()); } }
| 方法 | 说明 |
|---|---|
| getField(String name) | 获得某个公有的属性对象 |
| getFields() | 获得所有公有的属性对象 |
| getDeclaredField(String name) | 获得某个属性对 |
| getDeclaredFields() | 获得所有属性对象 |
6.利用java反射执行代码
package reflection; public class Exec { public static void main(String[] args) throws Exception{ Class runtimeClass = Class.forName("java.lang.Runtime"); Object runtime = runtimeClass.getMethod("getRuntime").invoke(null);//getRuntime是静态方法,invoke时不需要传入对象 runtimeClass.getMethod("exec", String.class).invoke(runtime, "calc.exe"); } }
以上代码中,利用了Java的反射机制把我们的代码意图都利用字符串的形式进行体现,使得原本应该是字符串的属性,变成了代码执行的逻辑,而这个机制也是后续的漏洞使用的前提

java反序列化很难,但是日本为什么要排核废水
浙公网安备 33010602011771号