В этой статье говорится об обычном способе запоминания пользователей с помощью сессий и о Remember-Me аутентификации – способе, специфичном для Spring Security .
Код отличается одной строкой от примера.
Сессии
Сессии придуманы для того, чтобы сервер “помнил” пользователя при повторных запросах от него. То есть пользователь вводит однократно имя и пароль, и при дальнейших запросах сервер понимает, от кого именно пришел запрос.
Реализуется это с помощью идентификаторов сессий. Стандартный алгоритм следующий.
Сервер высылает клиенту при первом запросе (например, при успешном логине, но можно и анонимному клиенту) заголовок типа:
Set-Cookie: JSESSIONID=4C7871D1EF406F69C7CF20CD6BD283F1
Браузер сохраняет эти значения (свои для каждого сайта), и далее при каждом запросе на конкретный сайт браузер автоматически добавляет к запросу соответствующий заголовок:
Cookie: JSESSIONID=4C7871D1EF406F69C7CF20CD6BD283F1
Название JSESSIONID не универсально, а характерно именно для Java. В других языках используются другие названия.
При последующих запросах от того же клиента сервер (в нашем примере это Apache Tomcat – контейнер сервлетов) опознает клиента по идентификатору сессии. Контейнер хранит эти идентификаторы сессий и соответствующие данные клиента как словарь:
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 аутентификация будет работать, так как она не завязана на конкретный Tomcat-контейнер (и хранящуюся в его памяти сессию). Опознание пользователя происходит не путем заглядывания в словарь (JSESSIONID – данные сессии), а иначе.
Но отличие не только внешнее, сам принцип опознания пользователя отличается.
Чтобы понять принцип, надо рассмотреть структуру токена.
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, то при первой аунтефикации мы получаем
Set-Cookie: JSESSIONID=4C7871D1EF406F69C7CF20CD6BD283F1
и дальше например 30 минут ходим с этим JSESSIONID, даже не проверяя
Set-Cookie: remember-me=…. ?
И если ответ на вопрос выше “да”, то получается когда мы ходим с Set-Cookie: remember-me= , то сервер нам не выдает сессию?И чтобы ее нам получить нужно выйти и еще раз аунтефицироваться?
1) Да, remember-me вначале не проверяется (потому что когда запрос приходит в RememberMeAuthenticationFilter, к этому моменту уже есть заполненный в более ранних фильтрах Authentication, и блок проверки пропускается).
2) Когда ходим с Cookie: remember-me=, то сессия выдается новая (если сессии не отключены). Но аунтетифицироваться заново не надо, remember-me с захэшированным именем и паролем как раз играет роль автоматического перелогина, просто без интерфейса (перенаправления на форму, ручного ввода, вот этого всего). Разница в том, что при вводе с формы имя и пароль проверяются в UsernamePasswordAuthenticationFilter (и в нем создается объект Authentication), а при при передаче захешированного имени_пароля в remember-me-токене это происходит в RememberMeAuthenticationFilter.
Кстати, Remember-me сохранит аутентификацию не только при естественном истечении времени сессии, но и при перезапуске приложения. (Только надо тогда задать .key(“secretkey”), иначе ключ будет автогенериться при перезапуске приложения и проверка Remember-me токена с новым секретом не пройдет; впрочем, секретный ключ надо в любом случае задавать)