我们知道,实现了序列化的类。在反序列化时,实例的创建是由readObject方法来完成的。由于这是一个不同于构造函数的创建类实例的通道,因此在构造函数中的状态约束条件在readObjetc中也得一条不落下的实现。这很让人头大。
而且readObject的出现,让伪字节流攻击和内部域的盗用攻击成为可能。伪字节流攻击就是伪造一个字节流,通过readObject读取,内部域的盗用攻击是指用一个外部类包含该类,用外部类的字段去指向该类的可变对象。
因此这里介绍一种模式,将实例的反序列化也交给改造函数来完成,即序列化代理模式。要想稳健的将带有重要约束条件的对象序列化时,这种模式可能是最容易的方法。代码如下:
public class Period implements Serializable{
private static final long serialVersionUID = 1L;
private final Date start;
private final Date end;
public Period(Date start, Date end) {
if(null == start || null == end || start.after(end)){
throw new IllegalArgumentException("请传入正确的时间区间!");
}
this.start = start;
this.end = end;
}
public Date start(){
return new Date(start.getTime());
}
public Date end(){
return new Date(end.getTime());
}
@Override
public String toString(){
return "起始时间:" + start + " , 结束时间:" + end;
}
/**
* 序列化外围类时,虚拟机会转掉这个方法,最后其实是序列化了一个内部的代理类对象!
* @return
*/
private Object writeReplace(){
System.out.println("进入writeReplace()方法!");
return new SerializabtionProxy(this);
}
/**
* 如果攻击者伪造了一个字节码文件,然后来反序列化也无法成功,因为外围类的readObject方法直接抛异常!
* @param ois
* @throws InvalidObjectException
*/
private void readObject(ObjectInputStream ois) throws InvalidObjectException{
throw new InvalidObjectException("Proxy required!");
}
/**
* 序列化代理类,他精确表示了其当前外围类对象的状态!最后序列化时会将这个私有内部内进行序列化!
*/
private static class SerializabtionProxy implements Serializable{
private static final long serialVersionUID = 1L;
private final Date start;
private final Date end;
SerializabtionProxy(Period p){
this.start = p.start;
this.end = p.end;
}
/**
* 反序列化这个类时,虚拟机会调用这个方法,最后返回的对象是一个Period对象!这里同样调用了Period的构造函数,
* 会进行构造函数的一些校验!
*/
private Object readResolve(){
System.out.println("进入readResolve()方法,将返回Period对象!");
// 这里进行保护性拷贝!
return new Period(new Date(start.getTime()), new Date(end.getTime()));
}
}
思路很清晰,拒绝外围类的readObject的调用,也就防止了伪字节流攻击和内部域的盗用攻击,同时无需保护性拷贝。