Дополнительные поля входа с Spring Security
1. Вступление
В этой статье мы реализуем сценарий пользовательской аутентификации сSpring Security поadding an extra field to the standard login form.
Мы собираемся сосредоточиться на2 different approaches, чтобы показать универсальность фреймворка и гибкие способы его использования.
Our first approach будет простым решением, ориентированным на повторное использование существующих основных реализаций Spring Security.
Our second approach будет более индивидуальным решением, которое может быть более подходящим для сложных случаев использования.
Мы будем опираться на концепции, которые обсуждаются в нашихprevious articles on Spring Security login.
2. Maven Setup
Мы будем использовать стартеры Spring Boot для начальной загрузки нашего проекта и внесения всех необходимых зависимостей.
Для настройки, которую мы будем использовать, требуется родительская декларация, веб-стартер и стартер безопасности; мы также добавим тимелист:
org.springframework.boot
spring-boot-starter-parent
2.0.0.M7
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-thymeleaf
org.thymeleaf.extras
thymeleaf-extras-springsecurity4
Самую последнюю версию стартера безопасности Spring Boot можно найти вover at Maven Central.
3. Простая настройка проекта
В нашем первом подходе мы сосредоточимся на повторном использовании реализаций, предоставляемых Spring Security. В частности, мы будем повторно использоватьDaoAuthenticationProvider иUsernamePasswordToken, поскольку они существуют «из коробки».
Ключевые компоненты будут включать в себя:
-
SimpleAuthenticationFilter – расширениеUsernamePasswordAuthenticationFilter
-
SimpleUserDetailsService – реализацияUserDetailsService
-
User – расширение классаUser, предоставляемое Spring Security, которое объявляет наше дополнительное полеdomain
-
SecurityConfig – наша конфигурация Spring Security, которая вставляет нашSimpleAuthenticationFilter в цепочку фильтров, объявляет правила безопасности и связывает зависимости
-
login.html– страница входа, которая собираетusername,password иdomain
3.1. Простой фильтр аутентификации
В нашемSimpleAuthenticationFilter,the domain and username fields are extracted from the request. Мы объединяем эти значения и используем их для создания экземпляраUsernamePasswordAuthenticationToken.
The token is then passed along to the AuthenticationProvider для аутентификации:
public class SimpleAuthenticationFilter
extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response)
throws AuthenticationException {
// ...
UsernamePasswordAuthenticationToken authRequest
= getAuthRequest(request);
setDetails(request, authRequest);
return this.getAuthenticationManager()
.authenticate(authRequest);
}
private UsernamePasswordAuthenticationToken getAuthRequest(
HttpServletRequest request) {
String username = obtainUsername(request);
String password = obtainPassword(request);
String domain = obtainDomain(request);
// ...
String usernameDomain = String.format("%s%s%s", username.trim(),
String.valueOf(Character.LINE_SEPARATOR), domain);
return new UsernamePasswordAuthenticationToken(
usernameDomain, password);
}
// other methods
}
3.2. Простая услугаUserDetails
КонтрактUserDetailsService определяет единственный метод с именемloadUserByUsername.Our implementation extracts the username and domain.. Затем значения передаются в наш UserRepository, чтобы получитьUser:
public class SimpleUserDetailsService implements UserDetailsService {
// ...
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String[] usernameAndDomain = StringUtils.split(
username, String.valueOf(Character.LINE_SEPARATOR));
if (usernameAndDomain == null || usernameAndDomain.length != 2) {
throw new UsernameNotFoundException("Username and domain must be provided");
}
User user = userRepository.findUser(usernameAndDomain[0], usernameAndDomain[1]);
if (user == null) {
throw new UsernameNotFoundException(
String.format("Username not found for domain, username=%s, domain=%s",
usernameAndDomain[0], usernameAndDomain[1]));
}
return user;
}
}
3.3. Конфигурация Spring Security
Наша установка отличается от стандартной конфигурации Spring Security, потому чтоwe insert our SimpleAuthenticationFilter into the filter chain before the default с вызовомaddFilterBefore:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(authenticationFilter(),
UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/css/**", "/index").permitAll()
.antMatchers("/user/**").authenticated()
.and()
.formLogin().loginPage("/login")
.and()
.logout()
.logoutUrl("/logout");
}
Мы можем использовать предоставленныйDaoAuthenticationProvider, потому что мы настраиваем его с помощью нашегоSimpleUserDetailsService. Вспомните, чтоour SimpleUserDetailsService knows how to parse out our username and domain fields и верните соответствующийUser для использования при аутентификации:
public AuthenticationProvider authProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
Поскольку мы используемSimpleAuthenticationFilter, мы настраиваем наши собственныеAuthenticationFailureHandler, чтобы обеспечить надлежащую обработку неудачных попыток входа в систему:
public SimpleAuthenticationFilter authenticationFilter() throws Exception {
SimpleAuthenticationFilter filter = new SimpleAuthenticationFilter();
filter.setAuthenticationManager(authenticationManagerBean());
filter.setAuthenticationFailureHandler(failureHandler());
return filter;
}
3.4. Страница авторизации
Страница входа в систему, которую мы используем, собирает наше дополнительное полеdomain, которое извлекается нашимSimpleAuthenticationFilter:
Когда мы запускаем приложение и получаем доступ к контексту вhttp://localhost:8081, мы видим ссылку для доступа к защищенной странице. При нажатии на ссылку отобразится страница входа. Как и ожидалось,we see the additional domain field:
3.5. Резюме
В нашем первом примере мы смогли повторно использоватьDaoAuthenticationProvider иUsernamePasswordAuthenticationToken, «подделав» поле имени пользователя.
В результате мы смоглиadd support for an extra login field with a minimal amount of configuration and additional code.
4. Настройка пользовательского проекта
Наш второй подход будет очень похож на первый, но может быть более подходящим для нетривиальных вариантов использования.
Ключевые компоненты нашего второго подхода будут включать:
-
CustomAuthenticationFilter – расширениеUsernamePasswordAuthenticationFilter
-
CustomUserDetailsService – настраиваемый интерфейс, объявляющий методloadUserbyUsernameAndDomain
-
CustomUserDetailsServiceImpl – реализация нашегоCustomUserDetailsService
-
CustomUserDetailsAuthenticationProvider – расширениеAbstractUserDetailsAuthenticationProvider
-
CustomAuthenticationToken – расширениеUsernamePasswordAuthenticationToken
-
User – расширение классаUser, предоставляемое Spring Security, которое объявляет наше дополнительное полеdomain
-
SecurityConfig – наша конфигурация Spring Security, которая вставляет нашCustomAuthenticationFilter в цепочку фильтров, объявляет правила безопасности и связывает зависимости
-
login.html– страница входа, которая собираетusername,password иdomain
4.1. Пользовательский фильтр аутентификации
В нашемCustomAuthenticationFilter мыextract the username, password, and domain fields from the request. Эти значения используются для создания экземпляра нашего CustomAuthenticationToken, который передаетсяAuthenticationProvider для аутентификации:
public class CustomAuthenticationFilter
extends UsernamePasswordAuthenticationFilter {
public static final String SPRING_SECURITY_FORM_DOMAIN_KEY = "domain";
@Override
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response)
throws AuthenticationException {
// ...
CustomAuthenticationToken authRequest = getAuthRequest(request);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
private CustomAuthenticationToken getAuthRequest(HttpServletRequest request) {
String username = obtainUsername(request);
String password = obtainPassword(request);
String domain = obtainDomain(request);
// ...
return new CustomAuthenticationToken(username, password, domain);
}
4.2. Пользовательская услугаUserDetails
Наш контрактCustomUserDetailsService определяет единственный метод под названиемloadUserByUsernameAndDomain.
Созданный нами классCustomUserDetailsServiceImpl просто реализует контракт и делегирует нашемуCustomUserRepository, чтобы получитьUser:
public UserDetails loadUserByUsernameAndDomain(String username, String domain)
throws UsernameNotFoundException {
if (StringUtils.isAnyBlank(username, domain)) {
throw new UsernameNotFoundException("Username and domain must be provided");
}
User user = userRepository.findUser(username, domain);
if (user == null) {
throw new UsernameNotFoundException(
String.format("Username not found for domain, username=%s, domain=%s",
username, domain));
}
return user;
}
4.3. ПользовательскийUserDetailsAuthenticationProvider
НашCustomUserDetailsAuthenticationProvider расширяетAbstractUserDetailsAuthenticationProvider и делегирует нашемуCustomUserDetailService для полученияUser. The most important feature of this class is the implementation of the retrieveUser method.
Обратите внимание, что мы должны привести токен аутентификации к нашемуCustomAuthenticationToken для доступа к нашему настраиваемому полю:
@Override
protected UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
CustomAuthenticationToken auth = (CustomAuthenticationToken) authentication;
UserDetails loadedUser;
try {
loadedUser = this.userDetailsService
.loadUserByUsernameAndDomain(auth.getPrincipal()
.toString(), auth.getDomain());
} catch (UsernameNotFoundException notFound) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials()
.toString();
passwordEncoder.matches(presentedPassword, userNotFoundEncodedPassword);
}
throw notFound;
} catch (Exception repositoryProblem) {
throw new InternalAuthenticationServiceException(
repositoryProblem.getMessage(), repositoryProblem);
}
// ...
return loadedUser;
}
4.4. Резюме
Наш второй подход почти идентичен простому подходу, который мы представили первым. Реализовав собственныеAuthenticationProvider иCustomAuthenticationToken, мы избавились от необходимости адаптировать поле имени пользователя с помощью настраиваемой логики синтаксического анализа.
5. Заключение
В этой статье мы реализовали форму входа в Spring Security, в которой использовалось дополнительное поле для входа. Мы сделали это двумя разными способами:
-
В нашем простом подходе мы минимизировали объем кода, который нам нужно было написать. Мы смоглиreuse DaoAuthenticationProvider and UsernamePasswordAuthentication by adapting the username с помощью настраиваемой логики синтаксического анализа
-
В нашем более индивидуальном подходе мы предоставили поддержку настраиваемых полейextending AbstractUserDetailsAuthenticationProvider and providing our own CustomUserDetailsService with a CustomAuthenticationToken
Как всегда, весь исходный код можно найтиover on GitHub.