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的反射机制把我们的代码意图都利用字符串的形式进行体现,使得原本应该是字符串的属性,变成了代码执行的逻辑,而这个机制也是后续的漏洞使用的前提