序列化/反序列化如何打破不变性?

时间:2013-07-11 21:00:12

标签: java

我在接受采访时被问到这个问题。面试官想知道如何使一个对象不可变。然后他问我如何序列化这个对象 - 它会破坏不变性吗?如果是,我该如何预防呢?谁能帮助我理解这个?

7 个答案:

答案 0 :(得分:10)

不可变对象是一旦创建就无法更改的对象。您可以使用private访问修饰符和final关键字来创建此类对象。

如果序列化了不可变对象,则可以修改其原始字节,以便在反序列化时对象不再相同。

这无法完全避免。加密,校验和和CRC将有助于防止这种情况。

答案 1 :(得分:5)

您应该阅读Joshua Bloch撰写的Effective Java。整篇章节讲述了与序列化相关的安全问题,以及如何正确设计课程的建议。

简而言之:您应该了解readObject和readResolve方法。

更详细的回答: 是序列化可以打破不变性。

我们假设你有上课时间(这是约书亚书中的例子):

private final class Period implements Serializable {
    private final Date start;
    private final Date end;

public Period(Date start, Date end){
    this.start = new Date(start.getTime());
    this.end = new Date(end.getTime());
    if(this.start.compareTo(this.end() > 0)
        throw new IllegalArgumentException("sth");
}
//getters and others methods ommited
}

看起来很棒。它是不可变的(你不能在初始化后改变开始和结束),优雅,小巧,线程安全等。

但是...

您必须记住序列化是另一种创建对象的方式(并且它不使用构造函数)。对象是从字节流构建的。

考虑某人(攻击者)更改序列化字节数组的情况。如果他做了这样的事情,他可以打破你开始的条件<结束。此外,攻击者有可能将流(传递给反序列化方法)引用到他的Date对象(这是可变的,并且Period类的不变性将被完全破坏)。

如果你不需要,最好的防御不是使用序列化。 如果必须序列化类,请使用序列化代理模式。

编辑(在kurzbot请求): 如果要使用序列化代理,则必须在Period内添加静态内部类。此类对象将用于序列化而不是Period类对象。

在Period类中编写两个新方法:

private Object writeReplace(){
    return new SerializationProxy(this);
}

private void readObject(ObjectInputStream stream) throws InvalidObjectException {
    throw new InvalidObjectException("Need proxy");
}

第一种方法用SerializationProxy对象替换默认的序列化Period对象。第二个保证攻击者不会使用标准的readObject方法。

您应该为SerializationProxy编写writeObject方法,以便您可以使用:

private Object readResolve() {
    return new Period(start, end);
}

在这种情况下,您只使用公共API并且确定Period类将保持不变。

答案 2 :(得分:4)

当序列化对同一对象有多个引用的对象图时,序列化程序会注意到这一事实,因此反序列化的对象图具有相同的结构。

例如,

int[] none = new int[0];
int[][] twoArrays = new int[] { none, none };
System.out.print(twoArrays[0] == twoArrays[1]);

将打印true,如果您序列化并反序列化twoArrays,那么您将获得相同的结果,而不是数组中的每个元素都是不同的对象,如

int[][] twoDistinctArrays = new int[] { new int[0], new int[0] };

您可以利用此支持进行参考共享,以便在序列化条目之后创建字节,以与私有帮助对象或数组共享引用,然后进行变异。

因此,一个不可序列化的类可以维护不变量 - 一个私有对象不会逃避 - 一个可序列化的类无法维护。

答案 3 :(得分:1)

通过将所有状态信息保存在创建对象后无法更改的表单中,使其不可变。

Java在某些情况下不允许完美的不变性。

Serializable是你可以做的事情,但它并不完美,因为在反序列化时必须有一种方法来重新创建对象的精确副本,并且可能不足以使用相同的构造函数来反序列化和创建对象首先。这留下了一个漏洞。

有些事要做:

  • 除私人或最终财产外,别无其他。
  • 构造函数设置对操作至关重要的任何属性。

要考虑的其他一些事情:

  • 静态变量可能是一个坏主意,尽管静态最终常量不是问题。在加载类时无法从外部设置这些,但是不能在以后再次设置它们。
  • 如果传递给构造函数的其中一个属性是对象,则调用者可以保留对该对象的引用,如果它也不是不可变的,则更改该对象的某些内部状态。这有效地改变了对象的内部状态,该对象存储了一个现在被修改的对象的副本。
  • 理论上,有人可以采用序列化形式并对其进行更改(或者只是从头开始构建序列化形式),然后使用它进行反序列化,从而创建对象的修改版本。 (我认为在大多数情况下这可能不值得担心。)
  • 您可以编写自定义序列化/反序列化代码来签署序列化表单(或加密它),以便可以检测到修改。或者您可以使用序列化表单的某种形式的传输,以确保它不会被更改。 (这假设您在不运输时可以控制序列化表格。)
  • 有字节码操纵器可以对对象执行任何操作。例如,将setter方法添加到其他不可变对象。

简单的答案是,在大多数情况下,只需遵循本答案顶部的两条规则即可,足以满足您对不变性的需求。

答案 4 :(得分:1)

正如其他人所说的那样,人们可以认为序列化产生了一个全新的对象,然后是不可改变的,所以不,序列化不会破坏它,但我认为我们必须考虑的不变性有更大的图景在回答这个问题之前。

我认为真正的答案完全取决于被序列化的类,以及所需的不变性水平,但由于访调员未能给我们提供源代码,我会想出自己的。我还想指出,一旦人们开始谈论不变性,他们就会开始抛出final关键字 - 是的,这使得引用不可变,但它不是实现不变性的唯一方法。好的,我们来看一些代码:

public class MyImmutableClass implements Serializable{
    private double value;

    public MyImmutableClass(double v){
        value = v;
    }

    public double getValue(){ return value; }
}

这个类是否可变,因为我实现了Serializable?它是否可变,因为我没有使用final关键字?没办法 - 它在每个实际意义上都是不可变的,因为我不会修改源代码(即使你很好地问我),但更重要的是,它是不可变的,因为没有外部类可以改变{的值{1}},不使用Reflection将其公开然后修改它。通过该令牌,我想你可以运行一些中间十六进制编辑器并手动修改RAM中的值,但这并不会使它比以前更可变。扩展类也不能修改它。当然,您可以对其进行扩展,然后覆盖value以返回不同的内容,但这样做不会更改基础getValue()

我知道这可能会让很多人误解,但我认为不变性通常纯粹是语义上的 - 例如有人从外部类调用你的代码是不可变的,还是在主板上使用BusPirate的人是不可变的?有很好的理由使用value来帮助确保不变性,但我认为它的重要性在很多论点中被夸大了。仅仅因为JVM被允许在引擎盖下做一些魔术以确保序列化工作并不意味着应用程序所需的不变性水平在某种程度上被破坏了。

答案 5 :(得分:0)

简单的答案是

class X implements Serializable {
    private final transient String foo = "foo";
}

如果对象是新创建的,则字段foo将等于“foo”,但在反序列化时将为null(并且不使用脏技巧,您将无法分配它)。

答案 6 :(得分:0)

您可以使用SecurityManager在Java中防止序列化或克隆

public final class ImmutableBean {
private final String name;

public ImmutableBean(String name) {
    this.name = name;
    //this line prevent it form serialization and reflection
    System.setSecurityManager(new SecurityManager());
}

public String getName() {
    return name;
}

}