06 Aug 2014

Spock Lifecycle and non-intuitive WHERE block

Résumé: as it appeared Spock’s Lifecycle is not always intuitive, this is especially true for where block. Even though it’s located inside of a test method, its execution happens outside and what’s even more peculiar - it’s invoked even before setup() happens or fields get initialized.

Spock allows you writing data-driven tests in a very easy manner using where:

def "square function"(int a, int square) {
        expect:
            square == a * a
        where:
            a | square
           -1 | 1
            0 | 0
            1 | 1
}

But docs keep silence on how this magic is implemented. Spock’s Syntax is not pure Groovy, its code gets transformed with Groovy AST. Depending on the result of these transformations we may or may not use some features of Spock.

First, where is going to be factored out into a separate method within the same test class. But what’s really interesting - it will be invoked before setup(). The whole thing is here:

This means that variables that are initialized in setup() won’t be seen in where:

class SomeTest extends Specification {
    int a = 2
    int b = 1

    def setup() {
        b = 1
    }
    def someTest(a, b){
      expect:
        b != a
      where:
        a << [a]
        b << [b]
    }
}

In this test we’ll get a == b == 0 which demonstrates the point.

As a consequence all the JUnit Listeners (after all Spock is just an extension of JUnit) that observe beforeTestMethod will also be triggered after the data is initialized in where. This includes DependencyInjectionTestExecutionListener from Spring TestContext. Which means that if you want to use @Autowired fields which are injected from the context, you won’t be able to use them in where, what you’ll get there is null.

Read more
18 Jun 2014

Spock Lifecycle и неинтуитивный блок where

Резюме: как оказывается у Spock’а порою не совсем интуитивный жизненный цикл, особенно это касается блока where который хоть и находится внутри тестового метода, а выполняется не то что до самого метода (что было бы логично), а даже до setup() и до инициализации полей класса.

Spock позволяет достаточно удобно строить data-driven тесты с помощью своего where:

def "square function"(int a, int square) {
        expect:
            square == a * a
        where:
            a | square
           -1 | 1
            0 | 0
            1 | 1
}

Нигде только не упоминается о том как же в итоге это все реализуется. Синтаксис используемый Spock’ом на самом деле не является чистым Groovy, этот код потом трансформируется с помощью Groovy AST. Так вот от того во что он трансформируется зависит возможность использования некоторых фич Spock’a.

Так, например, where будет вынесено отдельным методом в тот же класс и, что интересно, будет вызван до setup(). Цепочку можно проследить тут:

Это значит так же что переменные которые инициализируются в setup() не будут видны в where:

class SomeTest extends Specification {
    int a = 2
    int b = 1

    def setup() {
        b = 1
    }
    def someTest(a, b){
      expect:
        b != a
      where:
        a << [a]
        b << [b]
    }
}

И в итоге мы все равно получим в тесте a == b == 0.

Из этого так же следует, что все слушатели, которые навешаны на beforeTestMethod тоже отработают после того как данные будут инициализированы в where. К таким слушателям относятся и DependencyInjectionTestExecutionListener из Spring TestContext. Т.е. если вы захотите использовать @Autowired поля которые инжектятся из контекста, в where вы их использовать не сможете, там у вас все равно будут null.

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
21 Dec 2012

Пример настройки Database Connection Pool

Собственно хочу поделиться и описать конфигурацию DB Pool’a который мы используем в JTalks:

<!--Создаем Ленивый DataSource, который по возможности не будет вытягивать настоящее--> 
    <!--соединение к БД из настоящего пула до тех пор пока это правда необходимо. Например, -->
    <!--если Hibernate достает данные из кеша и не делает запрос в БД, нам не нужно--> 
    <!--настоящее соединение. Однако если не использовать ленивый DataSource, то соединение--> 
    <!--будет вытянуто и транзакция будет начата если метод помечен @Transactional-->
  <bean id="dataSource" class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy">
    <property name="targetDataSource">
    <!--Это собственно настоящий DB Pool. Многие говорят что пул, который Hibernate--> 
    <!--использует по умолчанию не является серьезным и использование его в production-->
    <!--коде не допустимо. Однако C3P0 как раз не является пулом по умочанию, у Hibernate -->
    <!--есть свой внутренний пул для "поиграться", однако это не C3P0 как многие думают! С другими-->
    <!--же пулами не сравнивал на самом деле, поэтому не могу сказать какой лучше,--> 
    <!--C3P0 используется исторически во многих приложениях с Hibernate. -->
      <bean class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
        <property name="driverClass" value="${jdbc.driverClassName}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
        <property name="user" value="${JCOMMUNE_DB_USER:root}"/>
        <property name="password" value="${JCOMMUNE_DB_PASS:root}"/>
        <!--Эта опция показывает сколько PreparedStatement'ов должно быть закешировано-->
        <!--на стороне Java, в самом Connection Pool. Имейте в виду что СУБД может -->
        <!--требовать дополнительной настройки для того чтоб эта опция показала себя эффективной-->
        <property name="maxStatements" value="1500"/>
        <!--А это сколько PreparedStatement'ов каждое соединение может кешировать для себя -->
        <property name="maxStatementsPerConnection" value="50"/>
        <!--Вот это самая сложная опция и для того чтоб ее правильно настроить,-->
        <!--нам нужны нагрузочные тесты а также рабочее в PROD приложение-->
        <!--Есть разные стратегии по работе с соединениями которые влияют на оптимальный-->
        <!--размер DB Pool'a, подробней читайте прекрасную книгу Release It!-->
        <property name="maxPoolSize" value="50"/>
        <!--MySQL прибивает соединение если оно не использовалось какое-то время.--> 
        <!--По умолчанию это 8 часов. Дабы избежать неприятных исключений нам -->
        <!--нужно бомбардировать MySQL запросами каждый N часов (минут)-->
        <property name="idleConnectionTestPeriod" value="3600"/>
        <!--Если мы испытываем большую нагрузку и/или запросы выполняются очень долго,-->
        <!--мы сможем обслуживать 50 пользователей одновременно (размер пула), однако -->
        <!--при увеличении нагрузки, клиенты начнут выстраиваться в очередь и просто ждать-->
        <!--заблокированные в synchronized методах внутри DB Pool'a. Дабы мы не оказались-->
        <!--в ситуации, когда мы заблокированы надолго и приложение совсем не отвечает, -->
        <!--через 10 секунд простаивания в очереди выбрасывается исключение и сервер разгружается-->
        <!--от лишних запросов. Да, это исключение и это неприятно, однако лучше отвалится -->
        <!--несколько клиентов (особенно если это DDoS'еры поганые) нежели сайт будет в полной отключке.-->
        <property name="checkoutTimeout" value="10000"/>
        <!--Также соединение может издохнуть. Пул может проверять его на работоспособность-->
        <!--в то время как приложение запрашивает соединение и наоборот - когда возвращает обратно.-->
        <!--Первое - более надежное, второе - более быстрое.-->
        <property name="testConnectionOnCheckin" value="true"/>
      </bean>
    </property>
  </bean>


  • Подробрей о кешировании PreparedStatement

  • Больше о разрыве и тестировании соединений можно прочесть тут, мини проект с примером настроенного пула тут.

  • Если вы получили подобное исключение, значит вам поможет iddleConnectionTestPeriod описанный выше:

com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: The last packet successfully received from the server was 64,129,968 milliseconds ago.  The last packet sent successfully to the server was 64,129,968 milliseconds ago. is longer than the server configured value of 'wait_timeout'. You should consider either expiring and/or testing connection validity before use in your application, increasing the server configured values for client timeouts, or using the Connector/J connection property 'autoReconnect=true' to avoid this problem.
    at com.mysql.jdbc.Util.handleNewInstance(Util.java:411)
    at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:1116)
    at com.mysql.jdbc.MysqlIO.send(MysqlIO.java:3851)
Read more