Как сериализация/десериализация прерывают неизменность?

Мне задали этот вопрос в интервью. Интервьюер хотел знать, как сделать объект неизменным. и затем он спросил, что, если я сериализую этот объект - будет ли он нарушать неизменность? Если да, как я могу это предотвратить? Может ли кто-нибудь помочь мне понять это?

Ответ 1

Неизменяемый объект - это тот, который нельзя изменить после создания. Вы можете создать такой объект с помощью модификаторов доступа private и ключевого слова final.

Если неизменяемый объект был сериализован, его необработанные байты могут быть изменены таким образом, чтобы при десериализации объект уже не был таким же.

Это невозможно полностью предотвратить. Шифрование, контрольные суммы и CRC помогут предотвратить это.

Ответ 2

Вы должны прочитать Эффективную Java, написанную Джошуа Блохом. Существует целая глава о проблемах безопасности, связанных с сериализацией, и советы по правильному дизайну вашего класса.

В нескольких словах: вы должны узнать о методах readObject и readResolve.

Более подробный ответ: Да, сериализация может нарушить неизменность.

Предположим, у вас есть класс Period (пример из книги Джошуа):

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 будет полностью разрушена).

Лучшая защита не использует сериализацию, если вам это не нужно. Если вам нужно сериализовать свой класс, используйте шаблон прокси-сервера Serialization.

Изменить (по запросу kurzbot): Если вы хотите использовать Serialization Proxy, вам нужно добавить статический внутренний класс внутри Period. Эти объекты класса будут использоваться для сериализации вместо объектов класса Period.

В классе Period напишите два новых метода:

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

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

Первый метод заменит сериализованный по умолчанию объект Period с объектом SerializationProxy. Вторая гарантия того, что злоумышленник не будет использовать стандартный метод readObject.

Вы должны написать метод writeObject для SerializationProxy, чтобы вы могли использовать:

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

В этом случае вы используете только открытый API и уверены, что класс Period останется неизменным.

Ответ 3

Когда вы сериализуете граф объектов, который имеет несколько ссылок на один и тот же объект, сериализатор отмечает этот факт, так что граф десериализованного объекта имеет одинаковую структуру.

Например,

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] };

Вы можете использовать эту поддержку для совместного использования ссылок, чтобы обрабатывать байты после сериализованной записи, чтобы поделиться ссылкой с частным объектом или массивом справки, а затем изменить ее.

Таким образом, неэриализуемый класс может поддерживать инварианты - что частный объект не убегает - что сериализуемый класс не может поддерживать.

Ответ 4

Сделать его неизменным, сохраняя всю информацию о состоянии в форме, где она не может быть изменена после создания объекта.

В некоторых случаях Java не допускает идеальной неизменности.

Serializable - это то, что вы можете сделать, но это не идеально, потому что должен быть способ воссоздать точную копию объекта при десериализации, и может быть недостаточно использовать те же конструкторы для десериализации и создания объект в первую очередь. Это оставляет дыру.

Некоторые вещи:

  • Ничего, кроме частных или окончательных свойств.
  • Конструктор устанавливает любое из тех свойств, которые имеют решающее значение для работы.

Некоторые другие вещи, о которых нужно подумать:

  • статические переменные, вероятно, являются плохими идеями, хотя статическая конечная константа не является проблемой. Невозможно установить их извне при загрузке класса, но не удалять их позже.
  • Если одно из свойств, переданных конструктору, является объектом, вызывающий может хранить ссылку на этот объект и, если он не является также неизменным, изменить некоторое внутреннее состояние этого объекта. Это эффективно изменяет внутреннее состояние вашего объекта, который сохранил копию этого, теперь измененного объекта.
  • кто-то может теоретически взять сериализованную форму и изменить ее (или просто построить сериализованную форму с нуля), а затем использовать ее для десериализации, создав таким образом модифицированную версию объекта. (Я полагаю, что это, вероятно, не стоит беспокоиться в большинстве случаев.)
  • вы можете написать собственный код serialize/deserialize, который подписывает сериализованную форму (или шифрует ее), чтобы изменения были обнаружены. Или вы можете использовать некоторую форму передачи сериализованной формы, которая гарантирует, что она не будет изменена. (Это предполагает, что у вас есть некоторый контроль над сериализованной формой, когда он не находится в пути.)
  • Существуют манипуляторы байтового кода, которые могут делать все, что захотят для объекта. Например, добавьте метод setter в неизменяемый объект.

Простой ответ заключается в том, что в большинстве случаев просто следуйте двум правилам в верхней части этого ответа, и это будет достаточно хорошо, чтобы удовлетворить ваши потребности в неизменности.

Ответ 5

Как говорили другие, можно было бы утверждать, что сериализация приводит к созданию совершенно нового объекта, который затем является неизменным, поэтому нет, сериализация не нарушает его, но я думаю, что существует большая картина неизменности, которую мы должны рассмотреть прежде чем ответить на этот вопрос.

Я думаю, что реальный ответ полностью зависит от сериализованного класса и требуемого уровня неизменности, но поскольку интервьюер не дал нам исходный код, я придумаю свой собственный. Я также хотел бы отметить, что, как только люди начинают говорить о неизменности, они начинают метать ключевое слово final - да, это делает ссылку неизменной, но это не единственный способ добиться неизменности. Хорошо, давайте посмотрим на код:

public class MyImmutableClass implements Serializable{
    private double value;

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

    public double getValue(){ return value; }
}

Является ли этот класс изменчивым, потому что я реализовал Serializable? Является ли это изменчивым, потому что я не использовал ключевое слово final? Ни в коем случае - он неизменен в каждом практическом смысле этого слова, потому что я не буду изменять исходный код (даже если вы попросите меня красиво), но что более важно, он неизменен, потому что никакой внешний класс не может изменить значение value, не используя Reflection, чтобы сделать его общедоступным, а затем изменив его. Под этим маркером, я полагаю, вы могли бы запустить некоторый промежуточный шестнадцатеричный редактор и вручную изменить значение в ОЗУ тоже, но это не делает его более изменчивым, чем раньше. Расширение классов также не может изменить его. Конечно, вы можете расширить его, а затем переопределить getValue(), чтобы вернуть что-то другое, но при этом не будет изменено базовое value.

Я знаю, что это может испортить много людей неправильным образом, но я считаю, что неизменность часто является чисто смысловой - например. является ли он неизменным для кого-то, вызывающего ваш код из внешнего класса, или он неизменен от кого-то, использующего BusPirate на вашей материнской плате? Есть очень веские причины использовать final, чтобы помочь обеспечить неизменность, но я думаю, что это значение значительно преувеличено более чем в нескольких аргументах. Просто потому, что JVM разрешено делать какую-то магию под капотом, чтобы гарантировать, что работа по сериализации не означает, что уровень неизменности, требуемый вашим приложением, как-то сломан.

Ответ 6

Самый простой ответ -

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

Поле foo будет равно "foo", если объект вновь создан, но будет нулевым при десериализации (и не прибегая к грязным трюкам, вы не сможете его назначить).

Ответ 7

Вы можете предотвратить сериализацию или клонирование с помощью 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;
}

}