Tests unitaires avec sécurité à ressort

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 regarde le module de sécurité Spring pour déterminer si c'est quelque chose que nous pouvons/devrions utiliser.

Nos exigences de sécurité sont assez basiques; un utilisateur a juste besoin de 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 avoir accès.

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) en 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 à la place, ce qui aurait l'avantage de la suppression de toute sorte de "comment pouvons-nous suivre les utilisateurs connectés?"et "comment authentifier les utilisateurs?"de mon contrôleur / code d'entreprise.

Il semble que Spring Security fournisse un objet "contexte" (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 Non-Spring comme si cet objet était un singleton (global), d'une certaine manière.

Ma question Est la suivante: si c'est le moyen standard d'accéder aux informations sur l'authentifié utilisateur dans Spring Security, Quelle est la manière acceptée d'injecter un objet D'authentification dans SecurityContext afin qu'il soit disponible pour mes tests unitaires lorsque les tests unitaires nécessitent un utilisateur authentifié?

Dois-je relier 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. Est-il un moyen plus facile?

L'objet SecurityContextHolder lui-même semble très peu printanier...

104
demandé sur Scott Bale 2008-12-11 22:11:43

11 réponses

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

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

Si vous êtes en implémentant votre propre service d'authentification, vous pouvez essentiellement faire la même chose: créer un bean à portée de session avec une propriété "principal", l'injecter dans votre service d'authentification, demander au service auth de définir la propriété sur auth réussie, puis rendre le service auth disponible aux autres beans selon vos besoins.

Je ne me sentirais pas trop mal d'utiliser SecurityContextHolder. bien. Je sais que c'est un Static / Singleton et que Spring décourage l'utilisation de telles choses mais leur l'implémentation prend soin de se comporter correctement en fonction de l'environnement: session-scoped dans un conteneur de Servlet, thread-scoped dans un test JUnit, etc. Le facteur limitant réel D'un Singleton est lorsqu'il fournit une implémentation inflexible à différents environnements.

34
répondu cliff.meyers 2008-12-16 19:27:37

Faites-le simplement de la manière habituelle, puis insérez-le 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);
108
répondu Leonardo Eloy 2015-12-01 06:15:26

Vous avez tout à fait raison d'être concerné - 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, vous laissant avec un code soigné et testable. SecurityContextHolder est une classe framework et bien qu'il soit possible que votre code de sécurité de bas niveau y soit lié, vous voulez probablement exposer une interface plus propre à vos composants D'interface utilisateur (c'est-à-dire contrôleur).

Falaise.meyers a mentionné une façon de contourner cela-créez votre propre type "principal" et injectez une instance dans les consommateurs. La balise Spring aop:scoped-proxy/> introduite dans 2.x combiné avec une définition de bean de portée de requête, et le support de la méthode d'usine peut être le ticket pour 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û faire la plupart de cela déjà. Ensuite, dans votre bean contexte définir un bean à portée de requête 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);
}

Espérons que cela aide!

27
répondu Pavel 2014-03-29 12:44:35

Sans répondre à la question sur la façon de créer et d'injecter des objets D'authentification, Spring Security 4.0 fournit des alternatives bienvenues en matière de test. L'annotation @WithMockUser permet au développeur de spécifier un utilisateur fictif (avec des autorités facultatives, un nom d'utilisateur, un mot de passe et des rôles) de manière ordonné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 exemple

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

Plus de détails peuvent être trouvés dans le @WithMockUser et les chapitres @ WithUserDetails dans les documents de référence de sécurité Spring (à partir desquels les exemples ci-dessus ont été copiés)

16
répondu matsev 2016-05-25 19:13:26

Personnellement, j'utiliserais simplement Powermock avec Mockito ou Easymock pour me moquer du SecurityContextHolder statique.getSecurityContext () 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 un peu de code de Plaque de chaudière ici, c'est-à-dire simuler un objet D'authentification, simuler un SecurityContext pour retourner l'authentification et enfin simuler le SecurityContextHolder pour obtenir le SecurityContext, mais c'est très flexible et vous permet de tester l'unité pour des scénarios comme null Objets d'authentification, etc. sans avoir à changer votre code (non test)

8
répondu 2012-03-07 21:03:26

L'utilisation d'un statique dans ce cas est la meilleure façon 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. Étant 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 aussi directement que possible. Cacher l'accès derrière une classe wrapper qui est injectée fournit à un attaquant plus de points à attaquer. Ils n'auraient pas besoin d'accéder au code (qu'ils auraient du mal à changer si le jar était signé), ils ont juste besoin d'un moyen de remplacer la configuration, ce qui peut être fait à l'exécution ou glisser du XML sur le classpath. Même en utilisant l'injection d'annotation serait substituable avec XML externe. Un tel XML pourrait injecter le système en cours d'exécution avec un principal voyou.

5
répondu Michael Bushe 2010-02-04 12:59:51

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

3
répondu Scott Bale 2017-05-23 11:47:16

Je voudrais jeter un oeil aux classes de test abstraites de Spring et aux objets fictifs dont on parle ici. Ils fournissent un moyen puissant de câblage automatique de vos objets gérés Spring, ce qui facilite les tests d'unité et d'intégration.

2
répondu digitalsanctum 2008-12-11 19:30:58

Généralités

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

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

Essais

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 cependant directement le SecurityContext. Si vous voulez faire assurez-vous que l'utilisateur est chargé à partir d'une session dans 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
répondu yankee 2015-11-16 12:07:02

Authentification est une propriété d'un fil dans un environnement serveur de la même manière qu'il est une propriété d'un processus en OS. Avoir une instance bean pour accéder aux informations d'authentification serait une configuration et un câblage gênants sans aucun avantage.

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

1
répondu Pavel Horal 2013-06-20 20:55:16

Après beaucoup de travail, j'ai pu reproduire le comportement désiré. J'avais émulé la connexion via MockMvc. C'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 rendront nos tests plus faciles.

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
répondu borjab 2014-06-11 10:59:08