Безопасность Spring: CookieTheftException с PersistentTokenBasedRememberMeServices

У меня есть веб-приложение с Spring-boot 2.0.1, защищенное Spring-Security. Я использую PersistentTokenRepository для Remember-Me и храню токены в базе данных MySQL.

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

Servlet.service() for servlet [dispatcherServlet] in context with path [/r] threw exception
  org.springframework.security.web.authentication.rememberme.CookieTheftException: Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.
    at org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices.processAutoLoginCookie(PersistentTokenBasedRememberMeServices.java:119) ~[spring-security-web-5.0.4.RELEASE.jar!/:5.0.4.RELEASE]

Ручное тестирование не смогло воспроизвести это. Удаление файла cookie сеанса, но сохранение файла cookie с функцией запоминания и отправка запроса на ограниченный URL-адрес приводит к нормальному сеансу с проверкой подлинности.

Вот соответствующие части моей конфигурации безопасности:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {
    @Configuration
    public static class SecurityConfiguration extends WebSecurityConfigurerAdapter {

        @Autowired
        private RememberMeServices rememberMeServices;

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .rememberMe()
                    .key(rememberMeKey)
                    .rememberMeServices(rememberMeServices);
            ;
        }
    }

    /**
     * Key for RememberMeServices and RememberMeAuthenticationProvider.
     */
    private static final String rememberMeKey = "...";

    @Bean
    public RememberMeServices rememberMeServices(UserDetailsService userDetailsService, PersistentTokenRepository persistentTokenRepository) {
        PersistentTokenBasedRememberMeServices rememberMeServices = new AnalyzingPersistentTokenBasedRememberMeServices(
                rememberMeKey, userDetailsService, persistentTokenRepository);
        rememberMeServices.setTokenValiditySeconds((int) Duration.of(366L, ChronoUnit.DAYS).toSeconds());
        return rememberMeServices;
    }

    @Bean
    public PersistentTokenRepository persistentTokenRepository(JdbcTemplate jdbcTemplate) {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setJdbcTemplate(jdbcTemplate);
        return tokenRepository;
    }
}

Этот AnalyzingPersistentTokenBasedRememberMeServices - это PersistentTokenBasedRememberMeServices с некоторой дополнительной записью в processAutoLoginCookie.

Еще одна особенность заключается в том, что я использую собственный AuthenticationProvider и предоставляю UserDetailsService только для RememberMe. Но, как было сказано выше, ручное тестирование работает нормально. Тем не менее, пользователи сообщают, что они слишком часто выходят из системы (тайм-аут сеанса составляет 24 часа).

Кто-нибудь испытал что-то подобное и нашел решение? Я пропустил какую-то важную конфигурацию?

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
0
1 055
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

PersistentTokenBasedRememberMeServices не подходит для приложений с параллельными запросами, которые могут отправлять одинаковые серии токенов.

См. Эти почти пятилетние и нерешенные отчеты об ошибках:

https://github.com/spring-projects/spring-security/issues/2648

https://github.com/spring-projects/spring-security/issues/3079

При использовании TokenBasedRememberMeServices таких проблем не возникает.

Этот ответ немного вводит в заблуждение. В большинстве случаев люди выбирали PersistentTokenBasedRememberMeServices, потому что TokenBasedRememberMeServices изначально не подходили. А именно это не так безопасно.

rougou 19.05.2020 06:03

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

Проблема с реализацией Spring Security Web 4.2.13 в механизме пролонгации токенов

protected UserDetails processAutoLoginCookie(String[] cookieTokens,...
  .....
    PersistentRememberMeToken newToken = new PersistentRememberMeToken(
            token.getUsername(), token.getSeries(), generateTokenData(), new Date());

    try {
        tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
                newToken.getDate());
        addCookie(newToken, request, response);
    }
    catch (Exception e) {
        logger.error("Failed to update token: ", e);
        throw new RememberMeAuthenticationException(
                "Autologin failed due to data access problem");
    } 

При обновлении в случае одновременного запроса значение токена для одной и той же серии может быть несколько раз переписано в БД, но в cookie может быть сохранено другое значение.

Создание токена во время входа в систему работает хорошо, в худшем случае у вас может быть много постоянных токенов в БД, но один из них будет работать =)

В моем проекте исправлена ​​ошибка путем модификации алгоритма обновления:

   if (token.date + 1 < new Date()){
       try {
            PersistentRememberMeToken newToken = new PersistentRememberMeToken( token.getUsername(), generateSeriesData(), generateTokenData(), new Date())
            tokenRepository.createNewToken(newToken)
            addCookie(newToken, request, response)
        }
        catch (Exception e) {
            logger.error("Failed to update token: ", e);
            throw new RememberMeAuthenticationException(
                    "Autologin failed due to data access problem");
        }
    }  

Этот код создает много записей БД в результате, но работает

Другие вопросы по теме