Zusätzliche Anmeldefelder mit Spring Security

1. Einleitung

In diesem Artikel implementieren wir ein benutzerdefiniertes Authentifizierungsszenario mit Spring Security, indem wir dem Standardanmeldeformular ein zusätzliches Feld hinzufügen .

Wir werden uns auf zwei verschiedene Ansätze konzentrieren , um die Vielseitigkeit des Frameworks und die flexiblen Möglichkeiten zu zeigen, wie wir es verwenden können.

Unser erster Ansatz wird eine einfache Lösung sein, die sich auf die Wiederverwendung vorhandener Spring Security-Kernimplementierungen konzentriert.

Unser zweiter Ansatz wird eine individuellere Lösung sein, die möglicherweise für fortgeschrittene Anwendungsfälle besser geeignet ist.

Wir werden auf Konzepten aufbauen, die in unseren vorherigen Artikeln zur Spring Security-Anmeldung erläutert wurden.

2. Maven Setup

Wir werden Spring Boot-Starter verwenden, um unser Projekt zu booten und alle erforderlichen Abhängigkeiten einzubeziehen.

Das Setup, das wir verwenden, erfordert eine übergeordnete Deklaration, einen Webstarter und einen Sicherheitsstarter. Wir werden auch Thymeleaf einschließen:

 org.springframework.boot spring-boot-starter-parent 2.2.6.RELEASE     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-springsecurity5  

Die aktuellste Version des Spring Boot-Sicherheitsstarters finden Sie bei Maven Central.

3. Einfache Projekteinrichtung

In unserem ersten Ansatz konzentrieren wir uns auf die Wiederverwendung von Implementierungen, die von Spring Security bereitgestellt werden. Insbesondere werden wir DaoAuthenticationProvider und UsernamePasswordToken wiederverwenden, da sie "out-of-the-box" existieren.

Die Schlüsselkomponenten umfassen:

  • SimpleAuthenticationFilter - eine Erweiterung von UsernamePasswordAuthenticationFilter
  • SimpleUserDetailsService - eine Implementierung von UserDetailsService
  • Us sich - eine Erweiterung der Benutzerklasse von Spring SecurityVerfügung gestelltdie unsere extra erklärt Domain Feld
  • Securi tyConfig - unsere Spring Security-Konfiguration, die unseren SimpleAuthenticationFilter in die Filterketteeinfügt, Sicherheitsregeln deklariert und Abhängigkeiten verkabelt
  • login.html - Eine Anmeldeseite, auf der der Benutzername , das Kennwort und die Domain erfasst werden

3.1. Einfacher Authentifizierungsfilter

In unserem SimpleAuthenticationFilter werden die Felder Domäne und Benutzername aus der Anforderung extrahiert . Wir verketten diese Werte und verwenden sie, um eine Instanz von UsernamePasswordAuthenticationToken zu erstellen .

Das Token wird dann zur Authentifizierung an den AuthenticationProvider weitergeleitet :

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. Einfacher UserDetails- Dienst

Der UserDetailsService- Vertrag definiert eine einzelne Methode namens loadUserByUsername. Unsere Implementierung extrahiert den Benutzernamen und die Domain. Die Werte werden dann an unser U serRepository übergeben , um den Benutzer zu erhalten :

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-Sicherheitskonfiguration

Unser Setup unterscheidet sich von einer Standardkonfiguration von Spring Security, da wir unseren SimpleAuthenticationFilter vor dem Standard mit einem Aufruf von addFilterBefore in die Filterkette einfügen :

@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"); }

Wir können den bereitgestellten DaoAuthenticationProvider verwenden, da wir ihn mit unserem SimpleUserDetailsService konfigurieren . Daran erinnert , dass unser SimpleUserDetailsService weiß , wie unsere zu analysieren, Benutzername und Domain - Felder und die entsprechenden Rück Benutzer zur Verwendung bei der Authentifizierung:

public AuthenticationProvider authProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(userDetailsService); provider.setPasswordEncoder(passwordEncoder()); return provider; } 

Da wir einen SimpleAuthenticationFilter verwenden , konfigurieren wir unseren eigenen AuthenticationFailureHandler, um sicherzustellen, dass fehlgeschlagene Anmeldeversuche ordnungsgemäß behandelt werden:

public SimpleAuthenticationFilter authenticationFilter() throws Exception { SimpleAuthenticationFilter filter = new SimpleAuthenticationFilter(); filter.setAuthenticationManager(authenticationManagerBean()); filter.setAuthenticationFailureHandler(failureHandler()); return filter; }

3.4. Loginseite

Die von uns verwendete Anmeldeseite sammelt unser zusätzliches Domänenfeld , das von unserem SimpleAuthenticationFilter extrahiert wird :

Please sign in

Example: user / domain / password

Invalid user, password, or domain

Username

Domain

Password

Sign in

Back to home page

Wenn wir die Anwendung ausführen und unter // localhost: 8081 auf den Kontext zugreifen, wird ein Link zum Zugriff auf eine gesicherte Seite angezeigt. Durch Klicken auf den Link wird die Anmeldeseite angezeigt. Wie erwartet sehen wir das zusätzliche Domainfeld :

3.5. Zusammenfassung

In unserem ersten Beispiel konnten wir DaoAuthenticationProvider und UsernamePasswordAuthenticationToken wiederverwenden, indem wir das Feld Benutzernameausfälschten “.

Infolgedessen konnten wir Unterstützung für ein zusätzliches Anmeldefeld mit minimalem Konfigurationsaufwand und zusätzlichem Code hinzufügen .

4. Benutzerdefiniertes Projekteinrichtung

Unser zweiter Ansatz wird dem ersten sehr ähnlich sein, ist jedoch möglicherweise für nicht triviale Anwendungsfälle besser geeignet.

Die Schlüsselkomponenten unseres zweiten Ansatzes umfassen:

  • CustomAuthenticationFilter - eine Erweiterung von UsernamePasswordAuthenticationFilter
  • CustomUserDetailsService - Eine benutzerdefinierte Schnittstelle, die eine loadUserbyUsernameAndDomain- Methodedeklariert
  • CustomUserDetailsServiceImpl - eine Implementierung unseres CustomUserDetailsService
  • CustomUserDetailsAuthenticationProvider - eine Erweiterung von AbstractUserDetailsAuthenticationProvider
  • CustomAuthenticationToken - eine Erweiterung von UsernamePasswordAuthenticationToken
  • Us sich - eine Erweiterung der Benutzerklasse von Spring SecurityVerfügung gestelltdie unsere extra erklärt Domain Feld
  • Securi tyConfig - unsere Spring Security-Konfiguration, die unseren CustomAuthenticationFilter in die Filterketteeinfügt, Sicherheitsregeln deklariert und Abhängigkeiten verkabelt
  • login.html - Die Anmeldeseite, auf der der Benutzername , das Kennwort und die Domain erfasst werden

4.1. Benutzerdefinierter Authentifizierungsfilter

In unserem CustomAuthenticationFilter extrahieren wir die Felder Benutzername, Kennwort und Domäne aus der Anforderung . Diese Werte werden verwendet, um eine Instanz unseres benutzerdefinierten Authentifizierungstokens zu erstellen , die zur Authentifizierung an den AuthenticationProvider übergeben wird :

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. Benutzerdefinierter UserDetails- Dienst

Unser CustomUserDetailsService- Vertrag definiert eine einzelne Methode namens loadUserByUsernameAndDomain.

Die von uns erstellte CustomUserDetailsServiceImpl- Klasse implementiert einfach den Vertrag und delegiert ihn an unser CustomUserRepository , um den Benutzer abzurufen :

 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. Custom UserDetailsAuthenticationProvider

Our CustomUserDetailsAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider and delegates to our CustomUserDetailService to retrieve the User. The most important feature of this class is the implementation of the retrieveUser method.

Note that we must cast the authentication token to our CustomAuthenticationToken for access to our custom field:

@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. Summary

Our second approach is nearly identical to the simple approach we presented first. By implementing our own AuthenticationProvider and CustomAuthenticationToken, we avoided needing to adapt our username field with custom parsing logic.

5. Conclusion

In this article, we've implemented a form login in Spring Security that made use of an extra login field. We did this in 2 different ways:

  • In our simple approach, we minimized the amount of code we needed write. We were able to reuse DaoAuthenticationProvider and UsernamePasswordAuthentication by adapting the username with custom parsing logic
  • In unserem individuelleren Ansatz haben wir benutzerdefinierte Feldunterstützung bereitgestellt, indem wir AbstractUserDetailsAuthenticationProvider erweitert und unseren eigenen CustomUserDetailsService mit einem CustomAuthenticationToken ausgestattet haben

Wie immer ist der gesamte Quellcode auf GitHub zu finden.