Extra inlogvelden met Spring Security

1. Inleiding

In dit artikel implementeren we een aangepast authenticatiescenario met Spring Security door het toevoegen van een extra veld aan het standaard inlogformulier.

We gaan ons concentreren op 2 verschillende benaderingen, om de veelzijdigheid van het framework en de flexibele manieren waarop we het kunnen gebruiken te laten zien.

Onze eerste benadering wordt een eenvoudige oplossing die zich richt op hergebruik van bestaande kernimplementaties van Spring Security.

Onze tweede benadering zal een meer op maat gemaakte oplossing zijn die mogelijk geschikter is voor geavanceerde gebruiksscenario's.

We bouwen voort op de concepten die zijn besproken in onze vorige artikelen over inloggen met Spring Security.

2. Maven-instellingen

We zullen Spring Boot-starters gebruiken om ons project op te starten en alle noodzakelijke afhankelijkheden in te voeren.

De setup die we zullen gebruiken vereist een ouderverklaring, webstarter en beveiligingsstarter; we zullen ook thymeleaf toevoegen:

 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 

De meest recente versie van Spring Boot-beveiligingsstarter is te vinden op Maven Central.

3. Eenvoudige projectconfiguratie

In onze eerste benadering zullen we ons concentreren op het hergebruiken van implementaties die worden geleverd door Spring Security. In het bijzonder zullen we hergebruiken DaoAuthenticationProvider en GebruikersnaamPasswordToken aangezien ze "out-of-the-box" bestaan.

De belangrijkste componenten zijn:

  • SimpleAuthenticationFiltereen extensie van GebruikersnaamPasswordAuthenticationFilter
  • SimpleUserDetailsServiceeen implementatie van UserDetailsService
  • Onseheen uitbreiding van de Gebruiker klasse aangeboden door Spring Security die onze extra declareert domein veld-
  • SecurityConfigonze Spring Security-configuratie die onze SimpleAuthenticationFilter in de filterketen, verklaart beveiligingsregels en koppelt afhankelijkheden aan
  • login.htmleen inlogpagina die de gebruikersnaam, wachtwoord, en domein

3.1. Eenvoudig authenticatiefilter

In onze SimpleAuthenticationFilter, de velden voor domein en gebruikersnaam worden uit het verzoek gehaald. We voegen deze waarden samen en gebruiken ze om een ​​instantie van te maken GebruikersnaamPasswordAuthenticationToken.

Het token wordt vervolgens doorgegeven aan de AuthenticationProvider voor authenticatie:

openbare klasse SimpleAuthenticationFilter breidt UsernamePasswordAuthenticationFilter {@Override openbare AuthenticatiepogingAuthenticatie (HttpServletRequest-verzoek, HttpServletResponse-antwoord) uit met AuthenticationException {// ... UsernamePasswordAuthenticationToken authRequest = getAuthRequest (verzoek); setDetails (verzoek, authRequest); retourneer this.getAuthenticationManager () .authenticate (authRequest); } private UsernamePasswordAuthenticationToken getAuthRequest (verzoek HttpServletRequest) {String gebruikersnaam = getUsername (verzoek); String wachtwoord = getPassword (verzoek); String domein = getDomain (verzoek); // ... String gebruikersnaamDomain = String.format ("% s% s% s", gebruikersnaam.trim (), String.valueOf (Character.LINE_SEPARATOR), domein); retourneer nieuwe UsernamePasswordAuthenticationToken (gebruikersnaamDomein, wachtwoord); } // andere methodes }

3.2. Gemakkelijk Gebruikersdetails Onderhoud

De UserDetailsService contract definieert een enkele methode genaamd loadUserByUsername. Onze implementatie haalt het gebruikersnaam en domein. De waarden worden vervolgens doorgegeven aan onze UserRepository om het Gebruiker:

openbare klasse SimpleUserDetailsService implementeert UserDetailsService {// ... @Override openbare UserDetails loadUserByUsername (String gebruikersnaam) genereert UsernameNotFoundException {String [] usernameAndDomain = StringUtils.split (gebruikersnaam, String.valueOf (Character.LINE_SEPARATOR)); if (usernameAndDomain == null || usernameAndDomain.length! = 2) {throw new UsernameNotFoundException ("Gebruikersnaam en domein moeten worden opgegeven"); } Gebruiker user = userRepository.findUser (gebruikersnaamAndDomain [0], gebruikersnaamAndDomain [1]); if (user == null) {throw new UsernameNotFoundException (String.format ("Gebruikersnaam niet gevonden voor domein, gebruikersnaam =% s, domein =% s", gebruikersnaamAndDomain [0], gebruikersnaamAndDomain [1])); } terugkerende gebruiker; }} 

3.3. Spring-beveiligingsconfiguratie

Onze setup verschilt van een standaard Spring Security-configuratie omdat we voegen onze SimpleAuthenticationFilter in de filterketen vóór de standaard met een oproep naar addFilterBefore:

@Override protected void configure (HttpSecurity http) genereert uitzondering {http .addFilterBefore (authenticationFilter (), UsernamePasswordAuthenticationFilter.class) .authorizeRequests () .antMatchers ("/ css / **", "/ index"). AllowAll () .antMatchers ("/ gebruiker / **"). geverifieerd (). en () .formLogin (). loginPage ("/ login"). en () .logout () .logoutUrl ("/ logout"); }

We kunnen de verstrekte gebruiken DaoAuthenticationProvider omdat we het configureren met onze SimpleUserDetailsService. Herhaal dat onze SimpleUserDetailsService weet hoe hij onze gebruikersnaam en domein velden en retourneer het juiste Gebruiker te gebruiken bij authenticatie:

openbare AuthenticationProvider authProvider () {DaoAuthenticationProvider-provider = nieuwe DaoAuthenticationProvider (); provider.setUserDetailsService (userDetailsService); provider.setPasswordEncoder (wachtwoordEncoder ()); terugkeerprovider; } 

Omdat we een SimpleAuthenticationFilter, we configureren onze eigen AuthenticationFailureHandler om ervoor te zorgen dat mislukte inlogpogingen correct worden afgehandeld:

openbaar SimpleAuthenticationFilter authenticationFilter () genereert uitzondering {SimpleAuthenticationFilter filter = nieuw SimpleAuthenticationFilter (); filter.setAuthenticationManager (authenticationManagerBean ()); filter.setAuthenticationFailureHandler (failureHandler ()); retourfilter; }

3.4. Login pagina

De inlogpagina die we gebruiken, verzamelt onze aanvullende domein veld dat wordt geëxtraheerd door onze SimpleAuthenticationFilter:

Log in alstublieft

Voorbeeld: gebruiker / domein / wachtwoord

Ongeldige gebruiker, wachtwoord of domein

Gebruikersnaam

Domein

Wachtwoord

Log in

Terug naar de startpagina

Wanneer we de applicatie starten en toegang krijgen tot de context op // localhost: 8081, zien we een link om toegang te krijgen tot een beveiligde pagina. Als u op de link klikt, wordt de inlogpagina weergegeven. Zoals verwacht, we zien het extra domeinveld:

3.5. Samenvatting

In ons eerste voorbeeld konden we hergebruiken DaoAuthenticationProvider en GebruikersnaamPasswordAuthenticationToken door het veld gebruikersnaam te "faken".

Het resultaat was dat we dat konden ondersteuning toevoegen voor een extra inlogveld met een minimale hoeveelheid configuratie en extra code.

4. Aangepaste projectinstellingen

Onze tweede benadering zal sterk lijken op de eerste, maar kan geschikter zijn voor gevallen van niet-triviaal gebruik.

De belangrijkste componenten van onze tweede benadering zijn:

  • CustomAuthenticationFiltereen extensie van GebruikersnaamPasswordAuthenticationFilter
  • CustomUserDetailsServiceeen aangepaste interface die een loadUserbyUsernameAndDomain methode
  • CustomUserDetailsServiceImpleen implementatie van onze CustomUserDetailsService
  • CustomUserDetailsAuthenticationProvidereen extensie van AbstractUserDetailsAuthenticationProvider
  • CustomAuthenticationTokeneen extensie van GebruikersnaamPasswordAuthenticationToken
  • Onseheen uitbreiding van de Gebruiker klasse aangeboden door Spring Security die onze extra declareert domein veld-
  • SecurityConfigonze Spring Security-configuratie die onze CustomAuthenticationFilter in de filterketen, verklaart beveiligingsregels en koppelt afhankelijkheden aan
  • login.htmlde inlogpagina die het gebruikersnaam, wachtwoord, en domein

4.1. Aangepast verificatiefilter

In onze CustomAuthenticationFilter, wij extraheer de gebruikersnaam, het wachtwoord en de domeinvelden uit het verzoek. Deze waarden worden gebruikt om een ​​instantie van onze CustomAuthenticationToken die wordt doorgegeven aan de AuthenticationProvider voor authenticatie:

openbare klasse CustomAuthenticationFilter breidt uit UsernamePasswordAuthenticationFilter {openbare statische laatste String SPRING_SECURITY_FORM_DOMAIN_KEY = "domein"; @Override openbare authenticatie pogingAuthenticatie (HttpServletRequest verzoek, HttpServletResponse antwoord) gooit AuthenticationException {// ... CustomAuthenticationToken authRequest = getAuthRequest (verzoek); setDetails (verzoek, authRequest); retourneer this.getAuthenticationManager (). authenticate (authRequest); } private CustomAuthenticationToken getAuthRequest (verzoek HttpServletRequest) {String gebruikersnaam = getAuthRequest (verzoek); String wachtwoord = getPassword (verzoek); String domein = getDomain (verzoek); // ... retourneer nieuwe CustomAuthenticationToken (gebruikersnaam, wachtwoord, domein); }

4.2. Op maat Gebruikersdetails Onderhoud

Onze CustomUserDetailsService contract definieert een enkele methode genaamd loadUserByUsernameAndDomain.

De CustomUserDetailsServiceImpl klasse die we creëren, voert eenvoudigweg het contract uit en delegeert naar onze CustomUserRepository om het Gebruiker:

 openbare UserDetails loadUserByUsernameAndDomain (String gebruikersnaam, String domein) gooit UsernameNotFoundException {if (StringUtils.isAnyBlank (gebruikersnaam, domein)) {throw nieuwe UsernameNotFoundException ("Gebruikersnaam en domein moeten worden opgegeven"); } Gebruiker user = userRepository.findUser (gebruikersnaam, domein); if (user == null) {throw new UsernameNotFoundException (String.format ("Gebruikersnaam niet gevonden voor domein, gebruikersnaam =% s, domein =% s", gebruikersnaam, domein)); } terugkerende gebruiker; }

4.3. Op maat UserDetailsAuthenticationProvider

Onze CustomUserDetailsAuthenticationProvider strekt zich uit AbstractUserDetailsAuthenticationProvider en afgevaardigden naar onze CustomUserDetailService om het Gebruiker. Het belangrijkste kenmerk van deze klasse is de implementatie van de retrieveUser methode.

Merk op dat we het authenticatietoken naar onze CustomAuthenticationToken voor toegang tot ons aangepaste veld:

@Override beschermde UserDetails retrieveUser (String gebruikersnaam, UsernamePasswordAuthenticationToken authentication) genereert AuthenticationException {CustomAuthenticationToken auth = (CustomAuthenticationToken) authenticatie; UserDetails loadedUser; probeer {loadedUser = this.userDetailsService .loadUserByUsernameAndDomain (auth.getPrincipal () .toString (), auth.getDomain ()); } catch (UsernameNotFoundException notFound) {if (authentication.getCredentials ()! = null) {String gepresenteerdePassword = authentication.getCredentials () .toString (); passwordEncoder.matches (gepresenteerdPassword, userNotFoundEncodedPassword); } gooien notFound; } catch (uitzondering repositoryProblem) {gooi nieuwe InternalAuthenticationServiceException (repositoryProblem.getMessage (), repositoryProblem); } // ... return loadedUser; }

4.4. Samenvatting

Onze tweede benadering is bijna identiek aan de eenvoudige benadering die we als eerste hebben gepresenteerd. Door onze eigen te implementeren AuthenticationProvider en CustomAuthenticationToken, hebben we voorkomen dat we ons gebruikersnaamveld moesten aanpassen met aangepaste parseerlogica.

5. Conclusie

In dit artikel hebben we een formulier login geïmplementeerd in Spring Security die gebruik maakte van een extra login veld. We hebben dit op 2 verschillende manieren gedaan:

  • Door onze eenvoudige aanpak hebben we de hoeveelheid code die we moesten schrijven tot een minimum beperkt. We waren in staat om hergebruik DaoAuthenticationProvider en UsernamePasswordAuthentication door de gebruikersnaam aan te passen met aangepaste ontledingslogica
  • In onze meer op maat gemaakte aanpak hebben we veldondersteuning op maat geleverd door het uitbreiden van AbstractUserDetailsAuthenticationProvider en het verstrekken van onze eigen CustomUserDetailsService met een CustomAuthenticationToken

Zoals altijd is alle broncode te vinden op GitHub.