对象的序列化与反序列化详解

本文涉及的产品
系统运维管理,不限时长
简介: 对象的序列化与反序列化详解

【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”删除干净则此漏洞便无法触发)。

20190115180925264.png

新版本虽然抓不到端口信息了,但是exp需要的基础类库仍然存在。

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

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


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

  • 应用层
  • 传输层
  • 网络层
  • 网络接口层



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

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


58f5a46992414ab69462f07a0182e5ea.png


参考博文:

序列化和反序列化、以及反序列化注入问题要点

JAVA反序列化漏洞

序列化与反序列化


目录
相关文章
|
1月前
|
JSON 数据格式 索引
Python中序列化/反序列化JSON格式的数据
【11月更文挑战第4天】本文介绍了 Python 中使用 `json` 模块进行序列化和反序列化的操作。序列化是指将 Python 对象(如字典、列表)转换为 JSON 字符串,主要使用 `json.dumps` 方法。示例包括基本的字典和列表序列化,以及自定义类的序列化。反序列化则是将 JSON 字符串转换回 Python 对象,使用 `json.loads` 方法。文中还提供了具体的代码示例,展示了如何处理不同类型的 Python 对象。
|
1月前
|
存储 安全 Java
Java编程中的对象序列化与反序列化
【10月更文挑战第22天】在Java的世界里,对象序列化和反序列化是数据持久化和网络传输的关键技术。本文将带你了解如何在Java中实现对象的序列化与反序列化,并探讨其背后的原理。通过实际代码示例,我们将一步步展示如何将复杂数据结构转换为字节流,以及如何将这些字节流还原为Java对象。文章还将讨论在使用序列化时应注意的安全性问题,以确保你的应用程序既高效又安全。
|
2月前
|
JSON 前端开发 数据格式
前端的全栈之路Meteor篇(五):自定义对象序列化的EJSON介绍 - 跨设备的对象传输
EJSON是Meteor框架中扩展了标准JSON的库,支持更多数据类型如`Date`、`Binary`等。它提供了序列化和反序列化功能,使客户端和服务器之间的复杂数据传输更加便捷高效。EJSON还支持自定义对象的定义和传输,通过`EJSON.addType`注册自定义类型,确保数据在两端无缝传递。
|
2月前
|
存储 Java
Java编程中的对象序列化与反序列化
【10月更文挑战第9天】在Java的世界里,对象序列化是连接数据持久化与网络通信的桥梁。本文将深入探讨Java对象序列化的机制、实践方法及反序列化过程,通过代码示例揭示其背后的原理。从基础概念到高级应用,我们将一步步揭开序列化技术的神秘面纱,让读者能够掌握这一强大工具,以应对数据存储和传输的挑战。
|
2月前
|
存储 安全 Java
Java编程中的对象序列化与反序列化
【10月更文挑战第3天】在Java编程的世界里,对象序列化与反序列化是实现数据持久化和网络传输的关键技术。本文将深入探讨Java序列化的原理、应用场景以及如何通过代码示例实现对象的序列化与反序列化过程。从基础概念到实践操作,我们将一步步揭示这一技术的魅力所在。
|
1月前
|
存储 缓存 NoSQL
一篇搞懂!Java对象序列化与反序列化的底层逻辑
本文介绍了Java中的序列化与反序列化,包括基本概念、应用场景、实现方式及注意事项。序列化是将对象转换为字节流,便于存储和传输;反序列化则是将字节流还原为对象。文中详细讲解了实现序列化的步骤,以及常见的反序列化失败原因和最佳实践。通过实例和代码示例,帮助读者更好地理解和应用这一重要技术。
49 0
|
4月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
4月前
|
存储 开发框架 .NET
解锁SqlSugar新境界:利用Serialize.Linq实现Lambda表达式灵活序列化与反序列化,赋能动态数据查询新高度!
【8月更文挑战第3天】随着软件开发复杂度提升,数据查询的灵活性变得至关重要。SqlSugar作为一款轻量级、高性能的.NET ORM框架,简化了数据库操作。但在需要跨服务共享查询逻辑时,直接传递Lambda表达式不可行。这时,Serialize.Linq库大显身手,能将Linq表达式序列化为字符串,实现在不同服务间传输查询逻辑。结合使用SqlSugar和Serialize.Linq,不仅能够保持代码清晰,还能实现复杂的动态查询逻辑,极大地增强了应用程序的灵活性和可扩展性。
161 2
|
3月前
|
JSON 安全 编译器
扩展类实例的序列化和反序列化
扩展类实例的序列化和反序列化
45 1
|
3月前
|
JSON fastjson Java
niubility!即使JavaBean没有默认无参构造器,fastjson也可以反序列化。- - - - 阿里Fastjson反序列化源码分析
本文详细分析了 Fastjson 反序列化对象的源码(版本 fastjson-1.2.60),揭示了即使 JavaBean 沲有默认无参构造器,Fastjson 仍能正常反序列化的技术内幕。文章通过案例展示了 Fastjson 在不同构造器情况下的行为,并深入探讨了 `ParserConfig#getDeserializer` 方法的核心逻辑。此外,还介绍了 ASM 字节码技术的应用及其在反序列化过程中的角色。
98 10