Как устроена Аутентификация в Spring Security

В этой статье проследим, как работает приложение из предыдущей статьи под капотом. Сфокусируемся на аутентификации.

Аутентификация — это проверка, что пользователь есть тот, за кого себя выдает. Чтобы выполнить проверку, надо:

  • Извлечь имя и пароль из HTTP-запроса. За это отвечает UsernamePasswordAuthenticationFilter (конкретно в нашем приложении с Form-Based аутентификацией).
  • Сравнить их с реальными именем и паролем, хранящимся где-то (в базе, на LDAP-сервере, во временной памяти приложения и т.д. где угодно). Это делает AuthenticationManager в методе authenticate().
    Вызывается authenticate() из фильтра UsernamePasswordAuthenticationFilter сразу после извлечения имени/пароля из HTTP-запроса.

Метод authenticate(Authentication authentication) интерфейса AuthenticationManager — проверка пароля

Допустим, нам приходит HTTP-запрос. Прежде чем попасть в контроллер, запрос проходит через цепочку фильтров. В UsernamePasswordAuthenticationFilter имя и пароль вытаскиваются из запроса. Дальше надо их сравнить с реальными. Тут то вступает в дело AuthenticationManager:

public interface AuthenticationManager {
    Authentication authenticate(Authentication var1) throws AuthenticationException;
}

Его единственный метод authenticate() выполняет аутентификацию, то есть решает, действительно ли пользователь тот, за кого себя выдает. Делегируется проверка конкретным провайдерам (в зависимости от того, как хранится реальный пользователь, проверка разнится).

Authentication до аутентификации

Как видно в коде выше, метод authenticate() получает на вход объект Authentication с именем и паролем, полученными от клиента и требующими проверку. Имя хранится в principal, а пароль в credenticals (до проверки, после проверки будет иначе):

До проверки в выделенных полях хранятся имя и пароль
До проверки в выделенных полях хранятся имя и пароль

Содержимое объекта Authentication можно проверить, если запустить предыдущий пример и поставить break-point в методе authenticate() класса ProviderManager. А потом по адресу /login отправить POST-запрос с формы ввода имени/пароля:

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
...
}
Найти класс ProviderManagerCtrl+N в IntelliJ IDEA

 

До аутентификации
До аутентификации

isAuthenticated() до аутентификации равно false.

Если аутентификация не прошла (имя и пароль неверны), то выбрасывается исключение BadCredentials.

В случае же успеха возвращается тоже объект Authentication,  но заполненный по-другому.

Authentication после аутентификации

После аутентификации в поле Principal объекта Authentication будет реальный пользователь в виде UserDetails:

После аутентификации
После аутентификации

При этом поле Credentials обнуляется, а isAuthenticated() меняется с false на true.

UserDetails:

public interface UserDetails extends Serializable { 
    
    Collection<? extends GrantedAuthority> getAuthorities(); 
    String getPassword(); 
    String getUsername(); 
    boolean isAccountNonExpired(); 
    boolean isAccountNonLocked();  
    boolean isCredentialsNonExpired(); 
    boolean isEnabled(); 
}

То есть имя и пароль перемещаются объект Principal:

UserDetails realUser= (UserDetails)authencation.getPrincipal();

Проверить содержимое объекта Authentication после аутентификации можно, внедрив его в контроллер и поставив в нем break-point (смотреть переменные нужно, когда заходишь под уже залогиненным пользователем; поскольку в нашем примере мы настраивали, что путь /user доступен только для залогиненного пользователя, то есть после того, как аутентификация уже успешно прошла):

@RestController
public class HelloController {

...
    @GetMapping("/user")
    public String user(Authentication authentication) {
        System.out.println((UserDetails)authentication.getPrincipal());
        return "User";
    }
...
}

Вывод в консоль:

org.springframework.security.core.userdetails.User@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER
break-point
break-point

Как же AuthenticationManager в authenticate() решает, правильный пароль, или нет? Очевидно, для этого надо сравнить переданный пароль с реальным. А для этого по переданному имени надо извлечь реального пользователя. И вот тут дальнейшее зависит от того, где этот пользователь хранится.

Типы аутентификации в Spring Security

Есть несколько стандартных типов хранения и извлечения пользователя, и за каждый из них отвечает свой AuthenticationProvider. AuthenticationManager делегирует провайдеру извлечь данные их хранилища. В Spring Security реализованы несколько стандартных провайдеров, все они задаются в методе configure():

@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter 
   @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                ....
    }
...
}

Итак, провайдеры:

  • In-Memory — простейший, задан в вышеприведенном фрагменте кода из примера
  • Jdbc —  рассмотрим в следующей статье
  • LDAP

А можно написать свой, так чаще всего и делают (тоже сделано в следующей статье).

Все упирается в получение реального пользователя по его имени — в метод loadUserByName интерфейса  UserDetailsService:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

где UserDetails содержит информацию о реальном пользователе (точнее, нам надо составить эту информацию из того, что есть в базе, например).

И составляем мы ее как раз в методе loadUserByUsername(), если реализовываем его вручную. Важно заполнить password, username и authorities (права) объекта UserDetails .

Но вернемся к методу authenticate(), в котором происходит аутентификация.

SecurityContext — хранилище объекта Authentication

Допустим, аутентификация прошла успешно — это значит, имя и пароль верные.

Тогда объект Authentication сохраняется в SecurityContext, а тот, в свою очередь, — в SecurityContextHolder:

SecurityContextHolder
SecurityContextHolder

Текущего пользователя из него можно получить так:

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
UserDetails principal = (UserDetails) authentication.getPrincipal();

Таким образом, SecurityContext используется для хранения объекта Authentication.

Восстановление Authentication из сессии

Аутентификация в нашем примере происходит только раз. Коль скоро она прошла успешно, authentication восстанавливается из контекста, а в итоге из сессии при последующих запросах. Происходит это в SecurityContextPersistenceFilter.

Сессии в нашем примере включены — они включены по умолчанию, если их специально не отключить. То есть после аутентификации в HTTP-ответе клиенту отправляется уникальный JSESSIONID, который он отправляет во всех последующих запросах. По JSESSIONID  восстанавливается сессия, из нее берется SecurityContext, а из него  Authentication.

Итоги

В тексте выше приводились примеры кода и ставились break-point приложения из статьи.

В следующем примере напишем пользовательскую аутентификацию с помощью UserDetailsService.

 

 

 

Как устроена Аутентификация в Spring Security: 9 комментариев

  1. Самое интересное для меня за кадром осталось(( Не могли бы описать не просто текстом, а код-снипетами (ну или псевдокодом) вот эту часть таинства из последнего буквально предложения: «По JSESSIONID восстанавливается сессия, из нее берется SecurityContext». Хотелось бы понять как оно работает. В принципе в поисках этого кусочка я и забрел сюда, а и тут нет…..
    Буду очень благодарен.

    1. 1) Если больше интересует 1-часть, а именно как «По JSESSIONID восстанавливается сессия» — то к Spring это особого отношения не имеет. Запущенный контейнер сервлетов Tomcat (независимо от Spring) хранит в куче (по умолчанию в куче, но есть варианты) объекты HttpSession пользователей. Это можно представить как ключ-значение, немного подробнее тут (первая часть статьи).

      2) «из нее берется SecurityContext»
      За это отвечает в Spring фильтр SecurityContextPersistenceFilter

      Есть класс HttpSessionSecurityContextRepository implements SecurityContextRepository, так вот в методе SecurityContext loadContext() (а из него в readSecurityContextFromSession()) контекст извлекается в сниппете:
      Object contextFromSession = httpSession.getAttribute(springSecurityContextKey)

      где springSecurityContextKey — строка «SPRING_SECURITY_CONTEXT» — атрибут обычной томкатовской сессии HttpSession. В понятии «атрибут сессии» тоже нет ничего Spring-специфичного. Вот просто есть такой «SPRING_SECURITY_CONTEXT» атрибут, который Spring использует для своих целей — для хранения контекста.

      Да, а в фильтре SecurityContextPersistenceFilter как раз и вызываются методы сохранения и извлечения контекста из HttpSessionSecurityContextRepository

      PS Найти класс — Ctrl+N в Idea

    2. По 1:
      В классе org.apache.catalina.session.ManagerBase (простейший менеджер сессий) есть
      Map sessions = new ConcurrentHashMap<>();
      Вот в ней как раз хранятся активные сессии и их идентификаторы. В менеджере есть методы add(Session), remove(Session)— добавить и удалить сессию.
      Но честно говоря, подробно смотреть код контейнера сервлетов смысла нет, главное — понимать принцип.

  2. У Вас во многих местах указано Authencation, вероятно правильнее будет Authentication 🙂

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

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