web-dev-qa-db-fra.com

Test unitaire avec Spring Security

Mon entreprise évalue Spring MVC afin de 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, pour l'instant, je regarde le module Spring Security pour déterminer si nous pouvons/devons l'utiliser.

Nos exigences 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 (pour obtenir des informations sur son compte, par exemple); et il y a une poignée de pages sur le site (FAQ, Support, etc.) où un utilisateur anonyme devrait avoir accès.

Dans le prototype que j'ai créé, j'ai stocké un objet "LoginCredentials" (qui ne contient que 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 plutôt à remplacer cette logique locale par Spring Security, ce qui aurait l'avantage de supprimer tout type de "comment suivrons-nous les utilisateurs connectés?" et "comment authentifions-nous les utilisateurs?" de mon contrôleur/code de l'entreprise.

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

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

... ce qui semble très indéfinissable, car cet objet est en quelque sorte un singleton (global).

Ma question est la suivante: s’il s’agit du moyen standard d’accéder aux informations relatives à l’utilisateur authentifié dans Spring Security, quel est le moyen accepté d’injecter un objet Authentication dans SecurityContext afin qu’il soit disponible pour mes tests unitaires lorsque les tests unitaires nécessitent une Utilisateur authentifié?

Dois-je câbler cela dans la méthode d'initialisation de chaque cas de test?

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

Cela semble trop verbeux. Y a-t-il un moyen plus facile?

L'objet SecurityContextHolder lui-même semble très ressemblant à celui d'un printemps ...

127
matt b

Le problème est que Spring Security ne rend pas l’objet Authentication disponible sous forme de bean dans le conteneur. Il n’ya donc aucun moyen de l’injecter ou de le transférer automatiquement hors de la boîte.

Avant de commencer à utiliser Spring Security, nous créions un bean de session dans le conteneur pour stocker le principal, l'injections dans un "AuthenticationService" (singleton), puis injections ce bean dans d'autres services nécessitant une connaissance du principal actuel.

Si vous implémentez votre propre service d'authentification, vous pouvez en principe faire la même chose: créer un bean de session avec une propriété "principale", l'injecter dans votre service d'authentification, laisser le service d'authentification définir la propriété sur une authentification réussie, puis rendre le service d'authentification disponible pour les autres haricots selon vos besoins.

Je ne me sentirais pas trop mal à l'idée d'utiliser SecurityContextHolder. bien que. Je sais que c’est un/Singleton statique et que Spring décourage d’utiliser de telles choses, mais leur implémentation prend soin de se comporter de manière appropriée en fonction de l’environnement: portée de session dans un conteneur Servlet, étendue de thread dans un test JUnit, etc. Le véritable facteur limitant d’un Singleton, c’est quand il fournit une implémentation inflexible à différents environnements.

42
cliff.meyers

Faites-le simplement comme d'habitude, puis insérez-le en utilisant SecurityContextHolder.setContext() dans votre classe de test, par exemple:

Manette:

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

Tester:

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);
139
Leonardo Eloy

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 vous moquer de vos dépendances. Ce que je vais vous montrer, c'est comment laisser le conteneur Spring IoC faire le sale boulot pour vous, en vous laissant avec un code net et testable. SecurityContextHolder est une classe de structure et, même si votre code de sécurité de bas niveau y est lié, vous souhaiterez probablement exposer une interface plus claire à vos composants d'interface utilisateur (c'est-à-dire aux contrôleurs).

cliff.meyers a mentionné un moyen de contourner le problème - créez votre propre type "principal" et injectez une instance dans les consommateurs. La balise Spring < aop: scoped-proxy /> introduite dans la version 2.x combinée à une définition de bean scope de requête, et la prise en charge de la méthode factory peuvent être le ticket du 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à dû faire la plupart de ces tâches. Ensuite, dans votre contexte de haricot, définissez un haricot à la demande 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 nouvelle demande HTTP et toute référence à la propriété currentUser sera résolue correctement. Maintenant, le test unitaire devient trivial:

protected void setUp() {
    // existing init code

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

J'espère que cela t'aides!

29
Pavel

Sans répondre à la question relative à la création et à l’injection d’objets d’Authentification, Spring Security 4.0 offre des solutions de rechange intéressantes en matière de test. Le @WithMockUser annotation permet au développeur de spécifier un utilisateur fictif (avec les autorités, le nom d'utilisateur, le mot de passe et les rôles facultatifs) de manière soignée:

@Test
@WithMockUser(username = "admin", authorities = { "ADMIN", "USER" })
public void getMessageWithMockUserCustomAuthorities() {
    String message = messageService.getMessage();
    ...
}

Il y a aussi la possibilité d'utiliser @WithUserDetails pour émuler un UserDetails renvoyé par le UserDetailsService, par ex.

@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
    String message = messageService.getMessage();
    ...
}

Vous trouverez plus de détails dans les chapitres @ WithMockUser et @ WithUserDetails dans la documentation de référence Spring Security (à partir de laquelle les exemples ci-dessus ont été copiés).

24
matsev

Personnellement, je voudrais simplement utiliser Powermock avec Mockito ou Easymock pour simuler le SecurityContextHolder.getSecurityContext () statique dans votre unité/test d’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);
        ...
    }
}

Certes, il y a pas mal de code de plaque de chaudière ici: c’est-à-dire créer un objet d’authentification, un SecurityContext pour renvoyer l’authentification et enfin le SecurityContextHolder pour obtenir le SecurityContext, mais il est très flexible et permet de tester un peu des scénarios tels que null. etc. sans avoir à changer votre code (non test)

9
user404345

Dans ce cas, utiliser un statique est le meilleur moyen d'écrire du code sécurisé.

Oui, la statique est généralement mauvaise - généralement, mais dans ce cas, la statique est ce que vous voulez. Etant donné que le contexte de sécurité associe un principal au thread en cours d'exécution, le code le plus sécurisé accéderait à la statique à partir du thread le plus directement possible. Cacher l'accès derrière une classe wrapper injectée fournit à un attaquant plus de points à attaquer. Ils n'auraient pas besoin d'accéder au code (ce qui leur serait difficile de changer si le fichier jar était signé), ils ont juste besoin d'un moyen de remplacer la configuration, ce qui peut être fait à l'exécution ou de glisser du code XML sur le chemin de classe. Même en utilisant l'injection d'annotation, il serait possible de remplacer le XML externe. Un tel XML pourrait injecter au système en fonctionnement un principal non autorisé.

5
Michael Bushe

J'ai moi-même posé la même question sur ici , et je viens de poster une réponse que j'ai récemment trouvée. La réponse courte est: injectez SecurityContext, et référez-vous à SecurityContextHolder uniquement dans votre configuration Spring pour obtenir le SecurityContext

4
Scott Bale

Général

Dans l'intervalle (depuis la version 3.2, en 2013, grâce à SEC-2298 ), l'authentification peut être injectée dans les méthodes MVC à l'aide de l'annotation @ AuthenticationPrincipal :

@Controller
class Controller {
  @RequestMapping("/somewhere")
  public void doStuff(@AuthenticationPrincipal UserDetails myUser) {
  }
}

Des tests

Dans votre test unitaire, vous pouvez évidemment appeler cette méthode directement. Dans les tests d'intégration utilisant org.springframework.test.web.servlet.MockMvc, Vous pouvez utiliser org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user() pour injecter l'utilisateur comme ceci:

mockMvc.perform(get("/somewhere").with(user(myUserDetails)));

Cela remplira toutefois directement le SecurityContext. Si vous voulez vous assurer que l'utilisateur est chargé à partir d'une session de votre test, vous pouvez utiliser ceci:

mockMvc.perform(get("/somewhere").with(sessionUser(myUserDetails)));
/* ... */
private static RequestPostProcessor sessionUser(final UserDetails userDetails) {
    return new RequestPostProcessor() {
        @Override
        public MockHttpServletRequest postProcessRequest(final MockHttpServletRequest request) {
            final SecurityContext securityContext = new SecurityContextImpl();
            securityContext.setAuthentication(
                new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities())
            );
            request.getSession().setAttribute(
                HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, securityContext
            );
            return request;
        }
    };
}
2
yankee

Je voudrais jeter un coup d'œil aux classes de test abstraites de Spring et aux objets fictifs dont on parle ici . Ils fournissent un moyen puissant d'auto-câblage de vos objets gérés Spring, facilitant ainsi les tests d'unités et d'intégration.

2
digitalsanctum

L'authentification est une propriété d'un thread dans un environnement de serveur de la même manière que c'est la propriété d'un processus dans un système d'exploitation. Avoir une instance de bean pour accéder aux informations d'authentification serait une configuration peu pratique et une surcharge de câblage sans aucun avantage.

En ce qui concerne l'authentification de test, il existe plusieurs façons de vous simplifier la vie. Mon préféré est de faire une annotation personnalisée @Authenticated et écouteur d'exécution de test, qui le gère. Vérifiez DirtiesContextTestExecutionListener pour l'inspiration.

1
Pavel Horal

Après beaucoup de travail, j'ai pu reproduire le comportement souhaité. J'avais imité la connexion via MockMvc. Il est trop lourd pour la plupart des tests unitaires mais utile pour les tests d'intégration.

Bien sûr, je suis prêt à voir ces nouvelles fonctionnalités dans Spring Security 4.0 qui faciliteront nos tests.

package [myPackage]

import static org.junit.Assert.*;

import javax.inject.Inject;
import javax.servlet.http.HttpSession;

import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;

@ContextConfiguration(locations={[my config file locations]})
@WebAppConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public static class getUserConfigurationTester{

    private MockMvc mockMvc;

    @Autowired
    private FilterChainProxy springSecurityFilterChain;

    @Autowired
    private MockHttpServletRequest request;

    @Autowired
    private WebApplicationContext webappContext;

    @Before  
    public void init() {  
        mockMvc = MockMvcBuilders.webAppContextSetup(webappContext)
                    .addFilters(springSecurityFilterChain)
                    .build();
    }  


    @Test
    public void testTwoReads() throws Exception{                        

    HttpSession session  = mockMvc.perform(post("/j_spring_security_check")
                        .param("j_username", "admin_001")
                        .param("j_password", "secret007"))
                        .andDo(print())
                        .andExpect(status().isMovedTemporarily())
                        .andExpect(redirectedUrl("/index"))
                        .andReturn()
                        .getRequest()
                        .getSession();

    request.setSession(session);

    SecurityContext securityContext = (SecurityContext)   session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);

    SecurityContextHolder.setContext(securityContext);

        // Your test goes here. User is logged with 
}
0
borjab