Authentification auprès d’un service REST avec sécurité Spring

Table des matières

  • link: #overview[ 1. Overview]

  • link: #goal[ 2. L’objectif]

  • link: #client[ 3. Le client]

  • link: #testing[ 4. Test du service d’authentification]

  • link: #conclusion[ 5. Conclusion]

1. Vue d’ensemble

Cet article porte sur la manière de s’authentifier auprès d’une API REST sécurisée fournissant un compte utilisateur RESTful et un service d’authentification.

2. Le but

Voyons d’abord les acteurs - l’application typique de Spring Security devant s’authentifier contre quelque chose - que quelque chose peut être:

  • une base de données

  • LDAP

  • un service REST

La base de données est le scénario le plus courant. Cependant, un service RESTful UAA (compte d’utilisateur et authentification) peut aussi bien fonctionner.

Pour les besoins de cet article, le service REST UAA exposera une seule opération GET sur /authentication , qui renverra les informations Principal requises par Spring Security pour exécuter le processus d’authentification complet.

3. Le client

En règle générale, une application simple compatible avec Spring Security utilise un service utilisateur simple comme source d’authentification:

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

Cela implémenterait le org.springframework.security.core.userdetails.UserDetailsService et renverrait le principal ** en fonction d’un username fourni:

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

Lorsqu’un client s’authentifie auprès du service RESTful UAA, travailler uniquement avec nom utilisateur ne suffira plus - le client a maintenant besoin des informations d’identification complètes - à la fois username et password - lorsqu’il envoie la demande d’authentification au service. Cela est parfaitement logique, car le service lui-même est sécurisé, de sorte que la demande doit contenir les informations d’authentification pour pouvoir être traitée correctement.

Du point de vue de Spring Security, cela ne peut pas être fait à partir de loadUserByUsername car le mot de passe password n’est plus disponible à ce stade - nous devons prendre le contrôle du processus d’authentification plus tôt .

Nous pouvons le faire en fournissant le fournisseur d’authentification complet à Spring Security:

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

Le remplacement du fournisseur d’authentification dans son intégralité nous laisse beaucoup plus de liberté pour effectuer une récupération personnalisée du principal du service, mais elle est assez complexe. Le fournisseur d’authentification standard - DaoAuthenticationProvider - possède l’essentiel de ce dont nous avons besoin. Il serait donc judicieux de l’étendre et de ne modifier que ce qui est nécessaire.

Malheureusement, cela n’est pas possible, car retrieveUser - la méthode que nous souhaiterions étendre - est final . Ceci est quelque peu peu intuitif (il y a a JIRA discutant du problème ) - il semble que l’intention de conception ici est simplement de fournir une implémentation alternative qui n’est pas idéale mais pas un problème majeur non plus - notre RestAuthenticationProvider copie-colle la majeure partie de la mise en œuvre de DaoAuthenticationProvider et récrit ce dont il a besoin - la récupération du principal du service:

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

Commençons par le début ** - la communication HTTP avec le service REST - elle est gérée par authenticationApi -- une simple API fournissant l’opération authenticate pour le service réel. L’opération elle-même peut être implémentée avec n’importe quelle bibliothèque capable de HTTP - dans ce cas, l’implémentation utilise 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;
}

Un FactoryBean peut être utilisé pour lier:/comment-utiliser-resttemplate-avec-basic-authentication-in-spring # resttemplate[configurer le RestTemplate dans le contexte].

  • Ensuite ** , si la demande d’authentification a abouti à un HTTP 401 Unauthorized , probablement à cause d’informations d’identification incorrectes fournies par le client, un principal avec des informations d’identification incorrectes est renvoyé afin que le processus d’authentification de Spring Security puisse les refuser:

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

Enfin, le principal de sécurité Spring a besoin de certaines autorités, des privilèges que ce principal aura et qu’il utilisera localement après le processus d’authentification. L’opération/ authenticate avait récupéré un principal complet, y compris des privilèges; ceux-ci doivent donc être extraits du résultat de la demande et transformés en objets GrantedAuthority , comme requis par Spring Security.

Les détails de la manière dont ces privilèges sont stockés sont sans importance ici - ils pourraient être stockés sous forme de simples chaînes ou sous la forme d’une structure complexe Role-Privilege - mais quels que soient les détails, il suffit d’utiliser leurs noms pour construire les objets GrantedAuthoritiy . Une fois le principal Spring Security créé, il est renvoyé au processus d’authentification standard:

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

4. Test du service d’authentification

Écrire un test d’intégration qui utilise le service REST d’authentification sur le chemin d’accès content est assez simple:

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

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

Suite à ce test simple, des tests d’intégration plus complexes peuvent également être mis en œuvre. Cependant, cela sort du cadre de cet article.

5. Conclusion

Cet article explique comment s’authentifier auprès d’une API REST plutôt que sur un système local tel qu’une base de données.

Pour une implémentation complète d’un service RESTful sécurisé pouvant servir de fournisseur d’authentification, consultez le projet GitHub .