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