Tests unitaires avec Spring Security

Mon entreprise a évalué Spring MVC pour déterminer si nous devions l’utiliser dans l’un de nos prochains projets. Jusqu’à présent, j’adore ce que j’ai vu, et en ce moment, je jette un coup d’œil au module Spring Security pour déterminer si c’est quelque chose que nous pouvons / devrions 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 (par exemple, 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 access.

Dans le prototype que j’ai créé, j’ai stocké un object “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 object est en session pour obtenir une référence au nom d’utilisateur connecté, par exemple. Je cherche plutôt à remplacer cette logique développée par Spring Security, ce qui aurait l’avantage de supprimer “comment suivre les utilisateurs connectés?” et “comment authentifier les utilisateurs?” de mon contrôleur / code métier.

Il semble que Spring Security fournisse un object “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 improbable puisque cet object est un singleton (global), en quelque sorte.

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 object Authentification dans SecurityContext pour qu’il soit disponible pour les tests unitaires lorsque les tests unitaires nécessitent un Utilisateur authentifié?

Dois-je le twigr dans la méthode d’initialisation de chaque test élémentaire?

 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’object SecurityContextHolder lui-même semble très non-Spring-like …

Le problème est que Spring Security ne rend pas l’object Authentification disponible en tant que bean dans le conteneur. Il est donc impossible de l’injecter ou de l’envoyer automatiquement.

Avant de commencer à utiliser Spring Security, nous avons créé un bean de session dans le conteneur pour stocker le principal, l’injecter dans un “AuthenticationService” (singleton) et ensuite injecter ce bean dans d’autres services nécessitant une connaissance du principal actuel.

Si vous implémentez votre propre service d’authentification, vous pouvez pratiquement faire la même chose: créer un bean de scope de session avec une propriété “principal”, injecter ceci dans votre service d’authentification, demander au service d’authentification de définir la propriété avec succès Rendre le service d’authentification disponible pour les autres beans comme vous en avez besoin.

Je ne me sentirais pas trop mal d’utiliser SecurityContextHolder. bien que. Je sais que c’est un static / Singleton 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: scope dans un conteneur Servlet, scope dans un test JUnit, etc. d’un Singleton c’est quand il fournit une implémentation qui est inflexible à différents environnements.

Faites-le de la manière habituelle, 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); 

Vous avez 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. Qu’est-ce que je vais vous montrer, c’est comment laisser le conteneur Spring IoC faire le sale boulot pour vous, vous laissant avec du code propre et testable. SecurityContextHolder est une classe de framework et, bien que cela puisse convenir pour que votre code de sécurité de bas niveau y soit associé, vous souhaiterez probablement exposer une interface plus soignée à vos composants d’interface utilisateur (à savoir les contrôleurs).

Cliff.meyers l’a mentionné dans un sens: créez votre propre type “principal” et injectez une instance dans les consommateurs. La balise Spring < aop: scoped-proxy /> introduite dans 2.x, combinée à une définition de bean scope de requête, et la prise en charge de la méthode usine peuvent constituer le ticket vers 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’ici, non? En fait, vous deviez probablement faire la plupart de ces tâches déjà. Ensuite, dans votre contexte de bean, définissez un bean de requête pour contenir le principal:

        

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 arrive et que toute référence à la propriété currentUser sera résolue correctement. Maintenant, les tests unitaires deviennent sortingviaux:

 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!

Sans répondre à la question de savoir comment créer et injecter des objects d’authentification, Spring Security 4.0 offre d’autres solutions intéressantes en matière de test. L’annotation @WithMockUser permet au développeur de spécifier un utilisateur @WithMockUser (avec des permissions facultatives, un nom d’utilisateur, un mot de passe et des rôles) de manière claire:

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

Il est également possible d’utiliser @WithUserDetails pour émuler un UserDetails renvoyé par UserDetailsService , par exemple

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

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

Personnellement, je me contenterais d’utiliser Powermock avec Mockito ou Easymock pour simuler le statique SecurityContextHolder.getSecurityContext () dans votre test d’unité / 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 chauffante ici: simuler un object Authentification, simuler un SecurityContext pour retourner l’authentification et enfin se moquer du SecurityContextHolder pour obtenir le SecurityContext, cependant très flexible et vous permet de tester des objects comme des objects d’authentification etc. sans avoir à changer votre code (non test)

Utiliser un statique dans ce cas 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, le 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ède à l’élément statique du thread aussi directement que possible. Cacher l’access derrière une classe d’encapsulation 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 fichier jar était signé), ils ont juste besoin d’un moyen de remplacer la configuration, ce qui peut être fait au moment de l’exécution ou du XML sur le classpath. Même en utilisant l’injection d’annotation serait remplaçable avec XML externe. Un tel XML pourrait injecter le système en cours avec un principal non autorisé.

J’ai posé moi-même la même question ici et je viens de poster une réponse que j’ai récemment trouvée. La réponse courte est: injecter un SecurityContext , et se référer à SecurityContextHolder seulement dans votre configuration de Spring pour obtenir le SecurityContext

Je voudrais jeter un coup d’oeil aux classes de test abstraites de Spring et aux objects fictifs dont on parle ici . Ils constituent un puissant moyen de câblage automatique des objects gérés par Spring, facilitant ainsi les tests d’unité et d’intégration.

Général

En attendant (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 cependant directement le SecurityContext. Si vous voulez vous assurer que l’utilisateur est chargé depuis 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().setAtsortingbute( HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, securityContext ); return request; } }; } 

L’authentification est une propriété d’un thread dans un environnement serveur de la même manière qu’une 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 des tests, il existe plusieurs manières de vous simplifier la vie. Mon préféré est de créer une annotation personnalisée @Authenticated et de tester le programme d’écoute, qui le gère. Vérifiez DirtiesContextTestExecutionListener pour de l’inspiration.

Après pas mal de travail, j’ai pu reproduire le comportement souhaité. J’avais simulé 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 disposé à voir les nouvelles fonctionnalités de 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.getAtsortingbute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); SecurityContextHolder.setContext(securityContext); // Your test goes here. User is logged with }