Zusätzliche Anmeldefelder mit Spring Security

Zusätzliche Login-Felder mit Spring Security

1. Einführung

In diesem Artikel implementieren wir ein benutzerdefiniertes Authentifizierungsszenario mitSpring Security maladding an extra field to the standard login form.

Wir werden uns auf2 different approacheskonzentrieren, um die Vielseitigkeit des Frameworks und die flexiblen Möglichkeiten zu zeigen, wie wir es verwenden können.

Our first approach ist eine einfache Lösung, die sich auf die Wiederverwendung vorhandener Kernimplementierungen von Spring Security konzentriert.

Our second approach ist eine individuellere Lösung, die möglicherweise besser für fortgeschrittene Anwendungsfälle geeignet ist.

Wir bauen auf Konzepten auf, die in unserenprevious articles on Spring Security login behandelt werden.

2. Maven Setup

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

Für das Setup, das wir verwenden, sind eine übergeordnete Deklaration, ein Webstarter und ein Sicherheitsstarter erforderlich. Wir werden auch Thymeleaf einschließen:


    org.springframework.boot
    spring-boot-starter-parent
    2.0.0.M7
    



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

Die aktuellste Version des Spring Boot-Sicherheitsstarters befindet sich inover at 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 wirDaoAuthenticationProvider undUsernamePasswordToken wiederverwenden, da sie "out-of-the-box" existieren.

Die Schlüsselkomponenten umfassen:

  • SimpleAuthenticationFilter ist eine Erweiterung vonUsernamePasswordAuthenticationFilter

  • SimpleUserDetailsService ist eine Implementierung vonUserDetailsService

  • Userist eine Erweiterung der von Spring Security bereitgestellten KlasseUser, die unser zusätzliches Felddomaindeklariert

  • SecurityConfigist unsere Spring Security-Konfiguration, die unsereSimpleAuthenticationFilterin die Filterkette einfügt, Sicherheitsregeln deklariert und Abhängigkeiten verkabelt

  • login.html ist eine Anmeldeseite, auf derusername,password unddomain erfasst werden

3.1. Einfacher Authentifizierungsfilter

In unserenSimpleAuthenticationFilter,the domain and username fields are extracted from the request. Wir verketten diese Werte und verwenden sie, um eine Instanz vonUsernamePasswordAuthenticationToken zu erstellen.

The token is then passed along to the AuthenticationProvider für die Authentifizierung:

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. EinfacherUserDetails Service

DerUserDetailsService-Vertrag definiert eine einzelne Methode namensloadUserByUsername.Our implementation extracts the username and domain.. Die Werte werden dann an unsere UserRepository übergeben, um dieUser 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, dawe insert our SimpleAuthenticationFilter into the filter chain before the default mit einem Aufruf vonaddFilterBefore:

@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 die bereitgestelltenDaoAuthenticationProvider verwenden, da wir sie mit unserenSimpleUserDetailsService konfigurieren. Erinnern Sie sich anour SimpleUserDetailsService knows how to parse out our username and domain fields und geben Sie die entsprechendenUser zurück, die bei der Authentifizierung verwendet werden sollen:

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

Da wirSimpleAuthenticationFilter verwenden, konfigurieren wir unsere eigenenAuthenticationFailureHandler, 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ätzlichesdomain-Feld, das von unserenSimpleAuthenticationFilter: extrahiert wird

Wenn wir die Anwendung ausführen und mithttp://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,we see the additional domain field:

image

3.5. Zusammenfassung

In unserem ersten Beispiel konnten wirDaoAuthenticationProvider undUsernamePasswordAuthenticationToken wiederverwenden, indem wir das Feld für den Benutzernamen „ausfälschten“.

Infolgedessen konnten wiradd support for an extra login field with a minimal amount of configuration and additional code.

4. Benutzerdefiniertes Projekteinrichtung

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

Die Schlüsselkomponenten unseres zweiten Ansatzes umfassen:

  • CustomAuthenticationFilter ist eine Erweiterung vonUsernamePasswordAuthenticationFilter

  • CustomUserDetailsServiceist eine benutzerdefinierte Schnittstelle, die eineloadUserbyUsernameAndDomain-Methode deklariert

  • CustomUserDetailsServiceImpl ist eine Implementierung unsererCustomUserDetailsService

  • CustomUserDetailsAuthenticationProvider ist eine Erweiterung vonAbstractUserDetailsAuthenticationProvider

  • CustomAuthenticationToken ist eine Erweiterung vonUsernamePasswordAuthenticationToken

  • Userist eine Erweiterung der von Spring Security bereitgestellten KlasseUser, die unser zusätzliches Felddomaindeklariert

  • SecurityConfigist unsere Spring Security-Konfiguration, die unsereCustomAuthenticationFilterin die Filterkette einfügt, Sicherheitsregeln deklariert und Abhängigkeiten verkabelt

  • login.html die Anmeldeseite, auf derusername,password unddomain erfasst werden

4.1. Benutzerdefinierter Authentifizierungsfilter

In unserenCustomAuthenticationFilter sind wirextract the username, password, and domain fields from the request. Diese Werte werden verwendet, um eine Instanz unserer benutzerdefiniertenAuthenticationTokenzu erstellen, die zur Authentifizierung anAuthenticationProviderü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. BenutzerdefinierterUserDetails-Service

UnserCustomUserDetailsService Vertrag definiert eine einzelne Methode namensloadUserByUsernameAndDomain.

Die von uns erstellte KlasseCustomUserDetailsServiceImplimplementiert einfach den Vertrag und delegiert an unsereCustomUserRepository, um dieUser zu erhalten:

 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. BenutzerdefinierteUserDetailsAuthenticationProvider

UnsereCustomUserDetailsAuthenticationProvider erweiternAbstractUserDetailsAuthenticationProvider und delegieren an unsereCustomUserDetailService, um dieUser abzurufen. The most important feature of this class is the implementation of the retrieveUser method.

Beachten Sie, dass wir das Authentifizierungstoken für den Zugriff auf unser benutzerdefiniertes Feld inCustomAuthenticationToken umwandeln müssen:

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

Unser zweiter Ansatz ist nahezu identisch mit dem einfachen Ansatz, den wir zuerst vorgestellt haben. Durch die Implementierung unserer eigenenAuthenticationProvider undCustomAuthenticationToken mussten wir vermeiden, dass unser Benutzernamenfeld mit einer benutzerdefinierten Parsing-Logik angepasst werden muss.

5. Fazit

In diesem Artikel haben wir in Spring Security eine Formularanmeldung implementiert, bei der ein zusätzliches Anmeldefeld verwendet wurde. Wir haben das auf zwei verschiedene Arten gemacht:

  • In unserem einfachen Ansatz haben wir die Menge an Code minimiert, die wir schreiben mussten. Wir konntenreuse DaoAuthenticationProvider and UsernamePasswordAuthentication by adapting the username mit benutzerdefinierter Parsing-Logik

  • In unserem individuelleren Ansatz haben wir eine benutzerdefinierte Feldunterstützung vonextending AbstractUserDetailsAuthenticationProvider and providing our own CustomUserDetailsService with a CustomAuthenticationToken bereitgestellt

Wie immer kann der gesamte Quellcodeover on GitHub gefunden werden.