乐趣区

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

简介

序列化是 java 中一个十分罕用又会被人漠视的性能,咱们将对象写入文件须要序列化,同时,对象如果想要在网络上传输也须要进行序列化。

序列化的目标就是保障对象能够正确的传输,那么咱们在序列化的过程中须要留神些什么问题呢?

一起来看看吧。

序列化简介

如果一个对象要想实现序列化,只须要实现 Serializable 接口即可。

奇怪的是 Serializable 是一个不须要任何实现的接口。如果咱们 implements Serializable 然而不重写任何办法,那么将会应用 JDK 自带的序列化格局。

然而如果 class 发送变动,比方减少了字段,那么默认的序列化格局就满足不了咱们的需要了,这时候咱们须要思考应用本人的序列化形式。

如果类中的字段不想被序列化,那么能够应用 transient 关键字。

同样的,static 示意的是类变量,也不须要被序列化。

留神 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/

最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!

欢送关注我的公众号:「程序那些事」, 懂技术,更懂你!

退出移动版