Authentifizierung gegen einen REST-Dienst mit Spring Security

Inhaltsverzeichnis

  • Link: #Übersicht[ 1. Übersicht]

  • link: #goal[ 2. Das Ziel]

  • link: #client[ 3. Der Kunde]

  • link: #testing[ 4. Authentifizierungsdienst testen]

  • link: #conclusion[ 5. Schlussfolgerung]

1. Überblick

Dieser Artikel konzentriert sich auf die Authentifizierung gegenüber einer sicheren REST-API , die ein RESTful-Benutzerkonto und einen Authentifizierungsdienst bereitstellt.

2. Das Ziel

Gehen wir zunächst die Akteure durch - die typische Spring Security-fähige Anwendung muss sich gegen etwas authentifizieren.

  • eine Datenbank

  • LDAP

  • ein REST-Service

Die Datenbank ist das häufigste Szenario. Ein RESTful-UAA-Dienst (User Account and Authentication) kann jedoch ebenfalls funktionieren.

Für die Zwecke dieses Artikels macht der REST-UAA-Dienst eine einzelne GET-Operation für /authentication verfügbar, die die Principal-Informationen zurückgibt, die Spring Security für die vollständige Authentifizierung benötigt.

3. Der Kunde

Normalerweise verwendet eine einfache Spring Security-fähige Anwendung einen einfachen Benutzerdienst als Authentifizierungsquelle:

<authentication-manager alias="authenticationManager">
    <authentication-provider user-service-ref="customUserDetailsService"/>
</authentication-manager>

Dies würde das org.springframework.security.core.userdetails.UserDetailsService implementieren und den Principal ** basierend auf einem bereitgestellten Benutzernamen zurückgeben:

@Component
public class CustomUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) {
      ...
    }
}

Wenn sich ein Client für den RESTful UAA-Dienst authentifiziert, reicht es nicht mehr aus, nur mit username zu arbeiten. Der Client benötigt nun die vollständigen Anmeldeinformationen ** - username und password -, wenn er die Authentifizierungsanforderung an den Dienst sendet. Dies ist absolut sinnvoll, da der Dienst selbst gesichert ist. Daher muss die Anforderung die Authentifizierungsinformationen enthalten, damit sie ordnungsgemäß verarbeitet werden kann.

Aus Sicht von Spring Security kann dies nicht aus loadUserByUsername heraus erfolgen, da das Kennwort zu diesem Zeitpunkt nicht mehr verfügbar ist.

Dazu können Sie Spring Security den vollständigen Authentifizierungsanbieter zur Verfügung stellen:

<authentication-manager alias="authenticationManager">
    <authentication-provider ref="restAuthenticationProvider"/>
</authentication-manager>

Durch das Überschreiben des gesamten Authentifizierungsanbieters haben wir viel mehr Freiheit, den Principal benutzerdefiniert aus dem Service abzurufen, aber er ist recht komplex. Der Standardauthentifizierungsanbieter - DaoAuthenticationProvider - hat das meiste, was wir brauchen. Ein guter Ansatz wäre, ihn einfach zu erweitern und nur das zu ändern, was erforderlich ist.

Leider ist dies nicht möglich, da retrieveUser - die Methode, an der wir interessiert wären - final ist. Dies ist etwas uninteressant (es gibt https://jira.springsource.org/browse/SEC-1954 (eine JIRA diskutiert das Problem])) - es scheint, als beabsichtige das Design einfach eine alternative Implementierung zu bieten, aber auch kein großes Problem - unser RestAuthenticationProvider kopiert die meisten Implementierungen von DaoAuthenticationProvider und schreibt, was erforderlich ist - den Abruf des Principals aus dem Dienst:

@Override
protected UserDetails retrieveUser(String name, UsernamePasswordAuthenticationToken auth){
    String password = auth.getCredentials().toString();
    UserDetails loadedUser = null;
    try {
        ResponseEntity<Principal> authenticationResponse =
            authenticationApi.authenticate(name, password);
        if (authenticationResponse.getStatusCode().value() == 401) {
            return new User("wrongUsername", "wrongPass",
                Lists.<GrantedAuthority> newArrayList());
        }
        Principal principalFromRest = authenticationResponse.getBody();
        Set<String> privilegesFromRest = Sets.newHashSet();
       //fill in the privilegesFromRest from the Principal
        String[]authoritiesAsArray =
            privilegesFromRest.toArray(new String[privilegesFromRest.size()]);
        List<GrantedAuthority> authorities =
            AuthorityUtils.createAuthorityList(authoritiesAsArray);
        loadedUser = new User(name, password, true, authorities);
    } catch (Exception ex) {
        throw new AuthenticationServiceException(repositoryProblem.getMessage(), ex);
    }
    return loadedUser;
}

Beginnen wir mit dem Anfang ** - der HTTP-Kommunikation mit dem REST-Service. - Dies wird von der authenticationApi – abgewickelt, einer einfachen API, die die authenticate -Operation für den tatsächlichen Service bereitstellt. Die Operation selbst kann mit jeder HTTP-fähigen Bibliothek implementiert werden. In diesem Fall verwendet die Implementierung RestTemplate :

public ResponseEntity<Principal> authenticate(String username, String pass) {
   HttpEntity<Principal> entity = new HttpEntity<Principal>(createHeaders(username, pass))
   return restTemplate.exchange(authenticationUri, HttpMethod.GET, entity, Principal.class);
}

HttpHeaders createHeaders(String email, String password) {
    HttpHeaders acceptHeaders = new HttpHeaders() {
        {
            set(com.google.common.net.HttpHeaders.ACCEPT,
                MediaType.APPLICATION__JSON.toString());
        }
    };
    String authorization = username + ":" + password;
    String basic = new String(Base64.encodeBase64
        (authorization.getBytes(Charset.forName("US-ASCII"))));
    acceptHeaders.set("Authorization", "Basic " + basic);

    return acceptHeaders;
}

Mit FactoryBean kann Folgendes verknüpft werden:/how-to-use-resttemplate-with-basic-authentication-in-spring # resttemplate[RestTemplate im Kontext einrichten].

  • Next ** : Wenn die Authentifizierungsanforderung zu einem HTTP 401 Unauthorized führte, wird höchstwahrscheinlich aufgrund falscher Anmeldeinformationen vom Client ein Principal mit falschen Anmeldeinformationen zurückgegeben, sodass der Spring Security-Authentifizierungsprozess diese ablehnen kann:

return new User("wrongUsername", "wrongPass", Lists.<GrantedAuthority> newArrayList());

Schließlich benötigt der Spring Security Principal einige Berechtigungen - die Privilegien, die dieser bestimmte Principal nach dem Authentifizierungsvorgang lokal hat und verwendet. Die/ authenticate -Operation hatte einen vollständigen Principal abgerufen, einschließlich der Berechtigungen. Daher müssen diese aus dem Ergebnis der Anforderung extrahiert und in GrantedAuthority -Objekte umgewandelt werden, wie von Spring Security gefordert.

Die Details, wie diese Privilegien gespeichert werden, sind hier irrelevant - sie könnten als einfache Strings oder als komplexe Rollen-Privileg-Struktur gespeichert werden. Unabhängig von den Details müssen wir jedoch nur deren Namen verwenden, um die GrantedAuthoritiy -Objekte zu erstellen. Nach der Erstellung des letzten Spring Security-Prinzipals wird der Standardauthentifizierungsprozess wieder hergestellt:

List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(authoritiesAsArray);
loadedUser = new User(name, password, true, authorities);

4. Testen des Authentifizierungsdienstes

Das Schreiben eines Integrationstests, der den Authentifizierungs-REST-Dienst auf dem Happy-Pfad verwendet, ist unkompliziert:

@Test
public void whenAuthenticating__then200IsReceived() {
   //When
    ResponseEntity<Principal> response
        = authenticationRestTemplate.authenticate("admin", "adminPass");

   //Then
    assertThat(response.getStatusCode().value(), is(200));
}

Nach diesem einfachen Test können auch komplexere Integrationstests implementiert werden - dies ist jedoch außerhalb des Postens.

5. Fazit

In diesem Artikel wurde erläutert, wie Sie sich bei einer REST-API authentifizieren können, anstatt bei einem lokalen System, z.

Eine vollständige Implementierung eines sicheren RESTful-Dienstes, der als Authentifizierungsanbieter verwendet werden kann, finden Sie im GitHub-Projekt .