Campi di accesso aggiuntivi con Spring Security

1. Introduzione

In questo articolo, implementeremo uno scenario di autenticazione personalizzato con Spring Security aggiungendo un campo aggiuntivo al modulo di accesso standard .

Ci concentreremo su 2 diversi approcci , per mostrare la versatilità del framework e i modi flessibili in cui possiamo usarlo.

Il nostro primo approccio sarà una soluzione semplice che si concentra sul riutilizzo delle implementazioni principali esistenti di Spring Security.

Il nostro secondo approccio sarà una soluzione più personalizzata che potrebbe essere più adatta per casi d'uso avanzati.

Ci baseremo sui concetti discussi nei nostri articoli precedenti sull'accesso a Spring Security.

2. Installazione di Maven

Useremo gli starter Spring Boot per avviare il nostro progetto e portare tutte le dipendenze necessarie.

La configurazione che useremo richiede una dichiarazione genitore, un web starter e un security starter; includeremo anche thymeleaf:

 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  

La versione più recente di Spring Boot Security Starter può essere trovata su Maven Central.

3. Configurazione semplice del progetto

Nel nostro primo approccio, ci concentreremo sul riutilizzo delle implementazioni fornite da Spring Security. In particolare, riutilizzeremo DaoAuthenticationProvider e UsernamePasswordToken in quanto esistono "out-of-the-box".

I componenti chiave includeranno:

  • SimpleAuthenticationFilter : un'estensione di UsernamePasswordAuthenticationFilter
  • SimpleUserDetailsService : un'implementazione di UserDetailsService
  • Us er - un'estensione dellaclasse User fornita da Spring Security che dichiara il nostrocampo di dominio aggiuntivo
  • Securi tyConfig - nostra configurazione Primavera di sicurezza che si inserisce la nostra SimpleAuthenticationFilter nella catena dei filtri, dichiara regole di sicurezza e fili fino dipendenze
  • login.html : una pagina di accesso che raccoglie nome utente , password e dominio

3.1. Filtro di autenticazione semplice

Nel nostro SimpleAuthenticationFilter , i campi del dominio e del nome utente vengono estratti dalla richiesta . Concateniamo questi valori e li usiamo per creare un'istanza di UsernamePasswordAuthenticationToken .

Il token viene quindi passato al AuthenticationProvider per l'autenticazione :

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. Servizio UserDetails semplice

Il contratto UserDetailsService definisce un unico metodo denominato loadUserByUsername. La nostra implementazione estrae il nome utente e il dominio. I valori vengono quindi passati al nostro U serRepository per ottenere l' utente :

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. Configurazione Spring Security

La nostra configurazione è diversa da una configurazione standard di Spring Security perché inseriamo il nostro SimpleAuthenticationFilter nella catena di filtri prima del valore predefinito con una chiamata a 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"); }

Siamo in grado di utilizzare il DaoAuthenticationProvider fornito perché lo configuriamo con il nostro SimpleUserDetailsService . Ricordiamo che il nostro SimpleUserDetailsService sa come analizzare i nostri campi nome utente e dominio e restituire l' utente appropriato da utilizzare durante l'autenticazione:

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

Poiché stiamo utilizzando un SimpleAuthenticationFilter , configuriamo il nostro AuthenticationFailureHandler per garantire che i tentativi di accesso non riusciti siano gestiti in modo appropriato:

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

3.4. Pagina di login

La pagina di accesso che utilizziamo raccoglie il nostro campo di dominio aggiuntivo che viene estratto dal nostro SimpleAuthenticationFilter:

Please sign in

Example: user / domain / password

Invalid user, password, or domain

Username

Domain

Password

Sign in

Back to home page

Quando eseguiamo l'applicazione e accediamo al contesto in // localhost: 8081, vediamo un collegamento per accedere a una pagina protetta. Facendo clic sul collegamento verrà visualizzata la pagina di accesso. Come previsto, vediamo il campo del dominio aggiuntivo :

3.5. Sommario

Nel nostro primo esempio, siamo stati in grado di riutilizzare DaoAuthenticationProvider e UsernamePasswordAuthenticationToken " simulando " il campo del nome utente.

Di conseguenza, siamo stati in grado di aggiungere il supporto per un campo di accesso aggiuntivo con una quantità minima di configurazione e codice aggiuntivo .

4. Installazione del progetto personalizzato

Il nostro secondo approccio sarà molto simile al primo ma potrebbe essere più appropriato per casi d'uso non banali.

I componenti chiave del nostro secondo approccio includeranno:

  • CustomAuthenticationFilter : un'estensione di UsernamePasswordAuthenticationFilter
  • CustomUserDetailsService : un'interfaccia personalizzata che dichiara unmetodo loadUserbyUsernameAndDomain
  • CustomUserDetailsServiceImpl : un'implementazione del nostro CustomUserDetailsService
  • CustomUserDetailsAuthenticationProvider : un'estensione di AbstractUserDetailsAuthenticationProvider
  • CustomAuthenticationToken : un'estensione di UsernamePasswordAuthenticationToken
  • Us er - un'estensione dellaclasse User fornita da Spring Security che dichiara il nostrocampo di dominio aggiuntivo
  • Securi tyConfig - nostra configurazione Primavera di sicurezza che si inserisce la nostra CustomAuthenticationFilter nella catena dei filtri, dichiara regole di sicurezza e fili fino dipendenze
  • login.html : la pagina di accesso che raccoglie nome utente , password e dominio

4.1. Filtro di autenticazione personalizzato

Nel nostro CustomAuthenticationFilter , estraiamo i campi nome utente, password e dominio dalla richiesta . Questi valori vengono utilizzati per creare un'istanza del nostro token di autenticazione personalizzato che viene passato a AuthenticationProvider per l'autenticazione:

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. Servizio UserDetails personalizzato

Il nostro contratto CustomUserDetailsService definisce un unico metodo chiamato loadUserByUsernameAndDomain.

La classe CustomUserDetailsServiceImpl che creiamo implementa semplicemente il contratto e delega al nostro CustomUserRepository per ottenere l' utente :

 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
  • Nel nostro approccio più personalizzato, abbiamo fornito supporto sul campo personalizzato estendendo AbstractUserDetailsAuthenticationProvider e fornendo il nostro CustomUserDetailsService con un CustomAuthenticationToken

Come sempre, tutto il codice sorgente può essere trovato su GitHub.