Сессии и Remember-Me аутентификация

В этой статье говорится о том, как запоминать пользователя между сессиями с помощью Remember-Me токена.
Код отличается от примера одной строкой:

.and().rememberMe();

Введение

Долгие сессии могут нагрузить сервер, так как все объекты пользовательских сессий хранятся в куче контейнера. Поэтому сессии имеет смысл сделать короче, а идентичность пользователя запоминать с помощью специального долгосрочного Hash-Based токена. Он содержит только имя пользователя и хэш, с помощью которого можно проверить подлинность токена. При этом истекшие сессии не восстанавливаются, а начинаются заново. Зато пользователь может не совершать заново вход в систему — его помнят благодаря Remember-Me токену. Есть два вида Remember-Me токенов, мы будем использовать Hash-Based. Он идет по умолчанию.

О сессиях

Сессии придуманы для того, чтобы сервер «помнил» пользователя при повторных запросах от него. То есть пользователь вводит однократно имя и пароль, и при дальнейших запросах сервер понимает, от кого именно пришел запрос, а также какие объекты есть в данном сеансе (например, товары в корзине покупок).

Пояснить взаимодействие с сервером можно на примере обращения в службу поддержки. При первом обращении клиент описывает проблему и получает номер обращения (JSESSIONID). Дальше переписка идет под этим номером обращения. Клиенту не надо каждый раз заново все пересказывать. Служба поддержки (сервер) по номеру сама восстанавливает все детали (идентичность пользователя и данные сессии).

Реализуется это с помощью идентификаторов сессий. Стандартный алгоритм следующий.

Сервер высылает клиенту при первом запросе (например, при успешном логине, но можно и анонимному клиенту) заголовок типа:

Set-Cookie: JSESSIONID=4C7871D1EF406F69C7CF20CD6BD283F1

Браузер сохраняет эти значения (свои для каждого сайта), и далее при каждом запросе на конкретный сайт браузер автоматически добавляет к запросу соответствующий заголовок:

Cookie: JSESSIONID=4C7871D1EF406F69C7CF20CD6BD283F1
Название JSESSIONID не универсально, а характерно именно для Java. В других языках используются другие названия.

При последующих запросах от того же клиента сервер (в нашем примере  это Apache Tomcat — контейнер сервлетов) опознает клиента по идентификатору сессии. Контейнер хранит эти идентификаторы сессий и соответствующие данные клиента как словарь в Map:

ключ JSESSIONID (конкретный идентификатор) - данные сессии
Сессии и их данные
Сессии и их данные

Сессия имеет срок годности. Как только он истекает, данные исчезают, и в последующих запросах контейнер не принимает истекший Cookie конкретного клиента.

Cессии исчезнут, если перезапустить Tomcat, так как они хранятся в «куче» Tomcat.

По умолчанию в Apache Tomcat сессия уничтожается после 30 минут неактивности клиента.

Заметьте, что сессии работают и без Spring Boot, это фишка контейнера сервлетов (того, кто реализует интерфейс javax.servlet.http.HttpSession) — в нашем примере Apache Tomcat.

А вот Remember-Me аутентификация — это уже фишка Spring Boot.

Remember-Me аутентификация

По умолчанию (без Remember-Me функциональности) форма входа выглядит так:

Без Remember-Me

Но если включить Remember-Me аутентификацию, то появится флажок:

Форма с флажком
Форма с флажком

Если пользователь включит флажок, то будет создан Remember-Me токен. Он позволяет помнить пользователя и после того, как срок годности сессии истечет, а также после перезапуска сервера.

Токен высылается клиенту в Set-Cookie аналогично сессии:

Remember-Me Cookie
Remember-Me Cookie

Но восстановить из него можно только имя пользователя, никакие другие данные по нему не восстанавливаются — хранить в нем объекты нельзя (а в сессии можно).

При каждом запросе автоматически выполняется проверка подлинности токена, подробнее об этом ниже.

И если серверов несколько, то Remember-Me аутентификация будет работать, так как она не завязана на конкретный Tomcat-контейнер (и хранящуюся в его памяти сессию). Опознание пользователя происходит не путем обращения в Map в куче конкретного запущенного сервера (где содержатся ключи JSESSIONID и соответствующие данные сессии, среди которых имя пользователя), а иначе — путем проверки подлинности токена.

По умолчанию Remember-Me помнит пользователя две недели.

Simple Hash Based токен и проверка подлинности

Вообще в Remember-Me аутентификации можно выбрать два вида токенов: Simple Hash-Based Token и Persistence Token (хранится в базе).

при Simple Hash Based токене токен содержит:

  • имя пользователя и срок годности токена в открытом виде (почти открытом — Base64)
  • и некий хеш (md5Hex) — значение, вычисляемое на основе имени, пароля, срока годности токена и секретного ключа. Вычисляется он так:
    md5Hex(username + ":" + expirationTime + ":" password + ":" + key)

    Весь токен такой:

base64(username + ":" + expirationTime + ":" +
md5Hex(username + ":" + expirationTime + ":" password + ":" + key)), где

username:          As identifiable to the UserDetailsService
password:          That matches the one in the retrieved UserDetails
expirationTime:    The date and time when the remember-me token expires, expressed in milliseconds
key:               A private key to prevent modification of the remember-me token

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

Теоретически клиент каждый раз мог бы просто высылать имя и пароль, а приложение каждый раз так же находить по имени пароль и проверять, совпадает ли он. Но пароль передавать опасно. Суть в том, чтобы передавать именно хеш — значение, из которого невозможно извлечь данные, на которых он построен. То есть он работает в одну сторону. Зная данные (у нас это имя, пароль, срок годности и секретный ключ), хэш можно посчитать, а из хэша обратно данные не получить. То есть хотя в браузере и виден токен, пароль из него не извлечь.

Настройка Remember-Me аутентификации

Сессии по умолчанию включены. Давайте добавим Remember-Me:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/user/**").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
                .antMatchers("/**").permitAll()
                .and().formLogin()
                .and().rememberMe();
    }

При такой настройке будут использоваться как сессии, так и Simple Hash-Based токен. Токен продолжит действовать, когда сессия истечет, но данные из сессии (если они есть) будет уже не извлечь.

Чтобы задать срок действия токена 24 часа:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/user/**").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
                .antMatchers("/**").permitAll()
                .and().formLogin()
                .and().rememberMe().tokenValiditySeconds(86400);
    }

Можно отключить сессии и использовать только токен:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
               
                .antMatchers("/user/**").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
                .antMatchers("/**").permitAll()
                .and().formLogin()
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().rememberMe();
    }

Чтобы задать секретный ключ, используем key(«secretkey»):

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/user/**").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
                .antMatchers("/**").permitAll()
                .and().formLogin()
                .and().rememberMe().key("secretkey");
    }

Если ключ не задать, он генерируется автоматически.

Также можно сделать создание токена обязательным, независимо от того, включен ли на форме флажок remember-me:

.alwaysRemember(true);

При отключенных сессиях так и надо делать (а флажок на форме убрать), потому что тогда Remember-Me токен остается единственным способом идентифицировать пользователя при последующих запросах, а значит, он должен быть обязательным.

Проверка

Итак, теперь в запросе появляется новый Cookie remember-me (см. картинку выше).

Если удалить сессию, аутентификация продолжает работать — доступ к защищенным страницам открыт.

Можно это проверить, удалив сookie JSESSIONID и перезагрузив страницу:

Как удалить кукис
Как удалить куки

Либо можно в настройках сделать Tomcat сессию короткой и дождаться, когда она истечет.

Код примера

Код примера есть на GitHub.

 

Сессии и Remember-Me аутентификация: 2 комментария

  1. Получается, если подключена сессия и remember-me, то при первой аунтефикации мы получаем
    Set-Cookie: JSESSIONID=4C7871D1EF406F69C7CF20CD6BD283F1
    и дальше например 30 минут ходим с этим JSESSIONID, даже не проверяя
    Set-Cookie: remember-me=…. ?
    И если ответ на вопрос выше «да», то получается когда мы ходим с Set-Cookie: remember-me= , то сервер нам не выдает сессию?И чтобы ее нам получить нужно выйти и еще раз аунтефицироваться?

    1. 1) Да, remember-me вначале не проверяется (потому что когда запрос приходит в RememberMeAuthenticationFilter, к этому моменту уже есть заполненный в более ранних фильтрах Authentication, и блок проверки пропускается).

      2) Когда ходим с Cookie: remember-me=, то сессия выдается новая (если сессии не отключены). Но аунтетифицироваться заново не надо, remember-me с захэшированным именем и паролем как раз играет роль автоматического перелогина, просто без интерфейса (перенаправления на форму, ручного ввода, вот этого всего). Разница в том, что при вводе с формы имя и пароль проверяются в UsernamePasswordAuthenticationFilter (и в нем создается объект Authentication), а при при передаче захешированного имени_пароля в remember-me-токене это происходит в RememberMeAuthenticationFilter.

      Кстати, Remember-me сохранит аутентификацию не только при естественном истечении времени сессии, но и при перезапуске приложения. (Только надо тогда задать .key(“secretkey”), иначе ключ будет автогенериться при перезапуске приложения и проверка Remember-me токена с новым секретом не пройдет; впрочем, секретный ключ надо в любом случае задавать)

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *