155 votes

Tests unitaires avec Spring Security

Mon entreprise a évalué Spring MVC pour déterminer si nous devrions l'utiliser dans l'un de nos prochains projets. Jusqu'à présent, j'aime ce que j'ai vu, et en ce moment, je jette un coup d'œil au module Spring Security pour déterminer si nous pouvons/devons l'utiliser.

Nos exigences en matière de sécurité sont assez basiques ; un utilisateur doit simplement pouvoir fournir un nom d'utilisateur et un mot de passe pour pouvoir accéder à certaines parties du site (par exemple pour obtenir des informations sur son compte) ; et il y a une poignée de pages sur le site (FAQ, Support, etc.) où un utilisateur anonyme devrait pouvoir accéder.

Dans le prototype que j'ai créé, j'ai stocké un objet "LoginCredentials" (qui contient juste le nom d'utilisateur et le mot de passe) dans Session pour un utilisateur authentifié ; certains contrôleurs vérifient si cet objet est en session pour obtenir une référence au nom d'utilisateur connecté, par exemple. Je cherche à remplacer cette logique maison par Spring Security, ce qui aurait l'avantage de supprimer toute sorte de "comment suivre les utilisateurs connectés ?" et "comment authentifier les utilisateurs ?" de mon code de contrôleur/business.

Il semble que Spring Security fournisse un objet "context" (par thread) pour pouvoir accéder aux informations de nom d'utilisateur/principal depuis n'importe où dans votre application...

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

... ce qui semble très peu printanier puisque cet objet est un singleton (global), d'une certaine manière.

Ma question est la suivante : si c'est la façon standard d'accéder aux informations sur l'utilisateur authentifié dans Spring Security, quelle est la façon acceptée d'injecter un objet d'authentification dans le SecurityContext afin qu'il soit disponible pour mes tests unitaires lorsque ceux-ci nécessitent un utilisateur authentifié ?

Dois-je le faire dans la méthode d'initialisation de chaque scénario de test ?

protected void setUp() throws Exception {
    ...
    SecurityContextHolder.getContext().setAuthentication(
        new UsernamePasswordAuthenticationToken(testUser.getLogin(), testUser.getPassword()));
    ...
}

Cela semble trop verbeux. Existe-t-il un moyen plus simple ?

Le site SecurityContextHolder l'objet lui-même semble très peu printanier...

192voto

Leonardo Eloy Points 126

Il suffit de le faire de la manière habituelle et de l'insérer ensuite en utilisant SecurityContextHolder.setContext() dans votre classe de test, par exemple :

Contrôleur :

Authentication a = SecurityContextHolder.getContext().getAuthentication();

Test :

Authentication authentication = Mockito.mock(Authentication.class);
// Mockito.whens() for your authorization object
SecurityContext securityContext = Mockito.mock(SecurityContext.class);
Mockito.when(securityContext.getAuthentication()).thenReturn(authentication);
SecurityContextHolder.setContext(securityContext);

3 votes

@Leonardo où cela devrait-il Authentication a être ajouté dans le contrôleur ? Comme je peux le comprendre, dans chaque invocation de méthode ? Est-il possible de l'ajouter à la "manière Spring", au lieu de l'injecter ?

0 votes

Mais n'oubliez pas que cela ne fonctionnera pas avec TestNG, car SecurityContextHolder contient une variable locale du thread, ce qui signifie que vous partagez cette variable entre les tests...

1 votes

Faites-le en @BeforeEach (JUnit5) ou @Before (JUnit 4). Bon et simple.

49voto

cliff.meyers Points 10394

Le problème est que Spring Security ne rend pas l'objet Authentification disponible en tant que bean dans le conteneur, il n'y a donc aucun moyen de l'injecter ou de l'autofire facilement.

Avant d'utiliser Spring Security, nous créions un bean de session dans le conteneur pour stocker le principal, nous l'injections dans un "AuthenticationService" (singleton), puis nous injections ce bean dans d'autres services qui avaient besoin de connaître le principal actuel.

Si vous implémentez votre propre service d'authentification, vous pouvez faire la même chose : créer un bean de session avec une propriété "principal", l'injecter dans votre service d'authentification, faire en sorte que le service d'authentification définisse la propriété en cas d'authentification réussie, puis rendre le service d'authentification disponible pour d'autres beans selon vos besoins.

Je ne me sentirais pas trop mal d'utiliser le SecurityContextHolder, cependant. Je sais que c'est un Singleton statique et que Spring décourage l'utilisation de telles choses, mais leur implémentation prend soin de se comporter de manière appropriée en fonction de l'environnement : session-scoped dans un conteneur Servlet, thread-scoped dans un test JUnit, etc. Le vrai facteur limitant d'un Singleton est lorsqu'il fournit une implémentation qui n'est pas flexible aux différents environnements.

0 votes

Merci, ces conseils sont utiles. Ce que j'ai fait jusqu'à présent, c'est de procéder à l'appel de SecurityContextHolder.getContext() (par le biais de quelques méthodes enveloppantes de mon cru, de sorte qu'au moins il n'est appelé que par une seule classe).

2 votes

Une remarque : je ne pense pas que ServletContextHolder ait un quelconque concept de HttpSession ou un moyen de savoir s'il fonctionne dans un environnement de serveur web. Il utilise ThreadLocal à moins que vous ne le configuriez pour utiliser autre chose (les deux seuls autres modes intégrés sont InheritableThreadLocal et Global).

0 votes

Le seul inconvénient de l'utilisation des beans de session/requête dans Spring est qu'ils échoueront dans un test JUnit. Ce que vous pouvez faire est d'implémenter un scope personnalisé qui utilisera session/request si disponible et se rabattra sur thread si nécessaire. Je pense que Spring Security fait quelque chose de similaire...

30voto

Pavel Points 1128

Vous avez tout à fait raison de vous inquiéter - les appels de méthodes statiques sont particulièrement problématiques pour les tests unitaires car vous ne pouvez pas facilement simuler vos dépendances. Ce que je vais vous montrer, c'est comment laisser le conteneur IoC de Spring faire le sale boulot pour vous, vous laissant avec un code propre et testable. SecurityContextHolder est une classe de framework et bien qu'il soit acceptable que votre code de sécurité de bas niveau soit lié à cette classe, vous souhaitez probablement exposer une interface plus soignée à vos composants d'interface utilisateur (c'est-à-dire les contrôleurs).

cliff.meyers a mentionné un moyen de contourner ce problème - créer votre propre type "principal" et injecter une instance dans les consommateurs. L'application Spring < aop:scoped-proxy La balise /> introduite dans la version 2.x, combinée à une définition de la portée de la requête et à la prise en charge des méthodes d'usine, peut permettre d'obtenir le code le plus lisible.

Cela pourrait fonctionner comme suit :

public class MyUserDetails implements UserDetails {
    // this is your custom UserDetails implementation to serve as a principal
    // implement the Spring methods and add your own methods as appropriate
}

public class MyUserHolder {
    public static MyUserDetails getUserDetails() {
        Authentication a = SecurityContextHolder.getContext().getAuthentication();
        if (a == null) {
            return null;
        } else {
            return (MyUserDetails) a.getPrincipal();
        }
    }
}

public class MyUserAwareController {        
    MyUserDetails currentUser;

    public void setCurrentUser(MyUserDetails currentUser) { 
        this.currentUser = currentUser;
    }

    // controller code
}

Rien de compliqué jusqu'à présent, non ? En fait, vous avez probablement déjà eu à faire la plupart de ces choses. Ensuite, dans votre contexte de bean, définissez un bean de type request-scoped pour contenir le principal :

<bean id="userDetails" class="MyUserHolder" factory-method="getUserDetails" scope="request">
    <aop:scoped-proxy/>
</bean>

<bean id="controller" class="MyUserAwareController">
    <property name="currentUser" ref="userDetails"/>
    <!-- other props -->
</bean>

Grâce à la magie de la balise aop:scoped-proxy, la méthode statique getUserDetails sera appelée chaque fois qu'une nouvelle requête HTTP arrivera et toutes les références à la propriété currentUser seront résolues correctement. Maintenant, les tests unitaires deviennent triviaux :

protected void setUp() {
    // existing init code

    MyUserDetails user = new MyUserDetails();
    // set up user as you wish
    controller.setCurrentUser(user);
}

J'espère que cela vous aidera !

9voto

Toby Hobson Points 933

Personnellement, j'utiliserais simplement Powermock avec Mockito ou Easymock pour simuler le SecurityContextHolder.getSecurityContext() statique dans votre test unitaire/intégration, par exemple.

@RunWith(PowerMockRunner.class)
@PrepareForTest(SecurityContextHolder.class)
public class YourTestCase {

    @Mock SecurityContext mockSecurityContext;

    @Test
    public void testMethodThatCallsStaticMethod() {
        // Set mock behaviour/expectations on the mockSecurityContext
        when(mockSecurityContext.getAuthentication()).thenReturn(...)
        ...
        // Tell mockito to use Powermock to mock the SecurityContextHolder
        PowerMockito.mockStatic(SecurityContextHolder.class);

        // use Mockito to set up your expectation on SecurityContextHolder.getSecurityContext()
        Mockito.when(SecurityContextHolder.getSecurityContext()).thenReturn(mockSecurityContext);
        ...
    }
}

Il est vrai qu'il y a pas mal de code passe-partout ici, c'est-à-dire simuler un objet d'authentification, simuler un SecurityContext pour renvoyer l'authentification et enfin simuler le SecurityContextHolder pour obtenir le SecurityContext, mais c'est très flexible et cela vous permet de faire des tests unitaires pour des scénarios comme des objets d'authentification nuls, etc. sans avoir à modifier votre code (non testé).

7voto

Michael Bushe Points 148

L'utilisation d'un statique dans ce cas est la meilleure façon d'écrire un code sécurisé.

Oui, les statiques sont généralement mauvais - généralement, mais dans ce cas, le statique est ce que vous voulez. Puisque le contexte de sécurité associe un Principal au thread en cours d'exécution, le code le plus sûr accéderait à la statique à partir du thread aussi directement que possible. Cacher l'accès derrière une classe enveloppante qui est injectée fournit à un attaquant plus de points d'attaque. Il n'a pas besoin d'accéder au code (qu'il aurait du mal à modifier si le jar est signé), il a juste besoin d'un moyen de remplacer la configuration, ce qui peut être fait au moment de l'exécution ou en glissant du XML sur le classpath. Même l'injection d'annotations peut être remplacée par un XML externe. Un tel XML pourrait injecter un principal malveillant dans le système en cours d'exécution.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X