web-dev-qa-db-fra.com

Spring Test & Security: Comment se moquer de l'authentification?

J'essayais de comprendre comment tester un peu si les URL de mes contrôleurs étaient correctement sécurisées. Juste au cas où quelqu'un changerait les choses et supprimerait accidentellement les paramètres de sécurité.

Ma méthode de contrôleur ressemble à ceci:

@RequestMapping("/api/v1/resource/test") 
@Secured("ROLE_USER")
public @ResonseBody String test() {
    return "test";
}

J'ai mis en place un WebTestEnvironment comme ceci:

import javax.annotation.Resource;
import javax.naming.NamingException;
import javax.sql.DataSource;

import org.junit.Before;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.ActiveProfiles;
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;

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({ 
        "file:src/main/webapp/WEB-INF/spring/security.xml",
        "file:src/main/webapp/WEB-INF/spring/applicationContext.xml",
        "file:src/main/webapp/WEB-INF/spring/servlet-context.xml" })
public class WebappTestEnvironment2 {

    @Resource
    private FilterChainProxy springSecurityFilterChain;

    @Autowired
    @Qualifier("databaseUserService")
    protected UserDetailsService userDetailsService;

    @Autowired
    private WebApplicationContext wac;

    @Autowired
    protected DataSource dataSource;

    protected MockMvc mockMvc;

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    protected UsernamePasswordAuthenticationToken getPrincipal(String username) {

        UserDetails user = this.userDetailsService.loadUserByUsername(username);

        UsernamePasswordAuthenticationToken authentication = 
                new UsernamePasswordAuthenticationToken(
                        user, 
                        user.getPassword(), 
                        user.getAuthorities());

        return authentication;
    }

    @Before
    public void setupMockMvc() throws NamingException {

        // setup mock MVC
        this.mockMvc = MockMvcBuilders
                .webAppContextSetup(this.wac)
                .addFilters(this.springSecurityFilterChain)
                .build();
    }
}

Dans mon test actuel, j'ai essayé de faire quelque chose comme ceci:

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Test;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;

import eu.ubicon.webapp.test.WebappTestEnvironment;

public class CopyOfClaimTest extends WebappTestEnvironment {

    @Test
    public void signedIn() throws Exception {

        UsernamePasswordAuthenticationToken principal = 
                this.getPrincipal("test1");

        SecurityContextHolder.getContext().setAuthentication(principal);        

        super.mockMvc
            .perform(
                    get("/api/v1/resource/test")
//                    .principal(principal)
                    .session(session))
            .andExpect(status().isOk());
    }

}

J'ai ramassé ça ici:

Pourtant, si vous y regardez de près, cela n’aidera que si vous n’envoyez pas de demandes réelles à des URL, mais seulement lorsque vous testez des services au niveau fonction. Dans mon cas, une exception "accès refusé" a été levée:

org.springframework.security.access.AccessDeniedException: Access is denied
    at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.Java:83) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.Java:206) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
    at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.Java:60) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.Java:172) ~[spring-aop-3.2.1.RELEASE.jar:3.2.1.RELEASE]
        ...

Il convient de noter que les deux messages suivants du journal indiquent qu'aucun utilisateur n'a été authentifié, indiquant que la définition de la variable Principal ne fonctionnait pas ou qu'elle était écrasée.

14:20:34.454 [main] DEBUG o.s.s.a.i.a.MethodSecurityInterceptor - Secure object: ReflectiveMethodInvocation: public Java.util.List test.TestController.test(); target is of class [test.TestController]; Attributes: [ROLE_USER]
14:20:34.454 [main] DEBUG o.s.s.a.i.a.MethodSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken@9055e4a6: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS
93
Martin Becker

Il s’est avéré que la SecurityContextPersistenceFilter, qui fait partie de la chaîne de filtres Spring Security, réinitialise toujours ma SecurityContext, que je mets en appelant SecurityContextHolder.getContext().setAuthentication(principal) (ou en utilisant la méthode .principal(principal) ). Ce filtre définit le SecurityContext dans le SecurityContextHolder avec un SecurityContext à partir d'un SecurityContextRepositoryÉCRASANT celui défini précédemment. Le référentiel est un HttpSessionSecurityContextRepository par défaut. La HttpSessionSecurityContextRepository inspecte la HttpRequest donnée et tente d'accéder à la HttpSession correspondante. S'il existe, il essaiera de lire le SecurityContext du HttpSession. Si cela échoue, le référentiel génère un SecurityContext vide.

Ainsi, ma solution est de passer un HttpSession avec la requête, qui contient le SecurityContext:

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Test;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;

import eu.ubicon.webapp.test.WebappTestEnvironment;

public class Test extends WebappTestEnvironment {

    public static class MockSecurityContext implements SecurityContext {

        private static final long serialVersionUID = -1386535243513362694L;

        private Authentication authentication;

        public MockSecurityContext(Authentication authentication) {
            this.authentication = authentication;
        }

        @Override
        public Authentication getAuthentication() {
            return this.authentication;
        }

        @Override
        public void setAuthentication(Authentication authentication) {
            this.authentication = authentication;
        }
    }

    @Test
    public void signedIn() throws Exception {

        UsernamePasswordAuthenticationToken principal = 
                this.getPrincipal("test1");

        MockHttpSession session = new MockHttpSession();
        session.setAttribute(
                HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, 
                new MockSecurityContext(principal));


        super.mockMvc
            .perform(
                    get("/api/v1/resource/test")
                    .session(session))
            .andExpect(status().isOk());
    }
}
47
Martin Becker

En cherchant une réponse, je ne trouvais aucune solution facile et flexible à la fois, puis j’ai trouvé le Spring Security Reference et j’ai réalisé qu’il existe des solutions presque parfaites. Les solutions AOP sont souvent les meilleures pour les tests, et Spring les fournit avec @WithMockUser, @WithUserDetails et @WithSecurityContext, dans cet artefact:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <version>4.2.2.RELEASE</version>
    <scope>test</scope>
</dependency>

Dans la plupart des cas, @WithUserDetails réunit la souplesse et le pouvoir dont j'ai besoin.

Comment fonctionne @WithUserDetails?

Fondamentalement, il vous suffit de créer une UserDetailsService personnalisée avec tous les profils d'utilisateurs que vous souhaitez tester. Par exemple

@TestConfiguration
public class SpringSecurityWebAuxTestConfig {

    @Bean
    @Primary
    public UserDetailsService userDetailsService() {
        User basicUser = new UserImpl("Basic User", "[email protected]", "password");
        UserActive basicActiveUser = new UserActive(basicUser, Arrays.asList(
                new SimpleGrantedAuthority("ROLE_USER"),
                new SimpleGrantedAuthority("PERM_FOO_READ")
        ));

        User managerUser = new UserImpl("Manager User", "[email protected]", "password");
        UserActive managerActiveUser = new UserActive(managerUser, Arrays.asList(
                new SimpleGrantedAuthority("ROLE_MANAGER"),
                new SimpleGrantedAuthority("PERM_FOO_READ"),
                new SimpleGrantedAuthority("PERM_FOO_WRITE"),
                new SimpleGrantedAuthority("PERM_FOO_MANAGE")
        ));

        return new InMemoryUserDetailsManager(Arrays.asList(
                basicActiveUser, managerActiveUser
        ));
    }
}

Maintenant que nos utilisateurs sont prêts, imaginons que nous voulons tester le contrôle d’accès à cette fonction du contrôleur:

@RestController
@RequestMapping("/foo")
public class FooController {

    @Secured("ROLE_MANAGER")
    @GetMapping("/salute")
    public String saluteYourManager(@AuthenticationPrincipal User activeUser)
    {
        return String.format("Hi %s. Foo salutes you!", activeUser.getUsername());
    }
}

Ici nous avons une fonction get mappée à la route /foo/salute et nous testons un rôle sécurité basée sur l’annotation @Secured, bien que vous puissiez également tester @PreAuthorize et @PostAuthorize. Créons deux tests, l'un pour vérifier si un utilisateur valide peut voir cette réponse de salut, l'autre pour vérifier si elle est réellement interdite.

@RunWith(SpringRunner.class)
@SpringBootTest(
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        classes = SpringSecurityWebAuxTestConfig.class
)
@AutoConfigureMockMvc
public class WebApplicationSecurityTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    @WithUserDetails("[email protected]")
    public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
    {
        mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
                .accept(MediaType.ALL))
                .andExpect(status().isOk())
                .andExpect(content().string(containsString("[email protected]")));
    }

    @Test
    @WithUserDetails("[email protected]")
    public void givenBasicUser_whenGetFooSalute_thenForbidden() throws Exception
    {
        mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
                .accept(MediaType.ALL))
                .andExpect(status().isForbidden());
    }
}

Comme vous le voyez, nous avons importé SpringSecurityWebAuxTestConfig pour permettre à nos utilisateurs d'effectuer des tests. Chacun utilisait son cas de test correspondant en utilisant simplement une annotation simple, ce qui réduisait le code et la complexité.

Mieux utiliser @WithMockUser pour une sécurité simplifiée basée sur les rôles

Comme vous le voyez, @WithUserDetails dispose de toute la flexibilité dont vous avez besoin pour la plupart de vos applications. Il vous permet d'utiliser des utilisateurs personnalisés avec n'importe quel GrantedAuthority, tels que des rôles ou des autorisations. Mais si vous travaillez uniquement avec des rôles, les tests peuvent être encore plus faciles et vous pouvez éviter de créer un UserDetailsService personnalisé. Dans ce cas, spécifiez une combinaison simple d'utilisateur, de mot de passe et de rôles avec @ WithMockUser .

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@WithSecurityContext(
    factory = WithMockUserSecurityContextFactory.class
)
public @interface WithMockUser {
    String value() default "user";

    String username() default "";

    String[] roles() default {"USER"};

    String password() default "password";
}

L'annotation définit les valeurs par défaut pour un utilisateur très basique. Comme dans notre cas, la route que nous testons exige simplement que l'utilisateur authentifié soit un gestionnaire, nous pouvons quitter avec SpringSecurityWebAuxTestConfig et le faire.

@Test
@WithMockUser(roles = "MANAGER")
public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
{
    mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
            .accept(MediaType.ALL))
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("user")));
}

Notez que maintenant, au lieu de l'utilisateur [email protected] , nous obtenons la valeur par défaut fournie par @WithMockUser: utilisateur ; mais cela n’a aucune importance, car ce qui nous importe vraiment, c’est son rôle: ROLE_MANAGER.

Conclusions

Comme vous le voyez avec des annotations telles que @WithUserDetails et @WithMockUser, nous pouvons alterner entre différents scénarios d’utilisateurs authentifiés sans créer de classes éloignées de notre architecture uniquement pour effectuer des tests simples. Il vous a également recommandé de voir comment @ WithSecurityContext fonctionne pour encore plus de flexibilité.

71
EliuX

Depuis Spring 4.0+, la meilleure solution consiste à annoter la méthode de test avec @WithMockUser.

@Test
@WithMockUser(username = "user1", password = "pwd", roles = "USER")
public void mytest1() throws Exception {
    mockMvc.perform(get("/someApi"))
        .andExpect(status().isOk());
}

N'oubliez pas d'ajouter la dépendance suivante à votre projet

'org.springframework.security:spring-security-test:4.2.3.RELEASE'
35
GummyBear21

Ajouter dans pom.xml:

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <version>4.0.0.RC2</version>
    </dependency>

et utilisez org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors pour la demande d'autorisation. Voir l'exemple d'utilisation à l'adresse https://github.com/rwinch/spring-security-test-blog ( https://jira.spring.io/browse/SEC-2592 ).

Mise à jour:

4.0.0.RC2 fonctionne pour Spring-security 3.x. Pour Spring-security, 4 spring-test fait partie de Spring-security ( http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test , la version est la même).

La configuration est modifiée: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-mockmvc

public void setup() {
    mvc = MockMvcBuilders
            .webAppContextSetup(context)
            .apply(springSecurity())  
            .build();
}

Exemple pour l'authentification de base: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#testing-http-basic-authentication .

31
GKislin

Voici un exemple pour ceux qui souhaitent tester Spring MockMvc Security Config à l'aide de l'authentification de base Base64.

String basicDigestHeaderValue = "Basic " + new String(Base64.encodeBase64(("<username>:<password>").getBytes()));
this.mockMvc.perform(get("</get/url>").header("Authorization", basicDigestHeaderValue).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());

Dépendance Maven

    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
        <version>1.3</version>
    </dependency>
7
Jay

Réponse courte:

@Autowired
private WebApplicationContext webApplicationContext;

@Autowired
private Filter springSecurityFilterChain;

@Before
public void setUp() throws Exception {
    final MockHttpServletRequestBuilder defaultRequestBuilder = get("/dummy-path");
    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext)
            .defaultRequest(defaultRequestBuilder)
            .alwaysDo(result -> setSessionBackOnRequestBuilder(defaultRequestBuilder, result.getRequest()))
            .apply(springSecurity(springSecurityFilterChain))
            .build();
}

private MockHttpServletRequest setSessionBackOnRequestBuilder(final MockHttpServletRequestBuilder requestBuilder,
                                                             final MockHttpServletRequest request) {
    requestBuilder.session((MockHttpSession) request.getSession());
    return request;
}

Après avoir effectué formLogin à partir du test de sécurité du printemps, chacune de vos demandes sera automatiquement appelée en tant qu’utilisateur connecté.

Réponse longue:

Vérifiez cette solution (la réponse concerne le printemps 4): Comment connecter un utilisateur avec un nouveau test mvc du printemps 3.2

3
Nagy Attila

Options pour éviter d'utiliser SecurityContextHolder dans les tests:

  • Option 1: utilisez des simulacres - je veux dire simulacre SecurityContextHolder en utilisant une bibliothèque simulacre - EasyMock par exemple
  • Option 2: appel wrap SecurityContextHolder.get... dans votre code dans un service - par exemple, dans SecurityServiceImpl avec la méthode getCurrentPrincipal qui implémente l'interface SecurityService, puis dans vos tests, vous pouvez simplement créer une implémentation fictive de cette interface qui renvoie le principal souhaité sans accéder à SecurityContextHolder.
2
Pavla Nováková