Spring Test & Security: comment simuler l'authentification?

j'essayais de trouver comment tester l'unité si les URL de mes contrôleurs sont 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 environnement Webcomme celui-ci:

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 choisi ceci ici:

  • http://java.dzone.com/articles/spring-test-mvc-junit-testing ici:
  • http://techdive.in/solutions/how-mock-securitycontextholder-perfrom-junit-tests-spring-controller ou ici:
  • comment JUnit testes a @Preauthorizate annotation et son printemps EL spécifié par un contrôleur MVC à ressort?

pourtant, si l'on regarde de plus près, cela n'aide que lorsque l'on n'envoie pas de requêtes réelles à des URLs, mais seulement lorsque l'on teste des services au niveau de la fonction. Dans mon cas, une exception "Accès refusé" a été rejeté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]
        ...

les deux messages de journalisation suivants sont dignes de mention et indiquent essentiellement qu'Aucun utilisateur n'a été authentifié, ce qui indique que le paramétrage du Principal n'a pas fonctionné ou qu'il n'a pas fonctionné. écraser.

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
67
demandé sur Community 2013-03-04 18:11:35

7 réponses

il s'est avéré que le SecurityContextPersistenceFilter , qui fait partie de la chaîne de filtrage de sécurité du ressort, réinitialise toujours mon SecurityContext , que j'ai réglé en appelant SecurityContextHolder.getContext().setAuthentication(principal) (ou en utilisant la méthode .principal(principal) ). Ce filtre place le SecurityContext dans le SecurityContextHolder avec un SecurityContext d'un SecurityContextRepository OVERWRITING celui que j'ai réglé plus tôt. Le dépôt est un HttpSessionSecurityContextRepository par défaut. Le HttpSessionSecurityContextRepository inspecte le HttpRequest donné et tente d'accéder à la correspondant HttpSession . S'il existe, il essaiera de lire le SecurityContext du HttpSession . En cas d'échec, le dépôt génère un SecurityContext vide .

ainsi, ma solution est de passer un HttpSession avec la demande, qui tient 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());
    }
}
44
répondu Martin Becker 2013-07-09 22:58:05

Seaching pour la réponse, je ne pouvais pas trouver tout pour être facile et souple à la fois, puis j'ai trouvé le Printemps de Référence sur la Sécurité et j'ai réalisé il y a près de à des solutions parfaites. AOP solutions sont souvent les plus grands pour l'essai, et le ressort lui fournit avec @WithMockUser , @WithUserDetails et @WithSecurityContext , dans cet artéfact:

<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 rassemble la flexibilité et la puissance dont j'ai besoin.

comment @WithUserDetails fonctionne?

Fondamentalement, vous avez juste besoin de créer un personnalisé UserDetailsService à tous les profils d'utilisateurs que vous souhaitez tester. E. g

@TestConfiguration
public class SpringSecurityWebAuxTestConfig {

    @Bean
    @Primary
    public UserDetailsService userDetailsService() {
        User basicUser = new UserImpl("Basic User", "user@company.com", "password");
        UserActive basicActiveUser = new UserActive(basicUser, Arrays.asList(
                new SimpleGrantedAuthority("ROLE_USER"),
                new SimpleGrantedAuthority("PERM_FOO_READ")
        ));

        User managerUser = new UserImpl("Manager User", "manager@company.com", "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
        ));
    }
}

nous avons maintenant nos utilisateurs prêts, alors imaginez que nous voulons tester le contrôle d'accès à cette fonction de 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 un fonction obtenir mappé à la route / foo / Salut et nous testons une sécurité basée sur les rôles avec l'annotation @Secured , bien que vous puissiez aussi tester @PreAuthorize et @PostAuthorize . Créons deux tests, l'un pour vérifier si un utilisateur valide peut voir cette réponse salute et 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("manager@company.com")
    public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
    {
        mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
                .accept(MediaType.ALL))
                .andExpect(status().isOk())
                .andExpect(content().string(containsString("manager@company.com")));
    }

    @Test
    @WithUserDetails("user@company.com")
    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 fournir à nos utilisateurs pour les tests. Chacun utilisé sur son cas d'essai correspondant simplement en utilisant une annotation simple, en réduisant le code et complexité.

meilleure utilisation @WithMockUser pour une sécurité basée sur le rôle plus simple

comme vous le voyez @WithUserDetails a 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 quelle autorité accordée, comme des rôles ou des permissions. Mais si vous travaillez simplement avec les rôles, les tests peuvent être encore plus faciles et vous pourriez éviter de construire un UserDetailsService personnalisé . Dans de tels cas, précisez une combinaison simple d'utilisateur, mot de passe et 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 simple. Comme dans notre cas, la route que nous testons exige simplement que l'utilisateur authentifié soit un gestionnaire, nous pouvons arrêter d'utiliser 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 à la place de l'utilisateur manager@company.com nous obtenons la valeur par défaut fournie par @WithMockUser : utilisateur ; mais cela n'aura pas d'importance parce que ce qui nous intéresse vraiment est son rôle: ROLE_MANAGER .

Conclusions

comme vous le voyez avec des annotations comme @WithUserDetails et @WithMockUser nous pouvons passer entre différents scénarios d'utilisateurs authentifiés sans construire des classes aliénées de notre architecture juste pour faire des tests simples. Il vous a également recommandé de voir comment @WithSecurityContext fonctionne pour encore plus de flexibilité.

32
répondu EliuX 2017-06-01 17:10:21

ajouter dans pom.xml:

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

et utiliser org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors pour la demande d'autorisation. Voir l'exemple d'utilisation à 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 le printemps-sécurité 4 printemps-sécurité-test de devenir une partie de ressort de sécurité ( http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test , version identique).

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();
}

échantillon pour authentification basique: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#testing-http-basic-authentication .

31
répondu GKislin 2016-07-26 14:11:40

depuis le printemps 4.0+, la meilleure solution est d'annoter la méthode d'essai 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'
13
répondu GummyBear21 2017-11-30 13:51:27

voici un exemple pour ceux qui veulent tester la configuration de sécurité Spring MockMvc en utilisant L'authentification Base64 basic.

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());

Maven Dependency

    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
        <version>1.3</version>
    </dependency>
6
répondu Jay 2014-02-25 18:33:14

brève réponse:

@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" perform formLogin de spring security test chacune de vos requêtes sera automatiquement appelée comme utilisateur connecté.

longue réponse:

Vérifiez cette solution (la réponse est pour le printemps 4): comment se connecter à un utilisateur avec le printemps 3.2 nouveau test mvc

3
répondu Nagy Attila 2017-11-02 07:49:31

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

  • Option 1 : mocker - je veux dire, se moquer de SecurityContextHolder à l'aide de certains maquette de la bibliothèque - EasyMock pour l'exemple
  • Option 2 : wrap call SecurityContextHolder.get... dans votre code dans un service - par exemple dans SecurityServiceImpl avec la méthode getCurrentPrincipal qui implémente l'interface SecurityService et ensuite dans vos tests vous pouvez simplement créer une implémentation simulée de cette interface qui renvoie le principal désiré sans accès à SecurityContextHolder .
2
répondu Pavla Nováková 2013-03-04 17:38:25