Question

Mon entreprise a évalué Spring MVC pour déterminer si nous devrions l'utiliser dans un de nos prochains projets. Jusqu'à présent, j'aime ce que je l'ai vu, et maintenant je prends un coup d'oeil sur le module de sécurité du printemps pour déterminer si elle est quelque chose que nous pouvons / devons utiliser.

Nos exigences de sécurité sont assez simples; un utilisateur a juste besoin d'être en mesure de fournir un nom d'utilisateur et mot de passe pour pouvoir accéder à certaines parties du site (par exemple pour obtenir des informations sur leur compte); et il y a une poignée de pages sur le site (FAQ, soutien, etc.) où un utilisateur anonyme doit être donné accès.

Dans le prototype, j'ai crée, je suis le stockage d'un objet « LoginCredentials » (qui ne contient que le nom d'utilisateur et mot de passe) en session d'un utilisateur authentifié; certains des contrôleurs vérifier pour voir si cet objet est en session pour obtenir une référence au nom d'utilisateur connecté, par exemple. Je cherche à remplacer cette logique adulte à la maison avec Spring Security à la place, ce qui aurait bel avantage d'éliminer toute sorte de « comment nous surveillons-utilisateurs connectés? » et « comment pouvons-nous authentifier les utilisateurs? » de mon contrôleur / code d'entreprise.

Il semble que la sécurité Spring fournit un (par thread) « contexte » objet pour pouvoir accéder au nom d'utilisateur / principale information où que vous soyez dans votre application ...

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

... qui semble très non comme ressort que cet objet est un (global) singleton, d'une manière.

Ma question est la suivante: si c'est le moyen standard pour accéder à des informations sur l'utilisateur authentifié dans Spring Security, quel est le moyen accepté d'injecter un objet d'authentification dans le SecurityContext afin qu'il soit disponible pour mes tests unitaires lorsque l'unité tests nécessitent un utilisateur authentifié?

Ai-je besoin de 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 bavard. Y at-il un moyen plus facile?

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

Était-ce utile?

La solution

Le problème est que la sécurité du printemps ne fait pas l'objet d'authentification disponible en tant que haricot dans le récipient, donc il n'y a aucun moyen d'injecter facilement ou hors de lier automatiquement la boîte.

Avant de commencer à utiliser Spring Security, nous créerions un haricot scope session dans le conteneur pour stocker le principal, injecter dans un « AuthenticationService » (singleton), puis injecter ce grain dans d'autres services qui avaient besoin connaissance du principal courant.

Si vous implémentez votre propre service d'authentification, vous pouvez essentiellement faire la même chose: créer un haricot scope session avec une propriété « principal », injecter dans votre service d'authentification, que le service auth définir la propriété sur auth réussie , puis rendre le service auth disponible à d'autres haricots que vous en avez besoin.

Je ne me sentirais pas trop mal sur l'utilisation SecurityContextHolder. bien que. Je sais que c'est statique / Singleton et que le printemps décourage l'utilisation de telles choses, mais leur mise en œuvre prend soin de se comporter de manière appropriée en fonction de l'environnement: scope session dans un conteneur de servlets, scope fil dans un test JUnit, etc. Le vrai facteur limitant d'un Singleton est quand il fournit une implémentation qui est rigide à différents environnements.

Autres conseils

Il suffit de le faire de la manière habituelle, puis insérez-le à l'aide dans votre classe SecurityContextHolder.setContext() 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);

Vous êtes tout à fait raison d'être concernés - les méthodes statiques sont particulièrement problématiques pour les tests unitaires que vous ne pouvez pas facilement se moquer de vos dépendances. Ce que je vais vous montrer comment laisser le conteneur IoC Spring faire le sale boulot pour vous, vous laissant avec le code propre, testables. SecurityContextHolder est une classe-cadre et si elle peut être ok pour votre code de sécurité de bas niveau pour être lié à elle, vous voulez probablement exposer une interface plus propre à vos composants de l'interface utilisateur (à savoir les contrôleurs).

cliff.meyers mentionné d'une manière autour d'elle - créez votre propre type « principal » et injecter une instance en consommateurs. Le printemps << a href = "http://static.springframework.org/spring/docs/2.5.x/reference/beans.html#beans-factory-scopes-other-injection" rel = "noreferrer"> aop: scope-proxy /> balise introduite en 2.x combinée à une portée de demande de définition haricot, et le soutien-méthode de fabrication peut être le ticket pour le code le plus lisible.

Il 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 eu à faire plus de ce déjà. Ensuite, dans votre contexte de haricot définir un haricot scope demande à tenir 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>

Merci à la magie du aop: tag scope-proxy, les getUserDetails méthodes statiques seront appelés à chaque fois qu'une nouvelle requête HTTP arrive et toute référence à la propriété currentUser sera résolu correctement. Maintenant, les tests unitaires devient trivial:

protected void setUp() {
    // existing init code

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

Hope this helps!

Sans répondre à la question sur la façon de créer et injecter des objets d'authentification, Spring Security 4.0 fournit des solutions d'accueil en matière de tests. L'annotation permet au développeur @WithMockUser de spécifier un utilisateur simulé (avec les autorités en option, nom d'utilisateur, mot de passe et les rôles) d'une manière nette:

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

Il y a aussi la possibilité d'utiliser pour émuler un @WithUserDetails retour de la UserDetails UserDetailsService, par exemple.

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

Plus de détails peuvent être trouvés dans la section @ WithMockUser et @ WithUserDetails chapitres dans la documentation de référence Spring Security (dont les exemples ci-dessus ont été copiés)

Personnellement, je voudrais simplement utiliser Powermock avec Mockito ou easymock pour se moquer de la SecurityContextHolder.getSecurityContext () statique dans votre test unité / 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 que il y a un peu de code plaque de la chaudière ici à savoir se moquer un objet d'authentification, se moquer d'un SecurityContext pour retourner l'authentification et enfin se moquer de l'SecurityContextHolder pour obtenir le SecurityContext, mais son très flexible et vous permet de tester l'unité pour les scénarios comme null objets d'authentification, etc., sans avoir à changer votre (non test) code

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

Oui, sont généralement mauvais statics - en général, mais dans ce cas, la statique est ce que vous voulez. Étant donné que le contexte de sécurité associe un principal avec le fil en cours d'exécution, le code le plus sûr serait accéder à la statique du fil le plus directement possible. Hiding l'accès derrière une classe wrapper qui est injecté fournit un attaquant avec plus de points à l'attaque. Ils ne voulaient pas avoir accès au code (qu'ils auraient du mal à changer si le pot a été signé), ils ont juste besoin d'un moyen de passer outre la configuration, qui peut être fait à l'exécution ou de glisser un peu XML sur le chemin de classe. Même en utilisant une injection d'annotation serait Overridable avec XML externe. Un tel XML pourrait injecter le système en cours d'exécution avec un principal escroc.

Je me demandais la même question sur ici , et vient de poster une réponse que j'ai récemment trouvé. Réponse courte est: injecter un SecurityContext et consultez seulement dans votre SecurityContextHolder config de printemps pour obtenir le <=>

Je prendrais un coup d'œil à des cours de test abstraites de printemps et les objets fantaisie qui ont parlé de ici . Ils fournissent un moyen puissant de-câblage automatique de vos objets gérés de fabrication de ressorts, les tests unitaires et d'intégration plus facile.

Général

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

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

Tests

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

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

Cela cependant suffit de remplir directement le SecurityContext. Si vous voulez vous assurer que l'utilisateur est chargé 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;
        }
    };
}

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

En ce qui concerne l'authentification de test il y a plusieurs façons comment vous pouvez vous rendre la vie plus facile. Mon préféré est de faire une annotation personnalisée et un test d'écoute @Authenticated d'exécution, qui le gère. Vérifiez d'inspiration DirtiesContextTestExecutionListener.

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

Bien sûr, je suis prêt à voir ces nouvelles fonctionnalités de Spring Security 4.0 qui rendra notre test plus facile.

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 
}
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top