Java序列化与序列化代理

序列化

内置序列化的3种方式

默认的序列化机制

即实现Serializable接口即可,不需要实现任何方

该接口没有任何方法,只是一个标记而已,告诉Java虚拟机该类可以被序列化了。然后利用ObjectOutputStream进行序列化和用ObjectInputStream进行反序列化。

注意

该方式下序列化机制会自动保存该对象的成员变量,static成员变量和transient关键字修饰的成员变量不会被序列化保存

这是最简单的一种方式,因为这种方式让序列化机制看起来很方便(然后,我们在进行对象序列化时,只需要使用ObjectOutputStream和ObjectInputStream的writeObject(object)方法和readObject()方法,就可以把传入的对象参数序列化和反序列化了,其他不用管)。

有时候想自己来控制序列化哪些成员,还有如何保存static和transient成员?

再注意:

该方式下,反序列化时不会调用该对象的构造器,但是会调用父类的构造器,如果父类没有默认构造器则会报错。static字段是类共享的字段,当该类的一个对象被序列化后,这个static变量可能会被另一个对象改变,所以这就决定了静态变量是不能序列化的,但如果再加上final修饰,就可以被序列化了,因为这是一个常量,不会改变。

实现Externalizable接口

Externalizable接口是继承自Serializable接口的,我们在实现Externalizable接口时,必须实现writeExternal(ObjectOutput)和readExternal(ObjectInput)方法,在这两个方法下我们可以手动的进行序列化和反序列化那些需要保存的成员变量。

反序列化时,首先会调用对象的默认构造器(没有则报错,如果默认构造器不是public的也会报错),然后再调用readExternal方法。

这种方式一定要显式的序列化成员变量,使得整个序列化过程是可控制的,可以自己选择将哪些部分序列化。

实现Serializable接口,在该实现类中再增加writeObject方法和readObject方法

在这两个方法里面需要使用stream.defaultWriteObject()序列化那些非static和非transient修饰的成员变量,static的和transient的变量则用stream.writeObject(object)显式序列化。

在序列化输出的时候,writeObject(object)会检查object参数,如果object拥有自己的writeObject()方法,那么就会使用它自己的writeObject()方法进行序列化。readObject()也采用了类似的步骤进行处理。如果object参数没有writeObject()方法,在readObject方法中就不能调用stream.readObject(),否则会报错。

Java内置序列化的三种方式

transient关键字

如果你不想让对象中的某个成员被序列化可以在定义它的时候加上transient关键字进行修饰

写入时替换对象——writeReplace

Serializable还有两个标记接口方法可以实现序列化对象的替换,即writeReplace和readResolve
如果实现了writeReplace方法后,那么在序列化时会先调用writeReplace方法将当前对象替换成另一个对象(该方法会返回替换后的对象)并将其写入流

!!!注意:

a. 实现writeReplace就不要实现writeObject了,因为writeReplace的返回值会被自动写入输出流中,就相当于自动这样调用:writeObject(writeReplace());
b. 因此writeReplace的返回值(对象)必须是可序列话的,如果是Java自己的基础类或者类型那就不用说了;
c. 但如果返回的是自定义类型的对象,那么该类型必须是彻底实现序列化的!  

writeReplace的替换如何在反序列化时被恢复

i. 注意!不是用readResolve恢复哦!readResolve并不是用来恢复writeReplace的
ii. 这里无法恢复了!即对象被彻底替换了!也就是说使用ObjectInputStream读取的对象只能是被替换后的对象,只能在读取后自己手动恢复了
iii.使用writeReplace替换写入后也不能通过实现readObject来实现自动恢复了,因为默认已经被彻底替换了,就不存在自定义反序列化的问题了

保护性恢复对象,同时也可以替换对象)——readResolve

readResolve会在readObject调用之后自动调用,它最主要的目的就是让恢复的对象变个样,比如readObject已经反序列化好了一个Person对象,那么就可以在readResolve里再对该对象进行一定的修改,而最终修改后的结果将作为ObjectInputStream的readObject的返回结果

writeReplace、readResolve

EA中的序列化代理

优先使用单值枚举

枚举类型由JVM帮我名保证唯一性,因此对于枚举实现的单例模式,反序列化也是唯一的

安全性

以Period序列化为例

public final class Period {
    private final Date start;
    private final Date end;

    public Period(Date start, Date end) {
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());
        if(this.start.compareTo(this.end) > 0) 
            throw new IllegalArgumentException(start + " after " + end);
    }

    public Date getStart() {
        return new Date(start.getTime());
    }

    public Date getEnd() {
        return new Date(end.getTime());
    }

    //反序列化时增加约束条件
    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        if(start.compareTo(end) > 0) {
            throw new InvalidObjectException(start + " after " + end);
        }
    }
}

伪造字节流,破坏约束

举例: 直接修改字节序列化流,序列化后文件以某种格式存储(具体格式可以参考JVM虚拟机实战)修改start值那么,就无法保证period中start

修复方案:
readObject内进行判断

private void readObject(ObjectInputStream s) {
    s.defaultReadObject();
    start = new Date(start.getTime());
    end = new Date(end.getTime());
    if(start.compareTo(end) > 0) {
        throw new InvalidObjectException(start + " after " + end);
    }
}

防止外部引用破坏约束

尽管readObject中增加了有效性检查,但通过伪造字节流创建可变的Period实例仍是可能
做法是:字节流以Period实例开头,然后附加上两个额外的引用执行Period实例中两个私有的Date域。攻击者从ObjectInputStream中读取Period实例,然后读取其后的「恶意引用」,通过这个引用攻击者就可以修改Period中私有的Date域

ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
Period period = new Period(new Date(), new Date());
out.writeObject(period);
byte[] ref = {0x71, 0, 0x7e, 0, 5}; //指向period中私有域start的字节
bos.write(ref);
ref[4] = 4; //指向period中私有域end的字节
bos.write(ref);

//反序列化
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
Period period1 = (Period)in.readObject();
//ref1指向period1中私有域start指向的对象,可通过这个引用修改不可变对象
Date ref1 = (Date)in.readObject(); 
Date ref2 = (Date)in.readObject();

修复方案: 序列化代理代

序列化代理代

首先为可序列化的类设计一个私有的静态嵌套类,精确的表示外围类实例的逻辑状态。它有一个单独的构造器,其参数类型为外围类。外围类及其序列化代理都必须实现Serializable接口。
将writeReplace方法添加到外围类中。
在SerializableProxy类中提供readResolve方法,它返回逻辑上相等的外围类的实例

//外围类不需要serialVersionUID
public final class Period implements Serializable {
    private final Date start;
    private final Date end;

    public Period(Date start, Date end) {
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());
        if(this.start.compareTo(this.end) > 0) 
            throw new IllegalArgumentException(start + " after " + end);
    }

    public Date getStart() {
        return new Date(start.getTime());
    }

    public Date getEnd() {
        return new Date(end.getTime());
    }

    //在序列化之前,将外围类的实例转变成它的序列化代理
    private Object writeReplace(){
        return new SerializationProxy(this);
    }

    //防止被攻击者使用
    private void readObject(ObjectInputStream stream) 
        throws InvalidObjectException{
        throw new InvalidObjectException("Proxy required");
    }

    private static class SerializationProxy implements Serializable {
        private static final long serialVersionUID = ...;
        private final Date start;
        private final Date end;

        SerializationProxy(Period p) {
            this.start = p.start;
            this.end = p.end;
        }

        private Object readResolve() {
            return new Period(start, end);
        }
    }
}

结论

序列化代理实质是将A对象通过writereplace替换私有的成序列化的B对象,从而将A对象进行序列化隐藏;反序列时是通过B对象readresolve进行保护性恢复,从而恢复成A对象

需要注意的是A的readObject方法需要禁用掉,即直接抛出异常(这是一个必要点哦);writerepalce后将无A的序列化对象文件,防止被攻击者使用(参考防止外部引用破坏约束)

你可能感兴趣的:(Java序列化与序列化代理)