【1】 序列化与反序列化

如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。

简单来说:

  • 序列化:将数据结构或对象转换成二进制字节流的过程
  • 反序列化:将在序列化过程中所生成的二进制字节流的过程转换成数据结构或者对象的过程

对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。

维基百科是如是介绍序列化的:

序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。对于许多对象,像是使用大量引用的复杂对象,这种序列化重建的过程并不容易。面向对象中的对象序列化,并不概括之前原始对象所关系的函数。这种过程也称为对象编组(marshalling)。从一系列字节提取数据结构的反向操作,是反序列化(也称为解编组、deserialization、unmarshalling)。

综上:序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
对象的序列化与反序列化详解_序列化

实际开发中有哪些用到序列化和反序列化的场景?

  • 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
  • 将对象存储到文件中的时候需要进行序列化,将对象从文件中读取出来需要进行反序列化。
  • 将对象存储到缓存数据库(如 Redis)时需要用到序列化,将对象从缓存数据库中读取出来需要反序列化。

对象序列化内容与机制

① Java对象序列化时参与序列化的内容包含以下几个方面:

  • 属性,包括基本数据类型、数组以及其他对象的引用;
  • 类名。

② 不能被序列化的内容有以下几个方面

  • 方法。
  • 有static修饰的属性。
  • 有transient修饰的属性。

③ 对象的序列化

对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其它程序获取了这种二进制流,就可以恢复成原来的Java对象。

序列化的好处在于可将任何实现了Serializable接口的对象转化为字节数据,使其在保存和传输时可被还原。

序列化是 RMI(Remote Method Invoke – 远程方法调用)过程的参数和返回值都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是 JavaEE 平台的基础。

如果需要让某个对象支持序列化机制,则必须让其类是可序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一:

  • Serializable
  • Externalizable

​Externalizable​​​是​​Serializable​​​接口的子类,有时我们不希望序列化那么多,可以使用这个接口,这个接口的​​writeExternal()和readExternal()​​方法可以指定序列化哪些属性。

显示定义serialVersionUID的用途

凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量:

private static final long serialVersionUID;

serialVersionUID用来表明类的不同版本间的兼容性。如果类没有显示定义这个静态变量,它的值是Java运行时环境根据类的内部细节自动生成的。若类的源代码作了修改,serialVersionUID 可能发生变化。故建议,显示声明。

  • 希望类的不同版本对序列化兼容,因此需确保类的不同版本具有相同的serialVersionUID。
  • 不希望类的不同版本对序列化兼容,因此需确保类的不同版本具有不同的serialVersionUID。

Java的序列化机制是通过在运行时判断类的​​serialVersionUID​​​来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的​​serialVersionUID​​​与本地相应实体(类)的​​serialVersionUID​​进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。

当实现​​java.io.Serializable​​​接口的实体(类)没有显式地定义一个名为​​serialVersionUID​​​,类型为long的变量时,Java序列化机制会根据编译的class自动生成一个​​serialVersionUID​​作序列化版本比较用,这种情况下,只有同一次编译生成的class才会生成相同的serialVersionUID 。

如果我们不希望通过编译来强制划分软件版本,即实现序列化接口的实体能够兼容先前版本,未作更改的类,就需要显式地定义一个名为serialVersionUID,类型为long的变量,不修改这个变量值的序列化实体都可以相互进行串行化和反串行化。


默认序列化机制

如果仅仅只是让某个类实现Serializable接口,而没有其它任何处理的话,则就是使用默认序列化机制。

使用默认机制,在序列化对象时,不仅会序列化当前对象本身,还会对该对象引用的其它对象也进行序列化,同样地,这些其它对象引用的另外对象也将被序列化,以此类推。所以,如果一个对象包含的成员变量是容器类对象,而这些容器所含有的元素也是容器类对象,那么这个序列化的过程就会较复杂,开销也较大。


【2】使用对象流序列化对象

若某个类实现了 Serializable 接口,该类的对象就是可序列化的:

① 序列化和反序列化

  • 序列化
    创建一个 ObjectOutputStream调用 ObjectOutputStream 对象的 writeObject(对象) 方法输出可序列化对象。注意写出一次,操作flush()
  • 反序列化
    创建一个 ObjectInputStream调用 readObject() 方法读取流中的对象

强调:如果某个类的字段不是基本数据类型或 String 类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型的 Field 的类也不能序列化。

  • 序列化一个对象将可能得到整个对象序列。

在序列化过程中不仅保留当前类对象的数据,而且递归保存对象引用的每个对象的数据。将整个对象层次写入字节流中,这也就是序列化对象的“深复制”,即复制对象本身及引用的对象本身。

另外,添加“​​transient​​”关键字的不能被序列化,读取时也不可被恢复,该属性值保持默认初始值。


② 实例demo
序列化:将对象写入到磁盘或者进行网络传输。

# 要求对象必须实现序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test3.txt"));
Person p = new Person("韩梅梅",18,"中华大街",new Pet());
oos.writeObject(p);
oos.flush();
oos.close();

反序列化:将磁盘中的对象数据源读出。

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test3.txt"));
Person p1 = (Person)ois.readObject();
System.out.println(p1.toString());
ois.close();

③ 使用的Person对象

package com.web.test;
import java.io.Serializable;
public class Person implements Serializable{
private String name;
private int age;
private String sex;

// get set tostring...
}

④ 序列化对象的读写

package com.web.test;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;

import org.junit.Test;

public class TestObject {

@Test
public void testWrite() throws IOException{
ObjectOutputStream oos = null;
String destFile = "D:\\person111.txt";
File file = new File(destFile);
if (!file.exists()) {
file.createNewFile();
}
/*每次写入不是覆盖而为追加*/
FileOutputStream fos = new FileOutputStream(file,true);
if (file.length()<1) {
oos = new ObjectOutputStream(fos);
}else {
/*使用重写的MyObjectOutputStream,后续写入时,不写入header;
* 否则会报:java.io.StreamCorruptedException:
* invalid type code: AC错误。*/
oos = new MyObjectOutputStream(fos);
}
oos.writeObject("Test");

oos.flush();

}
/*
* fileName:准备读取的字节文件
* */
@Test
public void testRead() throws Exception{
String fileName = "D:\\person111.txt";
FileInputStream fis = new FileInputStream(fileName);
ObjectInputStream ois = new ObjectInputStream(fis);

String name = (String) ois.readObject();
String name1 = (String) ois.readObject();
System.out.println(name+"888"+name1);

}
/*重写ObjectOutputStream*/
class MyObjectOutputStream extends ObjectOutputStream {
public MyObjectOutputStream() throws IOException {
super();
}
public MyObjectOutputStream(OutputStream out) throws IOException {
super(out);
}

protected void writeStreamHeader() throws IOException {
return;
}
}
}

⑤ 使用一个ObjectInputStream读取两个对象

@Test
public void testWrite1() throws Exception{
ObjectOutputStream oos = null;
String destFile = "D:\\person11.txt";
File file = new File(destFile);
if (!file.exists()) {
file.createNewFile();
}
/*每次写入不是覆盖而为追加*/
FileOutputStream fos = new FileOutputStream(file,true);
oos = new ObjectOutputStream(fos);
for(int i=0;i<=2;i++){
oos.writeObject(new Person("liuxing",12,"boy"));
}
/*默认调用flush()*/
oos.close();

FileInputStream fis = new FileInputStream(destFile);
ObjectInputStream ois = new ObjectInputStream(fis);

Person p = (Person) ois.readObject();
System.out.println(p.getName());
Person p1 = (Person) ois.readObject();
System.out.println(p1.getName());

}

⑥ transient关键字的作用

在实际开发过程中,我们常常会遇到这样的问题,这个类的有些属性需要序列化,而其他属性不需要被序列化,打个比方,如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上transient关键字。换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。

java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。


【3】反序列化的注入漏洞问题

1.WebLogic, WebSphere, JBoss, Jenkins, and OpenNMS等java应用都曾受过该漏洞影响。Apache Commons Collections这样的基础库非常多的Java应用都在用。一旦编程人员误用了反序列化这一机制,使得用户输入可以直接被反序列化,就能导致任意代码执行,这是一个极其严重的问题,WebLogic等存在此问题的应用可能只是冰山一角。

2.首先拿到一个漏洞应用,需要找到一个接受外部输入的序列化对象的接收点,即反序列化漏洞的触发点。我们可以通过审计源码中对反序列化函数的调用(例如readObject())来寻找,也可以直接通过对应用交互流量进行抓包,查看流量中是否包含序列化数据来判断,如java序列化数据的特征为以标记(ac ed 00 05)开头。

确定了反序列化输入点后,再考察应用的Class Path中是否包含相应的基础库,如Apache Commons Collections库,可通过“grep -R InvokerTransformer .”确认是否包含exp需要的类库(把“InvokerTransformer”删除干净则此漏洞便无法触发)。

对象的序列化与反序列化详解_对象_02
新版本虽然抓不到端口信息了,但是exp需要的基础类库仍然存在。

解决方法:可以加入防火墙过滤相应端口的通信。假若反序列化可以设置Java类型的白名单,那么问题的影响就小了很多。使用加密通信,如SSL。

【4】序列化协议对应于 TCP/IP 4 层模型的哪一层

我们知道网络通信的双方必须要采用和遵守相同的协议。TCP/IP 四层模型是下面这样的,序列化协议属于哪一层呢?

  • 应用层
  • 传输层
  • 网络层
  • 网络接口层
    对象的序列化与反序列化详解_java_03
    如上图所示,OSI 七层协议模型中,表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话,就是将二进制流转换成应用层的用户数据。此外,OSI 七层协议模型中的应用层、表示层和会话层对应的都是 TCP/IP 四层模型中的应用层,所以序列化协议属于 TCP/IP 协议应用层的一部分。

tcp/ip是事实标准,分4层。osi模型是国际标准,分7层。我们有时会看到如右图所示五层模型,这是从概念上来理解的。将网络接口层分为了数据链路层和物理层。
对象的序列化与反序列化详解_反序列化_04

参考博文:
​​​序列化和反序列化、以及反序列化注入问题要点​​​​JAVA反序列化漏洞​​​​序列化与反序列化​