Главная страница
Навигация по странице:

  • // ... // Остальное опущено

  • // Этот поток байтов не мог быть получен из реального экземпляра

  • // Сериализуем правильный экземпляр Period out.writeObject(new Period(new Date(), new Date()));/* * Добавляем вконец неконтролируемые "ссылки

  • // Десериализация экземпляра Period и "украденных" ссылок // на экземпляры Date

  • // Резервное копирование изменяемых компонентов start = new Date(start.getTime()); end = new Date(end.getTime()); // Проверка инвариантов

  • // Возвращает только истинный экземпляр Elvis и дает возможность // сборщику мусора позаботиться об Е - самозванце

  • // Идиома защищенного метода readResolve

  • Effective Java tmprogramming Language GuideJ o s h u a b lo c h


    Скачать 1.05 Mb.
    НазваниеEffective Java tmprogramming Language GuideJ o s h u a b lo c h
    Дата03.04.2018
    Размер1.05 Mb.
    Формат файлаpdf
    Имя файлаBlokh_Dzh_-_Java_Effektivnoe_programmirovanie.pdf
    ТипДокументы
    #40178
    страница25 из 25
    1   ...   17   18   19   20   21   22   23   24   25

    /**
    * @param start - начало периода * @param end - конец периода, не должен предшествовать началу периода * @throws IllegalArgument – если начало периода указано после конца * @throws NullPointerException – если начало или конец периода нулевые */
    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(start +" > "+ end);
    }
    public Date start () { return (Date) start.clone(); }
    public Date end () { return (Date) end.clone(); }
    public String toString() { return start + " - " + end; }
    // ... // Остальное опущено
    Предположим, что вам необходимо сделать этот класс сериализуемым. Поскольку физическое представление объекта Period в точности отражает его логическое содержание, вполне можно воспользоваться сериализованной формой, предлагаемой по умолчанию (статья 55). Может показаться, что все, что вам нужно для того, чтобы класс был сериализуемым,- это добавить в его декларацию слова "implements Serializable". Если вы поступите таким образом, то гарантировать классу сохранение его критически важных инвариантов будет невозможно. Проблема заключается в том, что метод readObject фактически является еще одним открытым конструктором и потому требует такого же внимания, как и любой другой конструктор. Точно также, как конструктор, метод readObject должен проверять правильность своих аргументов (статья 23) и при необходимости создавать для параметров резервные копии. Если метод readObject не выполнит хотя бы одно из этих условий, злоумышленник сможет относительно легко нарушить инварианты этого класса.
    211

    Метод readObject - это конструктор, который в качестве единственного входного параметра принимает поток байтов. В нормальных условиях этот поток байтов создается в результате сериализации нормально построенного экземпляра. Проблема возникает, когда метод readObject сталкивается с потоком байтов, полученным искусственно с целью генерации объекта, который нарушает инварианты этого класса. Допустим, что мылишь добавили "implements Serializable" в декларацию класса Period. Следующая уродливая программа генерирует такой экземпляр класса
    Period, в котором конец периода предшествует началу import java.io.*;
    public class BogusPeriod {
    // Этот поток байтов не мог быть получен из реального экземпляра
    private static final byte[] serializedForm = new byte[] {
    (byte)0xac, (byte)0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x06,
    0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x40, 0x7e, (byte)0xf8,
    0x2b, 0x4f, 0x46, (byte)0xc0, (byte)0xf4, 0x02, 0x00, 0x02,
    0x4c, 0x00, 0x03, 0x65, 0x6e, 0x64, 0x74, 0x00, 0x10, 0x4c,
    0x6a, 0x61, 0x76, 0x61, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x2f,
    0x44, 0x61, 0x74, 0x65, 0x3b, 0x4c, 0x00, 0x05, 0x73, 0x74,
    0x61, 0x72, 0x74, 0x71, 0x00, 0x7e, 0x00, 0x01, 0x78, 0x70,
    0x73, 0x72, 0x00, 0x0e, 0x6a, 0x61, 0x76, 0x61, 0x2e, 0x75,
    0x74, 0x69, 0x6c, 0x2e, 0x44, 0x61, 0x74, 0x65, 0x68, 0x6a,
    (byte)0x81, 0x01, 0x4b, 0x59, 0x74, 0x19, 0x03, 0x00, 0x00,
    0x78, 0x70, 0x77, 0x08, 0x00, 0x00, 0x00, 0x66, (byte)0xdf,
    0x6e, 0x1e, 0x00, 0x78, 0x73, 0x71, 0x00, 0x7e, 0x00, 0x03,
    0x77, 0x08, 0x00, 0x00, 0x00, (byte)0xd5, 0x17, 0x69, 0x22,
    0x00, 0x78 };
    public static void main(String[] args) {
    Period p = (Period) deserialize(serializedForm);
    System.out.println(p); } // Возвращает объект с указанной сериализованной формой public static Object deserialize(byte[] sf) {
    try {
    InputStream is = new ByteArrayInputStream(sf);
    ObjectInputStream ois = new ObjectInputStream(is);
    return ois.readObject();
    } catch (Exception e) {
    throw new IllegalArgumentException(e.toString());
    } } }
    212
    Фиксированный массив байтов, используемый для инициализации массива е, был получен путем сериализации обычного экземпляра Реи последующего редактирования потока байтов вручную. Детали построения потока для данного примера значения не имеют, однако если вам это любопытно, формат потока байтов описан в "Java
    тм
    O bject п п" [Serialization, 6]. Если вы запустите эту программу, она напечатает "Fri Jan 01 12: 00: 00 PST 1999 п Jan 01 12:00:00 PST
    1984". Таким образом, то, что класс Period стал сериализуемым, позволило создать объект, который нарушает инварианты этого класса. Для решения этой проблемы создадим в классе Period метод readObject, который будет вызывать defaultReadObject и проверять правильность десериализованного объекта. Если проверка покажет ошибку, метод readObject инициирует исключение !
    nvalidObjectException, что не позволит закончить десериализацию: private void readObject(ObjectlnputStream s) throws IOException, ClassNotFoundException {
    s.defaultReadObject();
    // Проверим правильность инвариантов if (start.compareTo(end) > 0) throw new InvalidObjectException(start +" after "+ end); Это решение не позволит злоумышленнику создать неправильный экземпляр класса Period. Однако здесь притаилась еще одна, более тонкая проблема. Можно создать изменяемый экземпляр Ре riod, Сфабриковав поток байтов, который начинается потоком байтов, представляющим правильный экземпляра затем формирует дополнительные ссылки на закрытые поля Date в этом экземпляре. Злоумышленник может прочесть экземпляр Period из ObjectlnputStream и получить "неконтролируемые ссылки на объекты, прилагаемые к этому потоку. Имея указанные ссылки, злоумышленник получает доступ к объектам, на которые есть ссылки в закрытых полях Date объекта
    Period. Меняя эти экземпляры Date, он может менять и сам экземпляр Period. Следующий класс демонстрирует атаку такого рода import java.util.*;
    import java.io.*;
    public class MutablePeriod {
    // Экземпляр интервала времени public final Period period;
    // Поле начала периода, к которому мы не должны иметь доступ public final Date start;
    // Поле конца периода, к которому мы не должны иметь public final Date end;
    public MutablePeriod() {
    try {
    ByteArrayOutputStream bos =
    new ByteArrayOutputStream();
    ObjectOutputStream out =
    new ObjectOutputStream(bos);
    213

    // Сериализуем правильный экземпляр Period
    out.writeObject(new Period(new Date(), new Date()));
    /*
    * Добавляем вконец неконтролируемые "ссылки
    * на предыдущие объекты" для внутренних полей Date
    * в экземпляре Period. Подробнее см. "Java Object * Serialization Specification", раздел
    6.4.
    */
    byte[] ref = { 0x71, 0, 0x7e, 0, 5 }; // Ref #5
    bos.write(ref); // The start field ref[4] = 4; // Ref # 4
    bos.write(ref); // The end field
    // Десериализация экземпляра Period и "украденных" ссылок
    // на экземпляры Date
    ObjectInputStream in = new ObjectInputStream(
    new ByteArrayInputStream(bos.toByteArray()));
    period = (Period) in.readObject();
    start = (Date) in.readObject();
    end = (Date) in.readObject();
    } catch (Exception e) {
    throw new RuntimeException(e.toString());
    }
    Чтобы увидеть описанную атаку в действии, запустим следующую программу public static void main(String[] args) {
    MutablePeriod mp = new MutablePeriod();
    Period p = mp.period;
    Date pEnd = mp.end;
    // Let's turn back the clock pEnd.setYear(78);
    System.out.println(p);
    // Bring back the 60's!
    pEnd.setYear(69);
    System.out.println(p);
    }
    }
    214
    Запустив эту программу, на выходе получим следующее
    Wed Маг 07 23:30:01 PST 2001 - Tue Маг 07 23:30:01 PST 1978
    Wed Маг 07 23:30:01 PST 2001 - Fri Маг 07 23:30:01 PST 1969 Хотя экземпляр Ре создается с неповрежденными инвариантами, при желании его внутренние компоненты можно поменять извне. Завладев изменяемым экземпляром класса Period, злоумышленник может причинить массу вреда, передав этот экземпляр классу, чья безопасность зависит от неизменяемости класса Period. И это не такая уж надуманная тема. Существуют классы, чья безопасность зависит от неизменяемости класса String. Причина этой проблемы кроется в том, что метод readObject класса Ре не выполняет необходимого резервного копирования. При десериализации объекта крайне важно создать резервные копии для всех полей, содержащих ссылки нате объекты, которые не должны попасть в распоряжение клиентов. Поэтому каждый сериализуемый неизменяемый класс, содержащий закрытые изменяемые компоненты, должен в своем методе readObject создавать резервные копии для этих компонентов. Следующий метод readObject достаточен для того, чтобы объект Ре оставался неизменяемыми сохранялись его инварианты private void readObfect(ObjectInputStream s) throws IOException, ClassNotFoundException {
    s.defaultReadObject();
    // Резервное копирование изменяемых компонентов start = new Date(start.getTime()); end = new Date(end.getTime());
    // Проверка инвариантов
    if (start.compareTo(end) > 0) throw new InvalidObjectException(start +" after "+ end); Заметим, что резервное копирование осуществляется перед проверкой корректности и что для резервного копирования не используется метод clone из класса Date. Указанные особенности реализации необходимы для защиты объекта Period (статья 24). Заметим также, что выполнить резервное копирование для полей final невозможно. Следовательно, для того чтобы можно было воспользоваться методом readObject, мы должны сделать поля start и end неокончательными. Это огорчает, но приходится выбирать из двух зол меньшее. Разместив в классе метод readObject и удалив модификатор final из полей start и end, мы обнаруживаем, что класс MutablePeriod потерял свою силу. Приведенная выше программа выводит теперь следующие строки
    Thu Маг 08 00:03:45 PST 2001 - Thu Маг 08 00:03:45 PST 2001
    Thu Маг 08 00:03:45 PST 2001 - Thu Маг 08 00:03:45 PST 2001 215
    Есть простой безошибочный тест, показывающий, приемлем ли метод readObject, предлагаемый по умолчанию. Будете ли вы чувствовать себя уютно, если добавите в класс открытый конструктор, который в качестве параметров принимает значения полей вашего объекта, записываемых в сериализованную форму, а затем заносит эти значения в соответствующие поля без какой-либо проверки Если вы не можете ответить на этот вопрос утвердительно, вам нужно явным образом реализовать метод readObject, который должен выполнять все необходимые проверки параметров и создавать все резервные копии, как это требуется от конструктора. Между конструкторами и методами readObject существует еще одно сходство, касающееся расширяемых сериализуемых классов метод readObject не должен вызывать переопределяемые методы ни прямо, ни косвенно (статья
    15). Если это правило нарушено и вызываемый метод переопределен, то он будет вызван прежде, чем будет десериализовано состояние соответствующего подкласса. Скорее всего, это приведет к сбою программы. Подведем итоги. Всякий раз, когда выпишите метод readObject, относитесь к нему как к открытому конструктору, который должен создавать правильный экземпляр независимо оттого, какой поток байтов был ему передан. Не надо исходить из того, что полученный поток байтов действительно представляет сериализованный экземпляр. Мы рассмотрели примеры классов, использующих сериализованную форму, предлагаемую по умолчанию. В той же степени все это относится к классам со специальными сериализованными формами. Приведем в кратком изложении рекомендации по написанию "пуленепробиваемого" метода readObject: для классов, где есть поля, которые хранят ссылки на объект и при этом должны оставаться закрытыми, для каждого объекта, который должен быть помещен в такое поле, необходимо создавать резервную копию. В эту категорию попадают также изменяемые компоненты неизменяемых классов. Для классов, где есть инварианты, выполняйте проверку этих инвариантов ив случае ошибки инициируйте исключение InvalidObjectException. Проверка должна производиться после создания всех резервных копий. Если после десериализации необходимо проверить целый грф объектов, следует использовать интерфейс ObjectlnputValidation. Порядок применения этого интерфейса выходит за рамки данной книги. Пример можно найти в
    The Java Class Libraries. п п. т e 1 (Chan98, стр.
    1256]. Ни прямо, ни косвенно не используйте в этом классе переопределяемых методов. Как альтернативу защищенному методу readObject можно использовать метод readResolve. Об этом говорится в статье 57.
    216
    При необходимости создавайте метод В статье
    2 описывается шаблон
    Singleton и приводится следующий пример класса. синглтона. Этот класс ограничивает доступ к конструктору стем, чтобы гарантировать создание только одного экземпляра public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elvis() { }
    // Остальное опущено Как отмечалось в статье
    2, этот класс перестает быть синглтоном, если в его декларацию добавляются слова "implements Serializable". Не имеет значения, использует этот класс сериализованную форму, предлагаемую по умолчанию, или специальную форму (статья
    55). Не имеет также значения, предоставляется ли пользователю в этом классе явный метод readObject (статья
    56). Любой метод readObject, явный или применяемый по умолчанию, возвращает вновь созданный экземпляра не тот, что был сформирован в момент инициализации класса. До выхода версии
    1.2 написать сериализуемый класс-синглтон было невозможно. В версии 1.2 механизм сериализации пополнился функцией readResolve [Serialization, 3.6]. Если класс десериализуемого объекта имеет метод readResolve с соответствующей декларацией, то по завершении десериализации этот метод будет вызван для вновь созданного объекта. Вместо ссылки на вновь созданный объект клиенту передается ссылка, возвращаемая этим методом. В большинстве случаев использования этого механизма ссылка на новый объект не сохраняется, асам объект фактически является мертворожденными немедленно становится объектом для сборки мусора. Если класс Elvis создан так, чтобы реализовать интерфейс Serializable, то для обеспечения свойства синглтона достаточно будет создать следующий метод readResolve: private Object readResolve() throws ObjectStreamException {
    // Возвращает только истинный экземпляр Elvis и дает возможность
    // сборщику мусора позаботиться об Е - самозванце
    return INSTANCE; }
    217
    Метод игнорирует десериализованный объект, просто возвращая уникальный экземпляр
    Elvis, который был создан вовремя инициализации класса. По этой причине необходимо, чтобы в сериализованной форме экземпляра
    Elvis не содержалось никаких реальных данных, а все поля экземпляра были помечены как transient. Это относится не только к классу
    Elvis, но и К любым другим синглтонам. Метод readResolve необходим не только для синглтонов, но и для всех остаЛЬНblХ классов, контролирующих свои экземпляры
    (instance-controlled), те. для тех классов, в которых сохранение некоего инварианта требует строгого контроля над процедурой создания экземпляров. Еще один пример класса, контролирующего свои экземпляры- перечисление типов статья
    21), чей метод readResolve должен возвращать канонический экземпляр, соответствующий указанной константе в перечислении. Главное правило таково, что если выпишете сериализуемый класс, не имеющий ни открытых, ни защищенных конструкторов, проверьте, нужен ли этому классу метод radResolve. Второй вариант применения метода readResolve - в качестве безопасной альтернаТИВbI защищенному методу readObject статья 56). В этом случае из метода readObject изымаются все проверки параметров, а также процедуры создания резервных копий, и применяются проверки и резервное копирование, предоставляемые обычным конструктором. При использовании сериализованной формы, предлагаемой по умолчанию, метод readObject может быть полностью исключен. Как объясняется в статье
    56, это дает возможность клиенту, имеющему злой умысел, создать экземпляр с испорченными инвариантами. Однако потенциально испорченный десериализованный экземпляр никогда не будет передан в активное использование. Из него просто будут извлечены данные для открытого конструктора или статического метода генерации, после чего он будет отброшен. Изящество этого подхода заключается в том, что фактически устраняются составные части сериализации, выходящие за пределы языка, что делает невозможным нарушение каких-либо инвариантов класса, имевшихся до того, как этот класс был сделан сериализуемым. Чтобы продемонстрировать эту методику, в примере с классом Ре riod статья 56) вместо защищенного метода readObject можно воспользоваться следующим методом readResolve:
    // Идиома защищенного метода readResolve
    private Object readResolve() throws ObjectStreamException
    { return new Period(start, end); Этот метод останавливает обе атаки, описанные в статье
    56. Идиома защищенного метода readResolve имеет несколько преимуществ перед защищенным readObject. Это чисто механический прием, позволяющий делать класс сериализуемым, не подвергая риску его инварианты. Он требует небольшого объема кода, немного размышлений и работает надежно. Наконец, он устраняет искусственные ограничения, накладываемые сериализацией на использование окончательных полей.
    218
    Хотя идиома защищенного метода readResolve не имеет широкого применения, она заслуживает серьезного рассмотрения. Главный ее недостаток состоит в том, что она не годится для класса, который можно наследовать за пределами соответствующего пакета. Это не касается неизменяемых классов, поскольку они обычно являются окончательными (статья
    13). Недостатком этой идиомы является и некоторое снижение скорости десериализации, поскольку она сопровождается созданием дополнительного объекта. На моей машине десериализация экземпляров Ре riod замедлилась примерно на один процент по сравнению с защищенным методом readObj ect. Большое значение имеет доступность метода readResolve. Если вы помещаете его в окончательный класс, например синглтон, он должен быть закрытым. Помещая метод readResolve в расширяемый класс, вы должны внимательно изучить его доступность. Если он будет закрытым, его не удастся использовать нив одном из подклассов. Если он будет доступен только в пакете, его применение ограничится подклассами из того же пакета. Защищенный или открытый метод будет доступен во всех подклассах, если только они его не переопределят. Если метод readResolve является защищенным или открытыми не переопределяется в подклассе, то десериализация сериализованного экземпляра подкласса создаст экземпляр суперкласса, а это, вероятно, будет совсем не то, чего вы ожидали. Предыдущее рассуждение наводит на мысль о причине, по которой в классах, допускающих наследование, метод readResolve не может служить заменой защищенному методу readObject. Если бы в суперклассе метод readResolve был помечен как final, это не позволило бы правильно десериализовать экземпляры подкласса. Если бы он был переопределяемым, то подкласс, написанный злоумышленником, мог бы его переопределить таким методом, который возвращает испорченный экземпляр. Подведем итоги. Вы должны использовать метод readResolve для защиты "инвариантов контроля над экземплярами" в синглтонах и других классах, которые контролируют свои экземпляры. По существу, метод readResolve превращает метод readObject из де-факто открытого конструктора в де- факто открытый статический метод генерации. Метод readResolve также полезен в качестве простой альтернативы защищенному методу readObject в классах, для которых запрещено наследование за пределами соответствующего пакета.
    219
    1   ...   17   18   19   20   21   22   23   24   25


    написать администратору сайта