Перейти к основному содержимому

Дополнительные поля входа в систему с Spring Security

· 7 мин. чтения

Задача: Наибольшая подстрока без повторений

Для заданной строки s, найдите длину наибольшей подстроки без повторяющихся символов. Подстрока — это непрерывная непустая последовательность символов внутри строки...

ANDROMEDA 42

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 , мы видим ссылку для доступа к защищенной странице. При нажатии на ссылку откроется страница входа. Как и ожидалось, мы видим дополнительное поле домена :

./aeae3dc690f52df217e36dd00aaebdd1.png

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 .