09 Jun 2014

Кэш Hibernate

Одной из основных возможностей Hibernate является использование кэша. При правильной настройке это может давать достаточно большой прирост производительности засчёт сокращения количества запросов к базе данных. Кэш содержит в себе локальную копию данных, которая может сохраняться в памяти либо на диске (имеет смысл, когда приложение и база данных распологаются на разных физических машинах).

Hibernate обращается к кэшу в следующих случаях:

  • Приложение выполняет поиск сущности по идентификатору

  • Приложение выполняет ленивую загрузку коллекции

Кэши разделяются в зависимости от области видимости (scope) на следующие виды:

  • Session scope cache (Кэш привязанный к транзакции, действительный только пока транзакция не завершиться. Каждая транзакция имеет свой кэш, следовательно, доступ к данному кэшу не может быть осуществлён в несколько потоков)

  • Process scope cache (Кэш привязанный к определённому процессу конкретной JVM и общий для многих транзакций с возможностью параллельного доступа)

  • Cluster scope cache (Кэш, общий для нескольких процессов на одной машине или нескольких машин в составе кластера).

По сути, Transaction scope cache представляет собой кэш первого уровня hibernate, кэш же второго уровня может быть реализован либо в области видимости процесса илибо как распределённый кэш.

Общий алгоритм работы кэша hibernate:

  1. Когда сессия пытается загрузить объект в первую очередь он ищется в кэше первого уровня
  2. Если в кэше первого уровня присутствует нужный объект, он возвращается как результат выполнения метода
  3. Если в кэше первого уровня объект не был найден, он ищется в кэше второго уровня
  4. Если объект сохранён в кэше второго уровня, он сохраняется так же в кэш первого уровня и возвращается в качестве результата метода
  5. Если в кэше второго уровня объекта так же не находится, то делается запрос к базе данных, и результат записывается сразу в оба кэша.

Ниже подробенее рассмотрим работу кэша первого и второго уровня.

Кэш первого уровня

Кэш первого уровня в hibernate связан с объектом сессии, он включён по умолчанию и нет возможности отключить его. Когда вы передаёте объект в метод save(), update() или saveOrUpdate(), а так же когда пытаетесь обратиться к нему с помощью методов load(), get(), scroll(), list(), iterate() выполняется добавление элемента в кэш сессии и следующий раз, когда нужно будет произвести повторную выборку данного объекта из БД в текущей сессии обращения к БД уже не произойдёт. Объект будет взят из кэша.

Обнуление кэша происходит после закрытия сессии. Так же, содержимым кэша можно управлять используя методы класса Session:

  • contains() - проверяет сохранён ли объект в кэше

  • flush() - синхронизирует содержимое кэша с базой данных

  • evict() - удаляет объект из кэша

  • clear() - обнуляет кэш

Кэш второго уровня

Кэш второго уровня в hibernate может быть настроен как кэш процесса, так и как распеределённый кэш (в рамках JVM или кластера). В отличие от кэша первого уровня, использование кэша второго уровня является опциональным. Он может быть как включён так и отключён.

В кэше второго уровня сущности хранятся в разобранном состоянии (что-то наподобие сериализованного состояния, однако, используемый алгоритм намного быстрее сериализации). Соответственно, доступ к объектам, сохранённым в кэше второго уроня осуществляется не по сслыке, а по значению. Обусловлено это ещё и тем, что доступ к сущности может осуществляться из параллельных транзакций. Так, каждая транзакция будет иметь свою копию данных.

Учитывая вышеперечисленное, были разработаны следующие стратегии паралельного доступа к кэшу второго уровня:

  • read only - используется для данных, которые часто читаются но никогда не изменяются. Изменение данных при использовании этой стратегии приведёт к исключению. Испльзование данного подхода в распределённом кэше позволяет не волноваться об синхронизации данных, однако может привести к сильной загрузке базы на начальном этапе работы кластера. Связано это с тем, что неизменяемые данные необходимы обычно большенству узлов кластера, и каждый из этих узлов будет обращаться напрямую к базе данных, пока не закэширует все необходимые объекты.

  • nonstrict read write - используется для данных, которые изменяются очень редко. При параллельном доступе к данным из разных транзакций данная стратегия не даёт никакой гарантии, что в кэше будут сохранены актуальные данные, так как Hibernate никак не изолирует от других транзакций данные во время изменения. Следовательно, чтение, которое выполняется параллельно изменению может получить доступ к данным, которые ещё не были закоммичены. Не слудует использовать данную стратегию, если небольшая вероятность считывания устаревших данных критична для приложения. При использовании распределённого кэша вероятность работы с устаревшими данными сильно увеличивается, так как устаревшие данные могут быть отражены в кэше многих узлов кластера и сохраняться там достаточно долго (до следующего обновления).

  • read write - используется для данных которые гораздо чаще читаются, чем обновляются, однако, устаревание которых критично для приложения. При этом подходе, данные блокируются для чтения во время их изменении с использованием “soft-lock” механизма. При попытке обратиться к заблокированным данным в данном случае будет произведён запрос к БД. Блокировака будет снята только после коммита. Данная стратегия обеспечивает уровень изоляции транзакций read commited. Использоване этого попхода для распределённого кэша имеет существенные недостатки. Так, если в кластере имеется возможность изменения одних и тех же данных разными узлами, довольно часто могут случаться блокировки устаревших данных в кэше, что сводит на нет приемущества использования данной стратегии. Для того чтобы избавиться от этого нужно использовать кэш-провайдер, поддерживающий блокировки.

  • transactional - используется, когда необходима изоляция транзакций вполоть до уровня repeatable read. Так же как и предыдущие используется для данных, которые гораздо чаще читаются нежели обновляются.

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

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

  • EhCache - изначально разрабатывался как кэш уровня процесса, однако, в последник версиях появилась возможность реализации так же распределённого кэша. Имеется поддрержка кэша запросов и выгрузки данных на диск.

  • OpenSymphony OSCache - реализует кэш только на уровне процесса. Поддерживает кэш запросов и выгрузку данных на диск.

  • SwarmCache - распределённый кэш, который базирется на JGroups. Нет поддержки кэша запросов.

  • JBoss Cache — может быть как локальным так и распределённым. Это полностью транзакционный кэш с возможностью репликации и инвалидаци, а так же с возможностью обмена как синхронными так и асинхронными валидационными сообщениями.

Однако, не каждый провайдер совместим с каждым типом стратегии параллельного доступа (смотри таблицу ниже)

Read-only Nonstrict-read-write Read-write Transactional
EhCache Yes Yes Yes No
OSCache Yes Yes Yes No
SwarmCache Yes Yes No No
Jboss Cache Yes No No Yes

Стоит заметить, что для удобства поиска, сущности разных классов, а так же сущности, принадлежащие разным коллекциям, сохраняются в разных регионах кэша. Под регионом, в данном случае, понимается некая именованная область кэша. Имя региона совпадает с именем класса, в случае кэширования классов, или с именем каласса объеденённым с именем свойства, в случае кэширования коллекций. Типичная настройка региона кэша на примере EhCache приведена наже:

<cache name="org.example.model.Entity"
	maxElementsInMemory="500" 
	timeToIdleSeconds="0" 
	timeToLiveSeconds="0" 
	eternal="true" 
	overflowToDisk="false" 
/>

Рассмотрим подробнее каждый из параметров:

  1. name — имя региона кэша
  2. maxElemetsInMemory — максимально возможное число объектов, хранимых в памяти
  3. timeToIdleSeconds — задаёт время жизни объекта в кэше с последнего обращения
  4. timeToLiveSeconds — задаёт время жизни объекта в кэше, начиная с его помещения туда
  5. eternal — атрибут определяющий будут ли элементы удаляться из кэша по истечении таймаута. Если этот атрибут установлен в true, то таймаут будет игнорироваться
  6. overflowToDisk — атрибут, определяющий, будут ли объекты выгружаться на диск при достижении maxElemetsInMemory. Имеет смысл включать, если БД и приложение развёрнуты на разных машинах.

Механизмы синхронизации данных в распределённом кэше

Приложения, которыми пользуется параллельно большое количество людей могут потребовать гораздо больей вычислительной мощности, чем может предоставить единичный сервер. В таком случае, несколько физических машин объеденяются в кластер. Для увеличения общей производительности кластера иногда полезно использовать распределённый кэш hibernate. В таком случае, каждый процесс хранит в своём кэше локальную копию недавно используемых данных. Поддержание актуальности этих данных осуществляется засчёт сетевого взаимодействия процессов с использованием протокола TCP либо UDP.

Механизмы поддержания актуальности данных:

  • копирование (replication) - если состояние сущности было изменнено, то её новое состояние будет разослано каждому члену кластера, что ведёт к повышенному потреблению трафика. А так же принуждает разные узлы кластера сохранять в свём кэше одинаковые данные, что не всегда полезно, так как, обычно, разные члены кластера работают с разными данными.

  • анулирование (invalidation) - если состояние сущности было изменено, то всем узлам кластера рассылается сообщение, указывающее на то, что сущность с определённым идентификатором была изменена. После получения сообщения другие члены кластера проверяют свой кэш на предмет наличия данной сущности, и, если таковая там находися, она удаляется из кэша. Данный подход не создаёт такой большой нагрузки на сеть как предыдущий, а основным его недостатком является то, что если изменённая сущность снова понадобится, она будет загружена запросом к базе данных, а это может привести к дополнительной нагрузке на БД. Однако, данные, которые необходимы многим узлам кластера обычно изменяются очень и очень редко.

Механизмы передачи сообщений о состоянии объектов:

  • синхронный - после отправи сообщения об изменении объекта в кэше доступ к кэшу блокируется, пока не будет получено подтверждение от всех узлов кластера.

  • асинхронный - отправка сообщения об изменении сущности не блокирует доступ к кэшу и не требует подтверждения от всех машин кластера. Вообще, при таком подходе, отпрака сообщений об изменении может происходить не обязательно сразу после изменения, а может быть реализована некая очередь собщений и расслыка в фоновом потоке.

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

Полезные ссылки:

Read more
15 May 2014

CookieTheftException в Spring Security "remember me"

Многие проекты, использующие для аутентикации пользователей Spring Security, сталкиваются со следующей проблемой: при входе в приложение с использованием «remember me», пользователей то и дело разлогинивает с выбросом CookieTheftException. Так, подобная проблема была найдена на багтрекере Atlassian и Grails. Не обошла она стороной и наш родной JTalks.

Для того, чтобы понять почему выбрасывается данное исключение, нужно разобраться в методах реализации функционала «remember me».
В Spring security для этого используется два основных подхода:

  • Подход, основанный на хешировании токенов (Simple hash-based token approach)

  • Подход, основанный на сохраняемых токенах (Persistent token approach)

Simple hash-based token approach использует md5-хеширование для реализации «remember me» стратегии. Реализуется всё это в классе TokenBasedRememberMeServices. При первом успешном входе пользователя с включённой опцией “запомнить меня”, срабатывает метод onLoginSuccess, который и устанавливает значение remember me куки. Сам файл cookie формируется следующим образом:

base64(username + ":" + expirationTime + ":" +md5Hex(username + ":" + expirationTime + ":" password + ":" + key))

    username:          Имя пользователя
    password:          Пароль пользователя
    expirationTime:    Дата и время окончания срока действия cookie (в милисекундах)
    key:               Уникальный ключ приложения

Cрок действия файла cookie устанавливается равным двум неделям. И, пока пока этот файл не устарел и не удалён, пользователь не будет вынужден вводить свои учётные данные при входе на страницу сайта. При последующем входе сработает метод processAutoLoginCookie, который будет искать пользователя по имени, полученном из cookie, а так же вычислять md5 хэш конкатенации имени пользователя, даты устаревания, пароля и ключа, и сравнивать его с полученным из cookie. Если всё пройдёт успешно, пользователь будет авторизован.

Недостатки:

  • Недостаточная безопасность (файлы cookies на протяжении всего времени жизни не изменяются, а значит легко могут быть украдены и использованы злоумышленником для входа в приложение)

  • Необходимость повторного входа после смены пароля, так как файл cookie зависит от пароля

Persistent token approach реализуется в классе PersistentTokenBasedRememberMeServices и основан на сохранении токенов в хранилище. При данном подходе сам токен представляет собой объект класса PersistentRememberMeToken, в котором хранятся следующие данные :

  • Значение токена

  • Серия токена

  • Имя пользователя

  • Дата и время окончания срока действия токена

При этом механизм сохранения должен реализовывать интерфейс PersistentTokenRepository.
На данный момент существуют две общеизвестных реализации данного интерфейса: InMemoryTokenRepositoryImpl и JdbcTokenRepositoryImpl.

InMemoryTokenRepositoryImpl - хранилище на основе обычного HashMap. В реальных проектах его рекомендуется использовать исключительно для отладки.

JdbcTokenRepositoryImpl - хранилище основанное на реляционной базе данных. Как можно заметить по названию класса, механизмом доступа к хранилищу является spring jdbc. При использовании данного хранилища в базе данных приложения будет создана таблица persistent_logins со столбцами, соответствующими полям класса PersistentRememberMeToken. Первичным ключом будет являться серия токена.
При первом успешном входе пользователя в приложение с включённой опцией “запомнить меня” сработает метод onLoginSuccess. В данном методе создаётся новый объект класса PersistentRememberMeToken, в котором значение и серия токена это случайным образом сгенерированные строки закодированные по алгоритму base64. Файл cookie формируется следующим образом:

base64(tokenSeries + ":" + tokenValue)

tokenSeries:         Серия токена
tokenValue:          Значение токена

Лично мной было замечено, что первые 34 символа куки отвечают за серию, последний символ незначащий, а остальные - значение токена.
Данный подход лишён недостатков предыдущего:

  • При каждом входе пользователя с использованием механизма «remember me» значение токена генерируется случайно и файл cookie переписывается, таким образом, каждый вход выполняется с новым значением токена. При этом, если cookie были похищены и злоумышленник не успел ими воспользоваться, после следующего входа пользователя они станут недействительными.

  • Значение файла cookie не зависит от пароля пользователя, а значит при смене пароля повторный вход не требуется

При последующем входе пользователя будет срабатывать уже метод processAutoLoginCookie. Данный метод отыскивает токен в хранилище по его серии и, если таковой был найден и не устарел, проверяет совпадают ли значение токена в файле cookie с сохранённым в хранилище. Затем, если всё прошло успешно, случайно генерируется новое значение токена, которое записывается в хранилище и файл cookie.
Если же токен по серии не был найден, пользователь входит в приложение как аноним, потому что могут существовать механизмы удаления устаревших токенов из хранилища.
Если значение токена, найденного в хранилище, не совпадает со значением токена в файле cookie, считается, что куки могли быть похищены злоумышленником. Пользователь уведомляется об этом по средствам выбрасывания CookieTheftException.

Обычно текст ошибки выглядит следующим образом:

RememberMe exception:
org.springframework.security.web.authentication.rememberme.CookieTheftException: Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.

Однако, несовпадение значения токена в хранилище и файлах cookie может вызывать не только похищение последних, но и последовательные запросы от одного клиента, проходящие в достаточно короткий промежуток времени через метод processAutoLoginCookie.
Происходит это по следующей схеме:

  • клиент отправляет серверу запрос 1

  • не дождавшись ответа на запрос 1, клиент отправляет запрос 2

  • запрос 1 попадает в метод processAutoLoginCookie

  • из базы извлекается токен, значение которого совпадает со значением токена в файле cookie запроса 1

  • генерируется новое значение токена, которое сохраняется в базу данных и cookie ответа на запрос 1

  • в это время запрос 2 со старыми куками попадает в метод processAutoLoginCookie

  • из базы извлекается токен, значение которого изменено запросом 1

  • после сравнения значений выбрасывается CookieTheftException, так как значения не совпадают.

При этом возникает вполне закономерный вопрос: «как такое возможно?».

Самой очевидной причиной является то, что разного рода ресурсы, такие как изображения, файлы скриптов, таблицы стилей, проходят через remember me фильтры Spring Security. Исправляется это достаточно просто. Нужно лишь добавить исключения для фильтров в файл secutity-context.xml. Для Spring Security версии ниже 3.1 это будет выглядеть следующим образом:

<security:intercept-url pattern="/resources/**" filters="none"/>
    <security:intercept-url pattern="/admin/logo" filters="none"/>
    <security:intercept-url pattern="/admin/icon/**" filters="none"/>
    <security:intercept-url pattern="/errors/**" filters="none"/>
    <security:intercept-url pattern="/users/*/avatar" filters="none"/>

Начиная со Spring Security 3.1 использование атрибута filters="none" считается устаревшим, и вместо этого рекомендуют использовать множественные тэги <http>:

<http pattern="/resources/**" security="none"/>
    <http pattern="/admin/logo" security="none"/>
    <http pattern="/admin/icon/**" security="none"/>
    <http pattern="/errors/**" security="none"/>
    <http pattern="/users/*/avatar" security="none"/>

Другим источником множественных запросов является предзагрузка веб-страниц браузером, в частности Google Chrome. Работает она следующим образом. Когда пользователь начинает вводить что-нибудь в адресную строку, браузер автоматически загружает содержимое страниц, которые считает наиболее релевантными введённому запросу. К тому времени, как пользователь отдаёт команду перейти на сайт, часть данных, скорее всего, окажется уже загруженной. Однако, на практике случается так, что ответ на запрос предзагрузки не успевает дойти до клиента, когда пользователь отдаёт команду перехода на сайт. И запрос со старыми значениями cookie попадает в метод processAutoLoginCookie из-за чего и вызывается CookieTheftException.

Всё это особо осложняет тот факт, что Google Chrome не посылает никаких отличительных хедеров в запросе на предзагрузку и вообще не позволяет никак отличтить его от обычного запроса. Выходит, единственной отличительной особенностью предзагрузочного запроса является то, что сразу за ним следует запрос с абсолютно такими же данными в файлах cookie.

Для того, чтобы отследить это, логичным является создание кэша информации о токенах, который будет сохранять серию токена, его значение, а так же время помещения в кэш. Так как нету никакой необходимости сохранять инфомацию о нескольких токенах одного пользователя, наиболее подходящей структурой данных для такого кэша является Map, ключом которой будет серия токена, а значением некий класс CachedRememberMeTokenInfo, листинг которого представлен ниже:

public class CachedRememberMeTokenInfo {

    private String value;
    long cachingTime;

    public CachedRememberMeTokenInfo(String tokenValue, long cachingTime) {
        this.value = tokenValue;
        this.cachingTime = cachingTime;
    }

    /**
     * Gets token date and time of token caching as milliseconds
     * @return Date and time of token caching
     */
    public long getCachingTime() {
        return cachingTime;
    }

    public String getValue() {
        return value;
    }
}

Далее нам необходимо создать класс ThrottlingRememberMeService, расширяющий стандартный класс PersistentTokenBasedRememberMeServices и переопределить в нём метод processAutoLoginCookie, таким образом, чтобы при первом запросе инфомация о токене сохранялась в кэш, и вход пользователя выполнялся в штатном режиме. При последующих же запросах необходимо проверять кэш на наличие текущего токена, и, если такой присутствует и был сохранён недавно, выполнять вход пользователя без вызова метода класса предка. Ниже представлена моя реализация данного класса:

public class ThrottlingRememberMeService extends PersistentTokenBasedRememberMeServices {
    private final static String REMOVE_TOKEN_QUERY = "DELETE FROM persistent_logins WHERE series = ? AND token = ?";
    // We should store a lot of tokens to prevent cache overflow
    private static final int TOKEN_CACHE_MAX_SIZE = 100;
    private final RememberMeCookieDecoder rememberMeCookieDecoder;
    private final JdbcTemplate jdbcTemplate;
    private final Map<String, CachedRememberMeTokenInfo> tokenCache = new ConcurrentHashMap<>();
    private PersistentTokenRepository tokenRepository = new InMemoryTokenRepositoryImpl();
    // 5 seconds should be enough for processing request and sending response to client
    private int cachedTokenValidityTime = 5 * 1000;

    /**
     * @param rememberMeCookieDecoder needed for extracting rememberme cookies
     * @param jdbcTemplate            needed to execute the sql queries
     * @throws Exception - see why {@link PersistentTokenBasedRememberMeServices} throws it
     */
    public ThrottlingRememberMeService(RememberMeCookieDecoder rememberMeCookieDecoder, JdbcTemplate jdbcTemplate)
            throws Exception {
        super();
        this.rememberMeCookieDecoder = rememberMeCookieDecoder;
        this.jdbcTemplate = jdbcTemplate;
    }

    /**
     * Causes a logout to be completed. The method must complete successfully.
     * Removes client's token which is extracted from the HTTP request.
     * {@inheritDoc}
     */
    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        String cookie = rememberMeCookieDecoder.exctractRememberMeCookieValue(request);
        if (cookie != null) {
            String[] seriesAndToken = rememberMeCookieDecoder.extractSeriesAndToken(cookie);
            if (logger.isDebugEnabled()) {
                logger.debug("Logout of user " + (authentication == null ? "Unknown" : authentication.getName()));
            }
            cancelCookie(request, response);
            jdbcTemplate.update(REMOVE_TOKEN_QUERY, seriesAndToken);
            tokenCache.remove(seriesAndToken[0]);
            validateTokenCache();
        }
    }

    /**
     * Solution for preventing "remember-me" bug. Some browsers sends preloading requests to server to speed-up
     * page loading. It may cause error when response of preload request not returned to client and second request
     * from client was send. This method implementation stores token in cache for <link>CACHED_TOKEN_VALIDITY_TIME</link>
     * milliseconds and check token presence in cache before process authentication. If there is no equivalent token in
     * cache authentication performs normally. If equivalent present in cache we should not update token in database.
     * This approach can provide acceptable security level and prevent errors.
     * {@inheritDoc}
     * @see <a href="http://jira.jtalks.org/browse/JC-1743">JC-1743</a>
     * @see <a href="https://developers.google.com/chrome/whitepapers/prerender?csw=1">Page preloading in Google Chrome</a>
     */
    @Override
    protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
        if (cookieTokens.length != 2) {
            throw new InvalidCookieException("Cookie token did not contain " + 2 +
                    " tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
        }

        final String presentedSeries = cookieTokens[0];
        final String presentedToken = cookieTokens[1];

        PersistentRememberMeToken token = tokenRepository.getTokenForSeries(presentedSeries);

        if (token == null) {
            // No series match, so we can't authenticate using this cookie
            throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
        }

        UserDetails details = null;

        if (isTokenCached(presentedSeries, presentedToken)) {
            tokenCache.remove(presentedSeries);
            details = getUserDetailsService().loadUserByUsername(token.getUsername());
            rewriteCookie(token, request, response);
        } else {
            /* IMPORTANT: We should store token in cache before calling <code>loginWithSpringSecurity</code> method.
               Because execution of this method can take a long time.
             */
            cacheToken(token);
            try {
                details = loginWithSpringSecurity(cookieTokens, request, response);
            //We should remove token from cache if cookie really was stolen or other authentication error occurred
            } catch (RememberMeAuthenticationException ex) {
                tokenCache.remove(token.getSeries());
                throw ex;
            }
        }
        validateTokenCache();

        return details;
    }

    /**
     * Calls PersistentTokenBasedRememberMeServices#processAutoLoginCookie method.
     * Needed for possibility to test.
     */
    @VisibleForTesting
    UserDetails loginWithSpringSecurity(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
        return super.processAutoLoginCookie(cookieTokens, request, response);
    }

    /**
     * Sets valid cookie to response
     * Needed for possibility to test.
     */
    @VisibleForTesting
    void rewriteCookie(PersistentRememberMeToken token, HttpServletRequest request, HttpServletResponse response) {
        setCookie(new String[] {token.getSeries(), token.getTokenValue()}, getTokenValiditySeconds(), request, response);
    }

    @Override
    public void setTokenRepository(PersistentTokenRepository tokenRepository) {
        this.tokenRepository = tokenRepository;
        super.setTokenRepository(tokenRepository);
    }

    /**
     * Stores token in cache.
     * @param token Token to be stored
     * @see CachedRememberMeTokenInfo
     */
    private void cacheToken(PersistentRememberMeToken token) {
        if (tokenCache.size() >= TOKEN_CACHE_MAX_SIZE) {
            validateTokenCache();
        }
        CachedRememberMeTokenInfo tokenInfo = new CachedRememberMeTokenInfo(token.getTokenValue(), System.currentTimeMillis());
        tokenCache.put(token.getSeries(), tokenInfo);
    }

    /**
     * Removes from cache tokens which were stored more than <link>CACHED_TOKEN_VALIDITY_TIME</link> milliseconds ago.
     */
    private void validateTokenCache() {
        for (Map.Entry<String, CachedRememberMeTokenInfo> entry: tokenCache.entrySet()) {
            if (!isTokenInfoValid(entry.getValue())) {
                tokenCache.remove(entry);
            }
        }
    }

    /**
     * Checks if given tokenInfo valid.
     * @param tokenInfo Token info to be checked
     * @return <code>true</code> tokenInfo was stored in cache less than <link>CACHED_TOKEN_VALIDITY_TIME</link> milliseconds ago.
     * <code>false</code> otherwise.
     * @see CachedRememberMeTokenInfo
     */
    private boolean isTokenInfoValid(CachedRememberMeTokenInfo tokenInfo) {
        if ((System.currentTimeMillis() - tokenInfo.getCachingTime()) >= cachedTokenValidityTime) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * Checks if token with given series and value stored in cache
     * @param series series to be checked
     * @param value value to be checked
     * @return <code>true</code> if token stored in cache< <code>false</code> otherwise.
     */
    private boolean isTokenCached(String series, String value) {
        if (tokenCache.containsKey(series) && isTokenInfoValid(tokenCache.get(series))
                && value.equals(tokenCache.get(series).getValue())) {
            return true;
        }
        return false;
    }

    /**
     * Needed for possibility to test.
     */
    public void setCachedTokenValidityTime(int cachedTokenValidityTime) {
        this.cachedTokenValidityTime = cachedTokenValidityTime;
    }
}
Read more
14 May 2014

Зачем нужны сервера приложений, если есть TomCat

Молодые программисты часто задают вопрос: а зачем нужны зачастую довольно тяжелые и дорогие промышленные сервера приложений (такие как JBoss AS, Oracle WebLogic, IBM WebSphere AS), если у нас есть замечательный легковесный фреймворк Spring и контейнер сервлетов Apache TomCat. Попробуем на него ответить. Сразу замечу, речь сейчас не идет об архитектуре приложения! Не важно, используете вы EJB или нет. Предположим, у вас приложение на Spring Framework и стоит вопрос, на чем его запускать. Итак, какие дополнительные сервисы предлагает нам сервер приложений.

  • Пулы соединений с БД. Да, у TomCat тоже есть пул соединений, но каковы его возможности? Может ли он периодически тестировать доступность СУБД и обновлять соединения в случае восстановления после сбоев? Умеет ли он делать замену прав доступа? Грубо говоря, подключаемся к БД под пользователем в зависимости от того, кто аутентифицировался в нашем приложении, если часть логики вынесена на уровень СУБД, то это бывает полезно. Может ли пул соединений Tomcat балансировать нагрузку между несколькими базами данных (например, в случае Oracle RAC), а так же определять, что вот эти узлы RAC умерли и теперь к ним не нужно пытаться подключаться, а затем понять, что они снова доступны и теперь их тоже можно использовать? В конце концов, может ли ваш пул соединений защитить от некорректного кода в приложении, которое по недосмотру не возвращает соединения, просто отбирая его после какого-то таймаута?
  • JMS. Если вы хотите использовать очереди в вашем приложении, развернутом на TomCat, то придется отдельно еще поднимать сервера очередей сообщений. В случае сервера приложений, очереди как правило доступны их коробки. Вместе с очередями доступны так же следующие вещи: кластеризация - вы можете строить распределенные очереди, расположенные сразу на нескольких серверах, что существенно увеличивает масштабируемость и доступность вашего приложения, миграция - в случае падения одного из серверов, его очереди автоматически перемещаются на другой, сохраняя необработанные сообщения. В некоторых серверах приложений поддерживается Unit-of-Order - гарантированный порядок обработки сообщений, удовлетворяющих некоторым критериям, очень часто при интеграции бывает полезен.
  • JTA. Те самые распределенные транзакции. Кто-то их понимает и использует, кто-то считает слишком тяжелыми. Как правило это так, они слишком тяжелые, но если вам нужно обеспечить согласованность данных в СУБД, разнесенных по разным углам нашей необъятной, или в СУБД и очереди, то без таких транзакций будет трудно. Суть распределенных транзакций в том, что мы не коммитим ни в одну из БД, пока не убедимся, что все БД, участвующие в транзакции, смогут принять наши данные. Тем самым мы избегаем проблемы “с одного счета в одном банке деньги списали, а на другой - в другом банке - не зачислили - сработало ограничение целостности”.
  • Безопасность. Современные сервера приложений предоставляют множество различных провайдеров безопасности. Доступны различные провайдеры аутентификации, вы можете хранить ваших пользователей во множестве мест: во встроенном LDAP-сервере, в базе данных, во внешнем LDAP-сервере, в различных Internet-directory - специализированных приложениях для управления правами доступа. Возможны следующие сценарии: на работу наняли человека, добавили его в Internet-directory/Access-manager, там запустился процесс раздачи прав, который выдал человеку права на все ресурсы вашего предприятия и теперь каждый сервер приложений в вашей компании (а их может быть очень много) видит эти права, так как подключен к этой Internet-directory/Access-manager. Доступно разделение пользовательской сессии между приложениями: мы аутентифицировались в одном приложении - нам уже не нужно аутентифицироваться в другом. Так же доступна реализация Single-Sign-On: вы делаете один из серверов базой для хранения пользователей, все другие сервера при аутентификации пользователя обращаются к этой базе. Реализуется SSO посредством Security Assertion Markup Language (SAML) 1/2 или посредством Simple and Protected Negotiate (SPNEGO) и Kerberos для Windows-клиентов. Возможна авторизация посредством протокола eXtensible Access Control Markup Language (XACML), позволяющего описывать довольно сложные политики (например, приложение доступно пользователю только в рабочее время). Опять же все данные возможности работают в кластерном окружении. Впрочем, стоит отметить, что с помощью Spring Security и Apache Shiro можно реализовать большинство из них, но вам придется “тянуть” эти реализации за каждой вашей программой, в то время как в сервере приложений они доступны из коробки.
  • Масштабируемость и высокая доступность. Да, для TomCat мы можем настроить кластеризацию, но она будет довольно примитивной. Мы не сможем сделать передачу пользовательской сессии из одного центра обработки данных (ЦоД) в другой через Интернет, мы не сможем эффективно настроить репликацию сессий на большом кластере, состоящем из 40-50 экземпляров сервера приложений. В случае сбоев, мы не сможем обеспечить миграцию экземпляров сервера на другую машину и т.д. Так же в TomCat нет механизмов автоматического мониторинга и реакции на ошибки: мы не можем автоматически перезапустить экземпляр сервера, если на нем зависло 10 потоков, мы не можем автоматически отправить письмо администратору при переполнении пула соединений и т.д.
  • Управляемость. В случае большого кластера TomCat у нас нет единого центра управления, т.н. AdminServer и аналога NodeManager’а. Мы не сможем одновременно запустить на старт 50 экземпляров сервера. Мы не можем посмотреть состояние экземпляров, посмотреть сколько у нас обработчиков на той или иной очереди, на том или ином сервере, сколько создано соединений с той или иной БД, какие из них можно убить, какие в данный момент выполняются транзакции, какие ресурсы в них задействованы и т.д. Конечно, можно все сделать “за три минуты на скриптах, ну как в Линуксе принято”, но результат будет плачевный.
  • Скриптовый язык. Кстати о скриптах, большинство промышленных серверов приложений содержат утилиты для выполнения скриптов как правило на языке Python, Пользоваться данными утилитами одно удовольствие. Администратор может описать в виде скрипта все шаги для подготовке к развертыванию сколь угодно большого приложения, таким образом запуск в продуктив или обновление займет сравнительно немного времени. С помощью таких скриптов можно создавать источники данных (представьте себе сервисную шину, подключенную к 120 экземплярам БД), JMS-очереди, менеджеры потоков, создавать новые экземпляры серверов и добавлять их в кластер, выполнять остановку и запуск серверов, их миграцию.
  • Административный канал и развертывание в промышленном режиме. Некоторые сервера приложений позволяют включить так называемый административный канал - отдельный порт, защищенный SSL, запросы по которому имеют приоритет. Таким образом, даже если ваш сервер завис, вы сможете на него зайти и посмотреть, какие транзакции выполняются и какие потоки висят. Но у данного канала есть и другое применение. При обновлении приложения вам не нужно выключать старую версию! Вы можете добавить на сервер новую версию приложения в административном режиме - пользователи продолжают работать со старой, а по административному каналу доступна новая, соответственно мы можем выполнить последнее тестирование перед запуском, проверить все ли у нас правильно развернулось. Затем мы окончательно публикуем приложение, при этом пользователи, уже имеющие сессию, продолжают работать со старой версией, чтобы не потерять данные. Новые пользователи аутентифицируются на новой версии. Тем самым мы обновляем приложение без его простоя, что очень важно для критических систем.

Как мы видим, сервисов, предоставляемых промышленными серверами приложений, довольно много. Возникает вполне закономерный вопрос, а почему сервлет-контейнер TomCat такой популярный? Здесь есть несколько соображений:

  • В первую очередь, цена. За все хорошее нужно платить, за отличное платить еще больше, особенно, если мы хотим доступ к технической поддержке и патчам. К примеру, сервер приложений Oracle WebLogic в базовой комплектации стоит $10 000/processor (под processor здесь понимается одно ядро, умноженное на т.н. core factor). Не каждый заказчик может себе позволить такое решение.
  • Не всем приложениям нужны вышеперечисленные сервисы, а иногда разработчики не умеют ими пользоваться. Например, если у нас простая учетная система, работающая с одной БД, то нам не нужны распределенные транзакции. С другой стороны, масштабирование. Приложение может следовать всем Java EE спецификациям, но при этом не быть масштабируемым. Простой пример: приложение читает из БД измененные записи (которые пишутся с помощью триггеров в отдельную табличку) и передает их в другую БД. При этом авторы как-то забыли про блокировки. Если мы запустим данную программу на кластере, то у нас каждая запись будет обрабатываться N-раз, по числу экземпляров TomCat в кластере. Такая масштабируемость нам не нужна. Аналогичные соображения можно привести и для других сервисов.
  • Простота и легкость освоения. Вообще администратор сервера приложений это отдельная профессия, такая же как и администратор баз данных. Это не просто линукс-админ. Посмотрим еще раз на список сервисов и задумаемся как долго нужно изучать возможности выбранного сервера приложений по их реализации и настройке. Курсы по администрированию IBM WebSphere или Oracle WebLogic могут стоить десятки тысяч рублей.
  • Сварим кашу из топора сами. Бывают ситуации, когда это необходимо. Не всегда есть смысл ждать патча, исправляющего какие-то критические для нашего приложения ошибки. Гораздо быстрее просто подменить версию библиотеки. Правда зачастую это можно сделать и на сервере приложений, добавив библиотеку к нашему приложению и настроив загрузчик классов. Причем современные сервера содержат в себе утилиты для поиска ошибок в иерархии загрузчиков.

Отдельно отметим причины популярности Spring Framework как на TomCat’е, так и на промышленных серверах приложений и немного их прокомментируем:

  • Исторические причины. Почему Spring Framework, а не EJB? Ну потому что я в 88-м году программировал на С++, фигня этот ваш С++. Да, действительно EJB 1.1 и EJB 2.x были очень тяжелы для освоения и для использования, но времена меняются. Опять же, начиная с Java EE 6, появился легковесный IoC-контейнер - CDI. Зачем тянуть в свое приложение сотни мегабайт библиотек, которые будут существенно замедлять процесс развертывания, если можно использовать готовые и довольно качественные реализации, предоставленные производителем сервера приложений? На самом деле иногда есть зачем.
  • Баланс между завязкой на конкретном производителе и переносимостью. Да, EJB это часть спецификации Java EE, причем наиболее сложная, сложнее только J2CA и по хорошему приложения, написанные для одного сервера, должны работать на другом. На практике это не всегда так. Зачастую для эффективного использования всех возможностей сервера приложений приходится в коде вызывать его API, а это уже делает приложение непереносимым. Правда, справедливости ради, с каждой новой версией Java EE таких завязок становится все меньше. С другой стороны, даже без явных завязок на API части стандарта могут быть реализованы разными серверами по своему, например, один сервер будет закрывать EntityManagerFactory при остановке приложения, другой - нет. Реализации иерархии загрузчиков классов так же могут отличаться.
  • При этом, явная завязка на Spring Framework тоже имеет свои минусы. Это такая же завязка на производителе, как и решение использовать только WebLogic. Но если с WebLogic хоть и со скрипом мы сможем уйти, то со Spring Framework скорее всего нет. Что будет, если завтра ведущие разработчики решат оставить свое детище и все дружно перейти в Oracle? Впрочем, думаю, что вероятность такого сценария не высока.
  • Отдельно стоит отметить поддержку Spring Framework со стороны разработчиков серверов приложений. Например, в Oracle WebLogic можно включить отдельную страницу в консоли администрирования для каждого построенного на данном фреймворке приложения. На странице будет отображено дерево бинов и показаны их свойства. Так же доступны бины самого сервера и упрощена разработка MBean’ов. Помимо этого, Spring Framework прозрачно интегрируется в кластерное окружение, а Spring Security может использовать подсистему безопасности сервера приложений.

В заключение хочется отметить, что выбор платформы для приложения это довольно нетривиальная инженерная задача, в которой должна учитываться масса факторов. Это и соотношение стоимости разработки к стоимости поддержки (при этом нужно учитывать, что разработка может идти год, а использоваться ПО может десяток лет), стоимость самих серверов приложений, ваши отношения с вендором, т.к. несмотря на высокую номинальную стоимость зачастую предоставляются скидки под 80%. Учитывайте вашу и вашей команды квалификацию в конце концов. Ну и не нужно быть ретроградом, если вы в 2001-м писали на EJB и с тех пор смотреть на них не можете, то это еще не повод отказываться от этой замечательной технологии и реализующих ее серверов приложений, но даже если вы гуру Spring Framework, подумайте, может быть для него на промышленном сервере тоже найдется место?

Read more