1. Введение
В этой статье мы реализуем собственный сценарий аутентификации с помощью Spring Security , добавив дополнительное поле в стандартную форму входа .
Мы сосредоточимся на двух разных подходах , чтобы показать универсальность фреймворка и гибкие способы его использования.
Нашим первым подходом будет простое решение, ориентированное на повторное использование существующих базовых реализаций Spring Security.
Наш второй подход будет более индивидуальным решением, которое может быть более подходящим для расширенных вариантов использования.
Мы будем опираться на концепции, которые обсуждались в наших предыдущих статьях о входе в Spring Security .
2. Настройка Мавена
Мы будем использовать стартеры Spring Boot для начальной загрузки нашего проекта и добавления всех необходимых зависимостей.
Настройка, которую мы будем использовать, требует родительского объявления, веб-стартера и стартера безопасности; мы также включим тимелеаф:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.1</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
</dependencies>
Самую последнюю версию Spring Boot Security Starter можно найти на Maven Central .
3. Простая настройка проекта
В нашем первом подходе мы сосредоточимся на повторном использовании реализаций, предоставляемых Spring Security. В частности, мы будем повторно использовать DaoAuthenticationProvider
и UsernamePasswordToken
, поскольку они существуют «из коробки».
Ключевые компоненты будут включать в себя:
SimpleAuthenticationFilter
— расширениеUsernamePasswordAuthenticationFilter .
SimpleUserDetailsService
— реализацияUserDetailsService
Пользователь
— `` расширениекласса User
,
предоставляемое Spring Security, которое объявляет наше дополнительное поледомена .
Securi
tyConfig
— наша конфигурация безопасности Spring, которая вставляет нашSimpleAuthenticationFilter
в цепочку фильтров, объявляет правила безопасности и подключает зависимости- login.html — страница входа, которая собирает
имя пользователя
,пароль
идомен .
3.1. Простой фильтр аутентификации
В нашем SimpleAuthenticationFilter
поля домена и имени пользователя извлекаются из запроса . Мы объединяем эти значения и используем их для создания экземпляра UsernamePasswordAuthenticationToken
.
Затем токен передается 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.
Наша реализация извлекает имя пользователя
и домен.
Затем значения передаются в наш 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
Наша настройка отличается от стандартной конфигурации Spring Security, потому что мы вставляем наш SimpleAuthenticationFilter
в цепочку фильтров до значения по умолчанию с вызовом 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
. Напомним, что наш SimpleUserDetailsService
знает, как анализировать наши поля имени пользователя
и домена
и возвращать соответствующего пользователя
для использования при аутентификации:
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. Страница авторизации
Страница входа, которую мы используем, собирает наше дополнительное поле домена , которое извлекается нашим
SimpleAuthenticationFilter:
<form class="form-signin" th:action="@{/login}" method="post">
<h2 class="form-signin-heading">Please sign in</h2>
<p>Example: user / domain / password</p>
<p th:if="${param.error}" class="error">Invalid user, password, or domain</p>
<p>
<label for="username" class="sr-only">Username</label>
<input type="text" id="username" name="username" class="form-control"
placeholder="Username" required autofocus/>
</p>
<p>
<label for="domain" class="sr-only">Domain</label>
<input type="text" id="domain" name="domain" class="form-control"
placeholder="Domain" required autofocus/>
</p>
<p>
<label for="password" class="sr-only">Password</label>
<input type="password" id="password" name="password" class="form-control"
placeholder="Password" required autofocus/>
</p>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button><br/>
<p><a href="/index" th:href="@{/index}">Back to home page</a></p>
</form>
Когда мы запускаем приложение и обращаемся к контексту по адресу http://localhost:8081 , мы видим ссылку для доступа к защищенной странице. При нажатии на ссылку откроется страница входа. Как и ожидалось, мы видим дополнительное поле домена :
3.5. Резюме
В нашем первом примере мы смогли повторно использовать DaoAuthenticationProvider
и UsernamePasswordAuthenticationToken
, «подделав» поле имени пользователя.
В результате мы смогли добавить поддержку дополнительного поля для входа с минимальным количеством настроек и дополнительного кода .
4. Настройка пользовательского проекта
Наш второй подход будет очень похож на первый, но может быть более подходящим для нетривиальных случаев использования.
Ключевые компоненты нашего второго подхода будут включать:
CustomAuthenticationFilter
— расширениеUsernamePasswordAuthenticationFilter .
CustomUserDetailsService
— настраиваемый интерфейс, объявляющий методloadUserbyUsernameAndDomain.
CustomUserDetailsServiceImpl
— реализация нашегоCustomUserDetailsService
CustomUserDetailsAuthenticationProvider
— расширениеAbstractUserDetailsAuthenticationProvider .
CustomAuthenticationToken
— расширениеUsernamePasswordAuthenticationToken .
Пользователь
— `` расширениекласса User
,
предоставляемое Spring Security, которое объявляет наше дополнительное поледомена .
Securi
tyConfig
— наша конфигурация безопасности Spring, которая вставляет нашCustomAuthenticationFilter
в цепочку фильтров, объявляет правила безопасности и подключает зависимости- login.html — страница входа, которая собирает
имя пользователя
,пароль
идомен .
4.1. Пользовательский фильтр аутентификации
В нашем CustomAuthenticationFilter
мы извлекаем из запроса поля имени пользователя, пароля и домена . Эти значения используются для создания экземпляра нашего Custom AuthenticationToken
, который передается 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
. Наиболее важной особенностью этого класса является реализация метода retrieveUser
.
Обратите внимание, что мы должны привести токен аутентификации к нашему 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, в котором использовалось дополнительное поле для входа. Мы сделали это двумя разными способами:
- В нашем простом подходе мы минимизировали объем кода, который нам нужно было написать. Мы смогли повторно использовать
DaoAuthenticationProvider
и UsernamePasswordAuthentication, адаптировав имя пользователя с пользовательской логикой синтаксического анализа . - В нашем более индивидуальном подходе мы предоставили поддержку настраиваемых полей, расширив AbstractUserDetailsAuthenticationProvider и предоставив собственный
CustomUserDetailsService
сCustomAuthenticationToken.
Как всегда, весь исходный код можно найти на GitHub .