JAVA 安全学习笔记(三)JAVA 序列化原理
九涅·烧包包儿 / / 程序员 / 阅读量
>  Author: shaobaobaoer
>  Codes : https://github.com/ninthDevilHAUNSTER/JavaSecLearning
>  Mail: shaobaobaoer@126.com
>  WebSite: shaobaobaoer.cn
>  Time: Friday, 24. July 2020 09:49PM

Java 序列化与反序列化

Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。把字节序列恢复为对象的过程称为对象的反序列化。

  • 序列化就是把对象的状态信息转换为字节序列(即可以存储或传输的形式)过程
  • 反序列化即逆过程,由字节流还原成对象

在Java中实现对象反序列化非常简单,实现java.io.Serializable(内部序列化)或java.io.Externalizable(外部序列化)接口即可被序列化,( Externalizable 实际上是继承了Serializable 的接口 )。

另外 Serializable 是一个空空的接口。仅仅用于标识。

public interface Serializable {
}

对于序列化,有着以下的注意点

  • 如果该类的某个属性标识为static类型的,则该属性不能序列化。
  • 如果该类的某个属性采用transient关键字标识,则该属性不能序列化。
  • 反序列化不会调用类的构造方法。因为本质上它是从字节流中获取的。
  • 具体而言 反序列化的 constructor 是源自于sun.reflect.ReflectionFactory.newConstructorForSerialization创建了一个反序列化专用的Constructor(反射构造方法对象),该方法可以绕过构造函数。

对此,我创建了一个Employee类,用于之后的实验


class Employee implements java.io.Serializable {
    public String name;
    public String address;
    public transient int SSN; // 暂时的
    public int number;

    Employee() {
        System.out.println("init Employee Class");
    }


    public static @NotNull Employee getEmployee() {
        return new Employee();
    }

    public void mailCheck() {
        System.out.println("Mailing a check to " + name
                + " " + address);
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", address='" + address + '\'' +
                ", SSN=" + SSN +
                ", number=" + number +
                '}';
    }

ObjectInputStream与ObjectOutputStream操作

  • java.io.ObjectOutputStream类最核心的方法是writeObject方法,即序列化类对象。
  • java.io.ObjectInputStream类最核心的功能是readObject方法,即反序列化类对象。

其对应操作如下:

// 序列化DeserializationTest类
Employee t = new Employee(...)
ObjectOutputStream out = new ObjectOutputStream(baos);
out.writeObject(t);

// 反序列化输入流数据为Employee对象
ObjectInputStream in = new ObjectInputStream(bais);
Employee e = (Employee) in.readObject();

于是问题来了,Serializables是一个空接口,但是那只能是ObjectOutputStream,ObjectInputStream来操作完成序列化与反序列化了。
在序列化与反序列化的过程中,往往会用到 ObjectInputStream 和 ObjectOutputStream 中的 readObj与writeObj方法。

这两个Stream均继承自java.io.ObjectStreamClass,该方法继承了 Serializable 的接口
在 java.io.ObjectStreamClass#ObjectStreamClass(java.lang.Class<?>):532/535行中有着这两行代码

writeObjectMethod = getPrivateMethod(cl, "writeObject",
new Class<?>[] { ObjectOutputStream.class },
Void.TYPE);

虽然我涉世未深,但是也能猜得出,这是在动态获取writeObject这两个方法,并且是Private的

继续跟入,可以发现,总共需要扩展这五个方法

  • private void writeObject 自定义序列化。
  • private void readObject  自定义反序列化。
  • private void readObjectNoData 空数据的反序列化
  • ~static|~abstract Object writeReplace 写入时替换对象
  • ~static|~abstract Object readResolve  读出时替换对象

之于具体还是得看源码。(源码探究部分好复杂...本来想着一并写到这个博客里去,但是能力有限。内容很复杂。之后会把这个坑填上,这个博客后续还会补充)

扩展 Serializable 接口

通过上述分析,可以自定义这五个方法,对此也能够看出这五个方法的调用顺序

class Employee{
...
    /**
     * 自定义反序列化类对象
     *
     * @param ois 反序列化输入流对象
     * @throws IOException            IO异常
     * @throws ClassNotFoundException 类未找到异常
     */
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        System.out.println("readObject...");
        // 调用ObjectInputStream默认反序列化方法
        ois.defaultReadObject();
        // 省去调用自定义反序列化逻辑...
    }

    /**
     * 自定义序列化类对象
     *
     * @param oos 序列化输出流对象
     * @throws IOException IO异常
     */
    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject();


        System.out.println("writeObject...");
        // 省去调用自定义序列化逻辑...
    }

    private void readObjectNoData() {
        System.out.println("readObjectNoData...");
    }

    /**
     * 写入时替换对象
     *
     * @return 替换后的对象
     */
    protected Object writeReplace() {
        System.out.println("writeReplace....");
        return this;
    }

    protected Object readResolve() {
        System.out.println("readResolve....");
        return this;
    }

}

ObjectInputStream 与 ObjectOutputStream 源码探究

图片标题

通过调用图,可以发现,这两个stream均实现了ObjectStreamConstants 这一个接口,这个接口仅仅包括一些序列化时用到的常量。

PS:通过这个调用图也看不出什么来,具体还是要看函数的调用链...

ObjectStreamClass类

官方对于这个类的描述如下:

Serialization’s descriptor for classes. It contains the name and serialVersionUID of the class. The ObjectStreamClass for a specific class loaded in this Java VM can be found/created using the lookup method.

ObjectStreamClass这个是类的序列化描述符,这个类可以描述需要被序列化的类的元数据,包括被序列化的类的名字以及序列号。可以通过lookup()方法来查找/创建在这个JVM中加载的特定的ObjectStreamClass对象。

由于之后会用到这个类,对此,需要解析一下其构造方法

private ObjectStreamClass(final Class<?> cl) {
        this.cl = cl;
        name = cl.getName();
        isProxy = Proxy.isProxyClass(cl);
        isEnum = Enum.class.isAssignableFrom(cl);
        isRecord = isRecord(cl);
        serializable = Serializable.class.isAssignableFrom(cl);
        externalizable = Externalizable.class.isAssignableFrom(cl);

        Class<?> superCl = cl.getSuperclass();
        superDesc = (superCl != null) ? lookup(superCl, false) : null;
        localDesc = this;

        if (serializable) {...}

// 一些错误处理模块
        ...
}

cl一般是传入Object.getClass()serializable判断了是否为可序列化对象。直接导致了会不会进到if循环中进行处理。

    public Void run() {
...
/*
获取实现Serializable接口对象的字段,实现Externalizable的为空字段
没有声明序列化字段的,则获取默认的序列化字段
*/

// 获取 SUID 
        suid = getDeclaredSUID(cl); // 下面会解释
        try {
            fields = getSerialFields(cl);  // 下面会解释
            computeFieldOffsets();
        } catch (InvalidClassException e) {
            serializeEx = deserializeEx =
                    new ObjectStreamClass.ExceptionInfo(e.classname, e.getMessage());
            fields = NO_FIELDS;
        }

        if (isRecord) {
            canonicalCtr = canonicalRecordCtr(cl);
        } else if (externalizable) {
            cons = getExternalizableConstructor(cl);
        } else {
//对于 Serializable接口对象 ,获取writeObject,readObject,readObjectNoData方法
            cons = getSerializableConstructor(cl);
            writeObjectMethod = getPrivateMethod(cl, "writeObject",
                    new Class<?>[] { ObjectOutputStream.class },
                    Void.TYPE);
            readObjectMethod = getPrivateMethod(cl, "readObject",
                    new Class<?>[] { ObjectInputStream.class },
                    Void.TYPE);
            readObjectNoDataMethod = getPrivateMethod(
                    cl, "readObjectNoData", null, Void.TYPE);
            hasWriteObjectData = (writeObjectMethod != null);
        }
// 获取 writeReplace,readResolve方法
        domains = getProtectionDomains(cons, cl);
        writeReplaceMethod = getInheritableMethod(
                cl, "writeReplace", null, Object.class);
        readResolveMethod = getInheritableMethod(
                cl, "readResolve", null, Object.class);
        return null;
    }
});

serialVersionUID也是一个非常重要的字段,它是由 getDeclaredSUID 获取的,并且这个字段必须是static final修饰的。mask = Modifier.STATIC | Modifier.FINAL;

    private static Long getDeclaredSUID(Class<?> cl) {
        try {
            Field f = cl.getDeclaredField("serialVersionUID");
            int mask = Modifier.STATIC | Modifier.FINAL;
            if ((f.getModifiers() & mask) == mask) {
                f.setAccessible(true);
                return Long.valueOf(f.getLong(null));
            }
        } catch (Exception ex) {
        }
        return null;
    }

ObjectOutputStream.writeObject源码追踪

首先观察构造函数

    public ObjectOutputStream(OutputStream out) throws IOException {
        verifySubclass();
        bout = new BlockDataOutputStream(out); // new DataOutputStream(this); 绑定一个底层对象
        handles = new HandleTable(10, (float) 3.00);
        subs = new ReplaceTable(10, (float) 3.00);
        enableOverride = false;
        writeStreamHeader(); // 写入文件头
        bout.setBlockDataMode(true);// flush数据
        if (extendedDebugInfo) { // debug方法
            debugInfoStack = new DebugTraceInfoStack();
        } else {
            debugInfoStack = null;
        }
    }

其中 writeStreamHeader()将写入一些魔术变量。这两个变量在ObjectStreamConstants 中有定义

    protected void writeStreamHeader() throws IOException {
        bout.writeShort(STREAM_MAGIC);
        bout.writeShort(STREAM_VERSION);
    }

之后观察 writeObject函数


public final void writeObject(Object obj) throws IOException {
...
        try {
            writeObject0(obj, false);
...

可以发现其调用了一个私有的writeObject0函数,继续追踪

该函数较为复杂,可以分为如下几个部分

  • 处理之前写的和不可替换的对象
  • 检查需要替换的对象::check for replacement object
  • 如果该对象被替换过,则循环检查其内部::if object replaced, run through original checks a second time
  • 如果是字符串、数组、枚举、实现了Serializable接口的对象可以序列化,其余的不可以。:: remaining cases
private void writeObject0(Object obj, boolean unshared)
    throws IOException
{
    ...
    try {
        ...
        Object orig = obj;
        // 获取要序列化的对象的Class对象
        Class cl = obj.getClass();
        ObjectStreamClass desc;
        //这是一个循环,目的是判断writeReplace()返回的对象中是否还有这个方法,一直到最后需要序列化的那个对象,并返回这个对象。
    	//这就是我们可以自定义具体序列化哪个对象。对应了writeReplace()的方法
        for (;;) {
            Class repCl;
            // 创建描述cl的ObjectStreamClass对象
            desc = ObjectStreamClass.lookup(cl, true);
            //如果有writeReplace()就通过反射执行,方法会返回一个Object对象,如果返回的这个对象的类和本类相同,就不重复执行writeReplace()
            if (!desc.hasWriteReplaceMethod() ||
                    (obj = desc.invokeWriteReplace(obj)) == null ||
                    (repCl = obj.getClass()) == cl)
                {
                    break;
                }
                cl = repCl;
        }
        ...
        // 如果返回的这个对象的类和本类不同,就重复执行writeReplace()方法
        ...
        //如果是字符串、数组、枚举、实现了Serializable接口的对象可以序列化,其余的不可以
        if (obj instanceof String) {
            writeString((String) obj, unshared);
        } else if (cl.isArray()) {
            writeArray(obj, desc, unshared);
        } else if (obj instanceof Enum) {
            writeEnum((Enum) obj, desc, unshared);
        } else if (obj instanceof Serializable) {
            // 被序列化对象实现了Serializable接口
            writeOrdinaryObject(obj, desc, unshared);
        } 
        ...
}

可见desc = ObjectStreamClass.lookup(cl, true);表明了

ObjectInputStream.readObject 源码追踪

首先观察构造函数

    public ObjectInputStream(InputStream in) throws IOException {
        verifySubclass();
        bin = new BlockDataInputStream(in);
        handles = new HandleTable(10);
        vlist = new ValidationList();
        serialFilter = ObjectInputFilter.Config.getSerialFilter();
        enableOverride = false;
        readStreamHeader();
        bin.setBlockDataMode(true);
    }
支付宝捐赠
请使用支付宝扫一扫进行捐赠
微信捐赠
请使用微信扫一扫进行赞赏
有 0 篇文章