В этой статье проследим, как работает приложение из предыдущей статьи под капотом. Сфокусируемся на аутентификации.
Аутентификация — это проверка, что пользователь есть тот, за кого себя выдает. Чтобы выполнить проверку, надо:
- Извлечь имя и пароль из 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 { ... }
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
Как же 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:
Текущего пользователя из него можно получить так:
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.
Самое интересное для меня за кадром осталось(( Не могли бы описать не просто текстом, а код-снипетами (ну или псевдокодом) вот эту часть таинства из последнего буквально предложения: «По JSESSIONID восстанавливается сессия, из нее берется SecurityContext». Хотелось бы понять как оно работает. В принципе в поисках этого кусочка я и забрел сюда, а и тут нет…..
Буду очень благодарен.
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
По 1:
В классе org.apache.catalina.session.ManagerBase (простейший менеджер сессий) есть
Map sessions = new ConcurrentHashMap<>();
Вот в ней как раз хранятся активные сессии и их идентификаторы. В менеджере есть методы add(Session), remove(Session)— добавить и удалить сессию.
Но честно говоря, подробно смотреть код контейнера сервлетов смысла нет, главное — понимать принцип.
Спасибо огромное!
У Вас во многих местах указано Authencation, вероятно правильнее будет Authentication 🙂
Спасибо, опечатка исправлена)
спасибо
думаю тут где то должен быть UsernamePasswordAuthenticationToken
это сам authentication (на картинках с дебагом видно тип)