JAVA反序列化基础

本文详细介绍了Java序列化和反序列化的概念、应用场景、相关类的使用,以及如何通过ObjectOutputStream和ObjectInputStream进行操作。同时,文章通过多个案例展示了序列化到文件和字节数组的过程,并探讨了反序列化漏洞的原理。此外,还提醒了序列化时transient关键字的作用和序列化ID(serialVersionUID)的影响。


前言

      本文包括:java序列化和反序列化的基础知识和案例演示,案例包括转化为文件字符输出流并保存成文件的方式和通过ByteArrayOutputStream保存为字节数组的方式进行序列化和反序列化,使用Binary工具查看序列化后的文件和数据含义解释。


一、基础知识

1.什么是序列化和反序列化?

      Java序列化是指把Java对象转换为字节序列的过程,通过ObjectOutputStream 类中的的writeObject方法可以实现序列化。
      Java反序列化是指把字节序列恢复为Java对象的过程,通过ObjectInputStream类的readObject方法进行反序列化。序列化和反序列化的过程都是基于字节流来完成的。
      需注意的点有:

  • 要进行序列化和反序列化则该类必须继承自java.io.Serializable接口(该类的全部属性也必须继承自Serializable接口),否则会抛出NotSerializableException报错,如下图所示。
  • 而且所有非transient关键字修饰的属性必须是可序列化的。
  • 类对象序列化之后不一定要保存成文件,也可以通过ByteArrayOutputStream保存为字节数组。
  • 反序列化之后返回的数据类型为Object类型,如果要转化为序列化之前的类,需要进行强制类型转化。

在这里插入图片描述

2.为什么要序列化

      因为java对象不只是存储在内存中,它还需要在网络中进行传输,而序列化就是将java对象的状态信息转换为一串由二进制字节组成的数组,这样就能让对象进行持久化存储或者在网络中传输了,以后有需要的时候也可以直接通过反序列化得到这个对象。

3.ObjectOutputStream 类

      java.io.ObjectOutputStream 类的作用就是将Java对象的原始数据类型写出到文件中,以实现对象的持久存储。
      一个对象要想序列化,必须满足以下两个条件:

  • 该类必须实现 java.io.Serializable 接口,Serializable 是一个标记接口,不实现此接口的类将不会
    进行序列化或反序列化,会抛出 NotSerializableException 。
  • 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态 的,使用 transient 关键字修饰。

4.ObjectInputStream 类

      在Java反序列化中,会调用被反序列化的ObjectInputStream类的readObject方法,当readObject方法被重写不当时就会产生反序列漏洞。

5.Java序列化算法

  • 所有保存到磁盘的对象都有一个序列化编码号,可以自定义编写如: private static final long serialVersionUID = -3066949856415001911L;
  • 当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。
  • 如果此对象已经序列化过,则直接输出编号即可。

6.java类中serialVersionUID的作用

      serialVersionUID适用于java序列化机制。简单来说,JAVA序列化的机制是通过判断类的serialVersionUID来验证的版本一致的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较。如果相同说明是一致的,可以进行反序列化,否则会出现反序列化版本一致的异常,即是InvalidCastException。

二、序列化和反序列化案例一

     1)先创建一个java的包并在里面创建一个可序列化的User类,注意要实现Serializable接口。本文环境为:idea 2021.3版本,jdk为8。

package serializable;

import java.io.Serializable;

public class User implements Serializable {
    private String name;

    public void setName(String name){
        this.name = name;
    }

    public String getName(){
        return name;
    }
}

在这里插入图片描述
     2)编写测试代码,测试序列化和反序列化。

package serializable;

import java.io.*;

public class test {
    public static void main(String[] args) throws IOException,ClassNotFoundException{
        User user = new User();
        user.setName("Serial");

        // 测试序列化
        // 创建文件字符输出流,保存到文件  本案例会生成serializable.dat文件
        FileOutputStream fileOutputStream = new FileOutputStream("D:/notes/java/practise/java_serializable/src/serializable/file/serializable.dat");
        // 将对象写入ObjectOutputStream中,再转化到FileOutputStream并输出到文件中
        ObjectOutputStream oos = new ObjectOutputStream(fileOutputStream);
        // 调用writeObject方法,序列化对象到文件serializable.dat中
        oos.writeObject(user);
        // 关闭流,否则再次write时容易出现文件空白的情况
        oos.close();

        // 从文件反序列化对象
        // 创建一个ObjectInputStream输入流,读取文件serializable.dat的磁盘数据
        // 创建一个FIleInutputStream
        FileInputStream fileInputStream = new FileInputStream("D:/notes/java/practise/java_serializable/src/serializable/file/serializable.dat");
        // 将FileInputStream封装到ObjectInputStream中,还需要进行一下类型转换,默认是Object类型
        ObjectInputStream ois = new ObjectInputStream(fileInputStream);
        // 使用ObjectInputStream中的readObject读取一个对象
        User newUser = (User) ois.readObject();
        // 关闭流,否则再次write时容易出现文件空白的情况
        ois.close();
        //  输出
        System.out.println(newUser.getName());

    }
}

在这里插入图片描述
     3)使用Binary 查看生成的序列化文件,可以看到存储的对象数值Serial,开头的 AC ED 00 05 为序列化内容的特征。Binary工具可在以下链接下载:https://pc.qq.com/detail/5/detail_90225.html。

十六进制数数据含义解释
AC ED申明使用序列化协议
00 05序列化协议版本
73这是一个新对象
72这里开始一个新的Class
00 17Class名字为23个字节
02标记号
00 04包含域的个数:4

在这里插入图片描述

三、序列化和反序列化案例二

     1)创建一个Employee.java文件,有一个成员变量被 transient 瞬态修饰,不能被反序列化。

package serializable;

import java.io.*;

public class Employee implements Serializable{
    public String name;
    public String address;
    public int ages;
    public transient int age;// transient瞬态修饰成员,不会被序列化

}

在这里插入图片描述
     2)创建一个SerializeDemo.java文件进行序列化生成了一个ser.dat文件,将Employee对象写入到了ser.dat文件中。

package serializable;

import java.io.*;

public class SerializeDemo {
    public static void main(String[] args) throws IOException {
        Employee employee = new Employee();
        employee.name = "zhangsan";
        employee.age = 20;
        employee.ages = 40;
        employee.address = "shenzhen";
        // 创建文件字符输出流并保存成文件 路径为将要生成的文件所在的路径,
        // 本案例会生成ser.dat文件
        // 将对象写入ObjectOutputStream中,再转化到FileOutputStream并输出到文件中
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("D:/notes/java/practise/java_serializable/src/serializable/file/ser.dat"));
        // 写出对象
        outputStream.writeObject(employee);
        // 释放资源,关闭流,否则再次write时容易出现文件空白的情况
        outputStream.close();
    }
}

在这里插入图片描述
     3)使用Binary工具查看生成的序列化文件。
在这里插入图片描述
     4)进行反序列化从二进制文件中提取出对象。

package serializable;

import java.io.*;

public class DeserializeDemo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 创建反序列化流,创建一个ObjectInputStream输入流,读取文件ser.dat的磁盘数据
        FileInputStream fileInputStream = new FileInputStream("D:/notes/java/practise/java_serializable/src/serializable/file/ser.dat");
        ObjectInputStream inputStream = new ObjectInputStream(fileInputStream);
        // 使用ObjectInputStream中的readObject读取一个对象
        Object o = inputStream.readObject();
        // 释放资源,关闭流,否则再次write时容易出现文件空白的情况
        inputStream.close();
        System.out.println(o);
    }
}

在这里插入图片描述

四、序列化和反序列化案例三

     1)之前都是文件的输入流输出流并且是引用的方式,这回写一个字节数组的案例,全都在一个文件中。创建一个Serialize的java文件。

package serializable;

import java.io.*;

public class Serialize implements Serializable {
    // 必须实现Serializable接口,表明当前类可以被序列化。
    // serialVersionUID不写的话,idea会自动生成,赋予每个类不同的序列化UID
    private static final long serialVersionUID = -3066949856415001911L;
    public String name;
    public int age;
    public transient String address;

    // 序列化方式一
    private void writeObject (ObjectOutputStream s) throws IOException {
    s.defaultWriteObject();
    s.writeObject("This is the first way");
    }

    // 反序列化方式一
    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        String s1 = (String) s.readObject();
        System.out.println(s1);
    }

    // 序列化和反序列化
    /* 过程就是:利用构造函数实例化Serialize类并赋值了name和age属性到serialize对,address因为有transient修饰,所以不会反序列化
       再通过ObjectOutputStream将serialize对象序列化输出字节流到了byteArrayOutputStream
       并且控制台输出了一下byteArrayOutputStream
       然后通过ObjectInputStream与ByteArrayInputStream反序列化了byteArrayOutputStream字节流取到了之前序列化的对象
       最后输出了之前赋值的name与age两个属性,并且address属性一直都没有被序列化
    */
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Serialize serialize = new Serialize();
        serialize.name = "hanmeimei";
        serialize.age = 20;
        serialize.address = "asdf";
        // 创建序列化流
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        // 写出对象
        objectOutputStream.writeObject(serialize);
        // 释放资源
        objectOutputStream.close();
        // 打印输出
        System.out.println(byteArrayOutputStream);
        // 创建反序列化流
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Serialize o = (Serialize) objectInputStream.readObject();
        System.out.println( o.name+ "\n" + o.age );    
    } 
}

在这里插入图片描述
     2)在上面的案例方式一中我们直接使用了writeObject方法,这个特殊点就是Java允许我们在序列化对象的时候插入一些自定义数据,并且在反序列化的时候能够使用 readObject 进行读取。如果自定义了writeObject方法和readObject方法,java会在Serialize对象默认序列化之后又增加写入了一串字符串的序列化数据,并且默认反序列化之后会把这个字符串读出来打印到控制台,这也是方式一种没有打印输出语句却打印出来了的原因。
     3)Java中ByteArrayOutputStream类的toByteArray()方法用于创建一个新分配的字节数组。新分配的字节数组的大小等于这个输出流的当前大小。此方法将缓冲区的有效内容复制到其中。

五、反序列化漏洞的基本原理和案例

     1)在Java反序列化中,会调用被反序列化的readObject方法,当readObject方法被重写不当时产生漏洞。类似于PHP反序列化时会自动执行__wakeup方法一样。
     2)一个小案例演示,实际环境中不会这么简单仅起演示作用。

package serializable;
import java.io.*;

public class Deserialization_1 { public static void main(String args[]) throws Exception{

    // 序列化
    // 定义mo对象
    MyObject mo = new MyObject();
    mo.name = "hello world";
    // 创建一个包含对象进行反序列化信息的”object”数据文件
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/notes/java/practise/java_serializable/src/serializable/file/object.dat"));
    // writeObject()方法将mo对象写入object文件
    oos.writeObject(mo);
    oos.close();

    // 创建反序列化流,创建一个ObjectInputStream输入流,读取文件object.dat的磁盘数据
    FileInputStream fileInputStream = new FileInputStream("D:/notes/java/practise/java_serializable/src/serializable/file/object.dat");
    ObjectInputStream ois = new ObjectInputStream(fileInputStream);
    // 使用ObjectInputStream中的readObject从磁盘中读取MyObject对象的值给objectFromDisk
    MyObject objectFromDisk = (MyObject)ois.readObject();
    // 打印name的值
    System.out.println(objectFromDisk.name);
    // 关闭流
    ois.close();
}

public static class MyObject implements Serializable {
    public String name;
    //重写readObject()方法
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
    //执行默认的readObject()方法
    in.defaultReadObject();
    //执行打开计算器程序命令
    Runtime.getRuntime().exec("calc.exe");
    }
}
}

在这里插入图片描述
     3)上面案例造成反序列化漏洞就是因为重写了readObject方法,然后执行了 Runtime.getRuntime().exec(),defaultReadObject方法为ObjectInputStream中执行readObject后的默认执行方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值