java安全编码指南之:序列化Serialization

简介

序列化是java中一个很是经常使用又会被人忽视的功能,咱们将对象写入文件须要序列化,同时,对象若是想要在网络上传输也须要进行序列化。java

序列化的目的就是保证对象能够正确的传输,那么咱们在序列化的过程当中须要注意些什么问题呢?git

一块儿来看看吧。github

序列化简介

若是一个对象要想实现序列化,只须要实现Serializable接口便可。网络

奇怪的是Serializable是一个不须要任何实现的接口。若是咱们implements Serializable可是不重写任何方法,那么将会使用JDK自带的序列化格式。函数

可是若是class发送变化,好比增长了字段,那么默认的序列化格式就知足不了咱们的需求了,这时候咱们须要考虑使用本身的序列化方式。this

若是类中的字段不想被序列化,那么可使用transient关键字。代理

一样的,static表示的是类变量,也不须要被序列化。code

注意serialVersionUID

serialVersionUID 表示的是对象的序列ID,若是咱们不指定的话,是JVM自动生成的。在反序列化的过程当中,JVM会首先判断serialVersionUID 是否一致,若是不一致,那么JVM会认为这不是同一个对象。对象

若是咱们的实例在后期须要被修改的话,注意必定不要使用默认的serialVersionUID,不然后期class发送变化以后,serialVersionUID也会一样的发生变化,最终致使和以前的序列化版本不兼容。教程

writeObject和readObject

若是要本身实现序列化,那么能够重写writeObject和readObject两个方法。

注意,这两个方法是private的,而且是non-static的:

private void writeObject(final ObjectOutputStream stream)
    throws IOException {
  stream.defaultWriteObject();
}
 
private void readObject(final ObjectInputStream stream)
    throws IOException, ClassNotFoundException {
  stream.defaultReadObject();
}

若是不是private和non-static的,那么JVM就不可以发现这两个方法,就不会使用他们来作自定义序列化。

readResolve和writeReplace

若是class中的字段比较多,而这些字段均可以从其中的某一个字段中自动生成,那么咱们其实并不须要序列化全部的字段,咱们只把那一个字段序列化就能够了,其余的字段能够从该字段衍生获得。

readResolve和writeReplace就是序列化对象的代理功能。

首先,序列化对象须要实现writeReplace方法,表示替换成真正想要写入的对象:

public class CustUserV3 implements java.io.Serializable{

    private String name;
    private String address;

    private Object writeReplace()
            throws java.io.ObjectStreamException
    {
        log.info("writeReplace {}",this);
        return new CustUserV3Proxy(this);
    }
}

而后在Proxy对象中,须要实现readResolve方法,用于从系列化过的数据中重构序列化对象。以下所示:

public class CustUserV3Proxy implements java.io.Serializable{

    private String data;

    public CustUserV3Proxy(CustUserV3 custUserV3){
        data =custUserV3.getName()+ "," + custUserV3.getAddress();
    }

    private Object readResolve()
            throws java.io.ObjectStreamException
    {
        String[] pieces = data.split(",");
        CustUserV3 result = new CustUserV3(pieces[0], pieces[1]);
        log.info("readResolve {}",result);
        return result;
    }
}

咱们看下怎么使用:

public void testCusUserV3() throws IOException, ClassNotFoundException {
        CustUserV3 custUserA=new CustUserV3("jack","www.flydean.com");

        try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(custUserA);
        }

        try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){
            ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            CustUserV3 custUser1 = (CustUserV3) objectInputStream.readObject();
            log.info("{}",custUser1);
        }
    }

注意,咱们写入和读出的都是CustUserV3对象。

不要序列化内部类

所谓内部类就是未显式或隐式声明为静态的嵌套类,为何咱们不要序列化内部类呢?

  • 序列化在非静态上下文中声明的内部类,该内部类包含对封闭类实例的隐式非瞬态引用,从而致使对其关联的外部类实例的序列化。
  • Java编译器对内部类的实如今不一样的编译器之间可能有所不一样。从而致使不一样版本的兼容性问题。
  • 由于Externalizable的对象须要一个无参的构造函数。可是内部类的构造函数是和外部类的实例相关联的,因此它们没法实现Externalizable。

因此下面的作法是正确的:

public class OuterSer implements Serializable {
  private int rank;
  class InnerSer {
    protected String name;
  }
}

若是你真的想序列化内部类,那么把内部类置为static吧。

若是类中有自定义变量,那么不要使用默认的序列化

若是是Serializable的序列化,在反序列化的时候是不会执行构造函数的。因此,若是咱们在构造函数或者其余的方法中对类中的变量有必定的约束范围的话,反序列化的过程当中也必需要加上这些约束,不然就会致使恶意的字段范围。

咱们举几个例子:

public class SingletonObject implements Serializable {
    private static final SingletonObject INSTANCE = new SingletonObject ();
    public static SingletonObject getInstance() {
        return INSTANCE;
    }
    private SingletonObject() {
    }

    public static Object deepCopy(Object obj) {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            new ObjectOutputStream(bos).writeObject(obj);
            ByteArrayInputStream bin =
                    new ByteArrayInputStream(bos.toByteArray());
            return new ObjectInputStream(bin).readObject();
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }

    public static void main(String[] args) {
        SingletonObject singletonObject= (SingletonObject) deepCopy(SingletonObject.getInstance());
        System.out.println(singletonObject == SingletonObject.getInstance());
    }
}

上面是一个singleton对象的例子,咱们在其中定义了一个deepCopy的方法,经过序列化来对对象进行拷贝,可是拷贝出来的是一个新的对象,尽管咱们定义的是singleton对象,最后运行的结果仍是false,这就意味着咱们的系统生成了一个不同的对象。

怎么解决这个问题呢?

加上一个readResolve方法就能够了:

protected final Object readResolve() throws NotSerializableException {
        return INSTANCE;
    }

在这个readResolve方法中,咱们返回了INSTANCE,以确保其是同一个对象。

还有一种状况是类中字段是有范围的。

public class FieldRangeObject implements Serializable {

    private int age;

    public FieldRangeObject(int age){
        if(age < 0 || age > 100){
            throw new IllegalArgumentException("age范围不对");
        }
        this.age=age;
    }
}

上面的类在反序列化中会有什么问题呢?

由于上面的类在反序列化的过程当中,并无对age字段进行校验,因此,恶意代码可能会生成超出范围的age数据,当反序列化以后就溢出了。

怎么处理呢?

很简单,咱们在readObject方法中进行范围的判断便可:

private  void readObject(java.io.ObjectInputStream s)
            throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField fields = s.readFields();
        int age = fields.get("age", 0);
        if (age > 100 || age < 0) {
            throw new InvalidObjectException("age范围不对!");
        }
        this.age = age;
    }

不要在readObject中调用可重写的方法

为何呢?readObject其实是反序列化的构造函数,在readObject方法没有结束以前,对象是没有构建完成,或者说是部分构建完成。若是readObject调用了可重写的方法,那么恶意代码就能够在方法的重写中获取到还未彻底实例化的对象,可能形成问题。

本文的代码:

learn-java-base-9-to-20/tree/master/security

本文已收录于 http://www.flydean.com/java-security-code-line-serialization/

最通俗的解读,最深入的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注个人公众号:「程序那些事」,懂技术,更懂你!