lostars
发布于 2020-04-08 / 2988 阅读
0

EFFECTIVE JAVA 3RD EDITION — 第十二章 序列化

Item 85 : Prefer alternatives to Java serialization

优先选择java序列化的备用方法

尽管Java提供了序列化功能,但是却存在潜在的风险和性能问题。

Java的序列化是通过执行 readObject 方法来执行反序列化,这个方法可以初始化 classpath 下的任何实现了 serializable 接口的类,在反序列化二进制流的时候可以执行该类的代码,这就给攻击者提供了入口,造成可远程执行代码和拒绝服务漏洞。

所以如果不可避免的要使用序列化,避免去反序列化任何不可靠的字节流,
如果要反序列化可以使用白名单或者黑名单的方式来控制可以序列化的字节,同时使用Java9中新增的ObjectInputFilter,最好的方式就是不反序列化任何东西。在新写的任何系统里都没有使用序列化的理由。这个时候可以使用跨平台数据结构,比如JSON和protobuf。他们的不同在于JSON是基于文本便于阅读的,而protobuf是基于二进制且更为高效。

总的来说应该避免使用序列化,如果需要跨平台的支持可以考虑使用JSON或者protobuf,避免去反序列化不可信的数据,在写可序列化的类时需要格外注意。

Item 86 : Implement Serializable with great caution

慎重实现Serializable接口

  1. 实现序列化接口会减少一个已经发布的类的灵活性,因为一旦发布了该类,这个类的序列化二进制数据流也会成为该类提供的API;
  2. 增加了bug几率和安全风险,不管是自己实现序列化方式或者使用默认的方案,序列化都提供了隐形的构造器去反序列化;
  3. 增加了发布一个类新版本的测试负担,因为需要在以前的版本中测试序列化和反序列化是否正常;

慎重的考虑实现序列化,用于可继承的类很少实现序列化接口,接口本身更不要集成序列化接口。
如果一个类同时需要用于继承和实现序列化接口,类中又有不变的属性的话就需要额外关注,将该类声明为final或者覆盖 finalize方法,如果这些不变的属性有默认值的话,就需要 readObjectNoData 方法来避免:

// readObjectNoData for stateful extendable serializable classes
private void readObjectNoData() throws InvalidObjectException {
    throw new InvalidObjectException("Stream data required");
}

内部类不应该实现序列化接口,由于内部类的存储方式不同,其序列化形式定义不明确,当然静态成员类是可以实现序列化接口的。

Item 87 : Consider using a custom serialized form

考虑使用自定义的序列化方式

尽量在对象的实际属性和逻辑属性相互对应的情况下使用Java的 Serializable ,比如下面的 Name 类:

// Good candidate for default serialized form 
public class Name implements Serializable {    
    /**     
     * Last name. Must be non-null.     
     * @serial     
     */    
    private final String lastName;    
    /**     
     * First name. Must be non-null.    
     * @serial     
     */    
    private final String firstName;    
    /**     
     * Middle name, or null if there is none.     
     * @serial     
     */    
    private final String middleName;   
     ... // Remainder omitted 
}

逻辑上来说,一个名字都是由姓、名、中间名构成,并且实际上该 Name 对象也是由这三个属性构成。即使使用默认的序列化方式,通常也要提供一个 readObject 方法来保证不变性和安全性

如果对象的实际内容和逻辑上的内容不同会造成下面的问题:

  1. 提供的api将永久绑定在当前内部数据上;
  2. 消耗过多的空间/时间;
  3. 容易造成 stack overflow;

在决定将一个字段标记为可序列化时,确保该字段是该对象逻辑属性的一部分,同时不需要序列化的字段可以使用 transient 标记,非序列化字段在反序列化时每个实例都会有一个默认值,比如对象默认是null,基本数字是0,布尔值是false。如果不能接受这些默认值,则需要提供 readObject 方法来反序列化这些值。

不管是否使用默认的序列化方式,在保证读取对象状态线程安全的情况下也要保证序列化的线程安全,比如给 wirteObject 方法加同步关键字 synchronized 。同时不管使用何种序列化方式都应该声明一个UID用于序列化,UID不一定要唯一,但是在修改了对象属性的情况下需要更新UID,不然会抛出InvalidClassException 。同时也注意为了保证已序列化对象的兼容性,不要去修改UID

总的来说需要仔细思考对象是否需要序列化以及需要序列化的字段,使用默认序列化考虑逻辑属性和实际属性是否对应,为了保证不同版本对象的序列化兼容性,在不同版本中应该尽量保证对象的完整。

Item 88 : Write readObject methods defensively

保护性的实现readObject方法

大的来讲, readObject 方法也算是一个构造器,只不过它使用的是字节流来构造。字节流能够构造出普通构造器无法创建的非法对象。反序列化时,防御性的复制不能拥有对象的任何引用非常重要,攻击都是通过对引用的修改来进行。

因此,每个有可变属性的序列化对象都必须保护性的复制这些属性(比如新建一个对象)。同时构造方法和 readObject 方法均不能直接或间接的调用可继承方法,因为这些方法会在子类反序列化之前执行。

总的来说 readObject 方法总是能够创建一个合法的对象,不管提供的二进制流是怎么样的。

实现 readObject 方法需要注意:

  1. 有对象引用的属性必须声明为私有,同时针对这些字段保护性的复制;
  2. 检查所有可变属性,失败时抛出 InvalidObjectException 异常,通常这个检查都在保护性的复制之后;
  3. 如果在反序列化之后需要校验整个对象的合法性,使用ObjectInputValidation 接口;
  4. 不直接或间接调用任何可继承的方法;

Item 89:For instance control, prefer enum types to readResolve

**为了实例控制尽量选择枚举而不是实现readResolve方法

如果使用readResovle方法来控制实例需要将所有字段声明为transient。使用枚举来进行实例控制是比较好的一种方式,但是如果需要实例控制的类无法在编译时获取到就没办法使用枚举了。

readResolve方法的访问权限非常重要,如果是声明在final类中,他应该是私有的;但如果在非final类中应该仔细考虑他的访问权限;

总的来说使用枚举来进行实例控制,实在不行提供readResolve方法进行实例控制,同时需要保证类中的属性都是私有的或者是transient。

Item 90: Consider serialization proxies instead of serialized instances

使用序列化代理模式来保证序列化安全

具体过程:

  1. 设计一个私有的静态内部类(序列化代理),其在逻辑上包含了外部类所有属性;
  2. 外部类添加一个writeReplace方法,这个方法返回一个序列化代理;
  3. 外部类添加readObject方法,并且在该方法中抛出异常,不允许直接的进行反序列化;
  4. 在序列化代理类中添加readResolve方法,该方法中调用外部类的公开构造方法进行构造返回对象;

实践可以查看Java EnumSet 源码:

    /**
     * This class is used to serialize all EnumSet instances, regardless of
     * implementation type.  It captures their "logical contents" and they
     * are reconstructed using public static factories.  This is necessary
     * to ensure that the existence of a particular implementation type is
     * an implementation detail.
     *
     * @serial include
     */
    private static class SerializationProxy <E extends Enum<E>>
        implements java.io.Serializable
    {
        /**
         * The element type of this enum set.
         *
         * @serial
         */
        private final Class<E> elementType;

        /**
         * The elements contained in this enum set.
         *
         * @serial
         */
        private final Enum<?>[] elements;

        SerializationProxy(EnumSet<E> set) {
            elementType = set.elementType;
            elements = set.toArray(ZERO_LENGTH_ENUM_ARRAY);
        }

        // instead of cast to E, we should perhaps use elementType.cast()
        // to avoid injection of forged stream, but it will slow the implementation
        @SuppressWarnings("unchecked")
        private Object readResolve() {
            EnumSet<E> result = EnumSet.noneOf(elementType);
            for (Enum<?> e : elements)
                result.add((E)e);
            return result;
        }

        private static final long serialVersionUID = 362491234563181265L;
    }

    Object writeReplace() {
        return new SerializationProxy<>(this);
    }

    // readObject method for the serialization proxy pattern
    // See Effective Java, Second Ed., Item 78.
    private void readObject(java.io.ObjectInputStream stream)
        throws java.io.InvalidObjectException {
        throw new java.io.InvalidObjectException("Proxy required");
    }

注释也非常清楚了,定义了一个SerializationProxy代理类,在这里面进行外部类的序列化,并且外部类的readObject是抛出了异常来避免非法的反序列化。

但是序列化代理模式有2点局限:

  1. 不兼容那些需要扩展的类
  2. 不兼容那些内部循环依赖和调用的类,如果这个时候尝试调用序列化代理类的readResolve方法会抛出类转换异常,因为这个时候还没有获取到对象,只是代理类而已;