尽管Java提供了序列化功能,但是却存在潜在的风险和性能问题。
Java的序列化是通过执行 readObject 方法来执行反序列化,这个方法可以初始化 classpath 下的任何实现了 serializable 接口的类,在反序列化二进制流的时候可以执行该类的代码,这就给攻击者提供了入口,造成可远程执行代码和拒绝服务漏洞。
所以如果不可避免的要使用序列化,避免去反序列化任何不可靠的字节流,
如果要反序列化可以使用白名单或者黑名单的方式来控制可以序列化的字节,同时使用Java9中新增的ObjectInputFilter,最好的方式就是不反序列化任何东西。在新写的任何系统里都没有使用序列化的理由。这个时候可以使用跨平台数据结构,比如JSON和protobuf。他们的不同在于JSON是基于文本便于阅读的,而protobuf是基于二进制且更为高效。
总的来说应该避免使用序列化,如果需要跨平台的支持可以考虑使用JSON或者protobuf,避免去反序列化不可信的数据,在写可序列化的类时需要格外注意。
慎重的考虑实现序列化,用于可继承的类很少实现序列化接口,接口本身更不要集成序列化接口。
如果一个类同时需要用于继承和实现序列化接口,类中又有不变的属性的话就需要额外关注,将该类声明为final或者覆盖 finalize方法,如果这些不变的属性有默认值的话,就需要 readObjectNoData 方法来避免:
// readObjectNoData for stateful extendable serializable classes private void readObjectNoData() throws InvalidObjectException { throw new InvalidObjectException("Stream data required"); }
内部类不应该实现序列化接口,由于内部类的存储方式不同,其序列化形式定义不明确,当然静态成员类是可以实现序列化接口的。
尽量在对象的实际属性和逻辑属性相互对应的情况下使用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 方法来保证不变性和安全性
如果对象的实际内容和逻辑上的内容不同会造成下面的问题:
在决定将一个字段标记为可序列化时,确保该字段是该对象逻辑属性的一部分,同时不需要序列化的字段可以使用 transient 标记,非序列化字段在反序列化时每个实例都会有一个默认值,比如对象默认是null,基本数字是0,布尔值是false。如果不能接受这些默认值,则需要提供 readObject 方法来反序列化这些值。
不管是否使用默认的序列化方式,在保证读取对象状态线程安全的情况下也要保证序列化的线程安全,比如给 wirteObject 方法加同步关键字 synchronized 。同时不管使用何种序列化方式都应该声明一个UID用于序列化,UID不一定要唯一,但是在修改了对象属性的情况下需要更新UID,不然会抛出InvalidClassException 。同时也注意为了保证已序列化对象的兼容性,不要去修改UID
总的来说需要仔细思考对象是否需要序列化以及需要序列化的字段,使用默认序列化考虑逻辑属性和实际属性是否对应,为了保证不同版本对象的序列化兼容性,在不同版本中应该尽量保证对象的完整。
大的来讲, readObject 方法也算是一个构造器,只不过它使用的是字节流来构造。字节流能够构造出普通构造器无法创建的非法对象。反序列化时,防御性的复制不能拥有对象的任何引用非常重要,攻击都是通过对引用的修改来进行。
因此,每个有可变属性的序列化对象都必须保护性的复制这些属性(比如新建一个对象)。同时构造方法和 readObject 方法均不能直接或间接的调用可继承方法,因为这些方法会在子类反序列化之前执行。
总的来说 readObject 方法总是能够创建一个合法的对象,不管提供的二进制流是怎么样的。
实现 readObject 方法需要注意:
readResolve
**为了实例控制尽量选择枚举而不是实现readResolve方法
如果使用readResovle方法来控制实例需要将所有字段声明为transient。使用枚举来进行实例控制是比较好的一种方式,但是如果需要实例控制的类无法在编译时获取到就没办法使用枚举了。
readResolve方法的访问权限非常重要,如果是声明在final类中,他应该是私有的;但如果在非final类中应该仔细考虑他的访问权限;
总的来说使用枚举来进行实例控制,实在不行提供readResolve方法进行实例控制,同时需要保证类中的属性都是私有的或者是transient。
具体过程:
实践可以查看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点局限:
版权所有丨转载请注明出处:https://minei.me/archives/539.html