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

В этой статье говорится об обычном способе запоминания пользователей с помощью сессий и о Remember-Me аутентификации – способе, специфичном для Spring Security .
Код отличается одной строкой от примера.

Сессии

Сессии придуманы для того, чтобы сервер “помнил” пользователя при повторных запросах от него. То есть пользователь вводит однократно имя и пароль, и при дальнейших запросах сервер понимает, от кого именно пришел запрос.

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

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

Set-Cookie: JSESSIONID=4C7871D1EF406F69C7CF20CD6BD283F1

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

Cookie: JSESSIONID=4C7871D1EF406F69C7CF20CD6BD283F1

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

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

JSESSIONID (конкретный идентификатор) - данные сессии
Сессии и их данные
Сессии и их данные
Пояснить взаимодействие с сервером можно на примере обращения в службу поддержки. При первом обращении клиент описывает проблему и получает номер обращения (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 токена  больше нет сессий, хранящихся в куче контейнера. Идентичность клиента можно подтвердить с помощью небольшой калькуляции при каждом запросе.

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

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

Но отличие не только внешнее, сам принцип опознания пользователя отличается.

Чтобы понять принцип, надо рассмотреть структуру токена.

Simple Hash Based токен

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

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

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

  • имя пользователя и срок годности токена в открытом виде
  • и некий хеш (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");
    }

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

Результат

Теперь запросе появляется новый Cookie:

Remember-Me Cookie
Remember-Me Cookie

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

Код примера

Код примера тут.

 

Сессии и 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, к этому моменту уже есть заполненный в более ранних фильтрах Authencation, и блок проверки пропускается).

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

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

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

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