170 votes

Spring Test & Security : Comment simuler l'authentification ?

J'essayais de trouver comment tester en unité si les URL de mes contrôleurs sont correctement sécurisés. Juste au cas où quelqu'un modifie les choses et supprime accidentellement les paramètres de sécurité.

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

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

J'ai configuré un WebTestEnvironment de la manière suivante :

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 ça :

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 pris ça ici :

Pourtant, si l'on y regarde de plus près, cela n'est utile que lorsque l'on n'envoie pas de demandes réelles à des URL, mais uniquement lorsque l'on teste des services au niveau des fonctions. 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]
        ...

Les deux messages suivants sont dignes d'intérêt : ils disent essentiellement qu'aucun utilisateur n'a été authentifié, ce qui indique que le réglage de l'option Principal n'a pas fonctionné, ou qu'il a été écrasé.

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

166voto

EliuX Points 4416

En cherchant une réponse, je n'en ai pas trouvé qui soit à la fois facile et flexible, puis j'ai trouvé la Référence sur la sécurité de Spring et j'ai réalisé qu'il y a des solutions presque parfaites. Les solutions de POA sont souvent les meilleures pour les tests, et Spring le fournit avec @WithMockUser , @WithUserDetails y @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 rassemble la flexibilité et la puissance dont j'ai besoin.

Comment fonctionne @WithUserDetails ?

En gros, il suffit de créer un UserDetailsService avec tous les profils d'utilisateurs possibles que vous voulez tester. Par exemple

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

Maintenant que nos utilisateurs sont prêts, imaginons 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 obtenir la fonction cartographiée à l'itinéraire /foo/salute et nous testons une sécurité basée sur les rôles avec la @Secured bien que vous puissiez tester @PreAuthorize y @PostAuthorize également. Créons deux tests, l'un pour vérifier si un utilisateur valide peut voir cette réponse de salut et l'autre pour vérifier si elle est effectivement 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 des tests à nos utilisateurs. Chacun d'eux est utilisé sur le scénario de test correspondant en utilisant simplement une annotation simple, ce qui réduit le code et la complexité.

Mieux vaut utiliser @WithMockUser pour simplifier la sécurité basée sur les rôles.

Comme vous le voyez @WithUserDetails possè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, comme des rôles ou des permissions. Mais si vous ne travaillez qu'avec des rôles, les tests peuvent être encore plus faciles et vous pourriez éviter de construire un fichier UserDetailsService . Dans de tels cas, spécifiez une simple combinaison 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 nécessite simplement que l'utilisateur authentifié soit un manager, nous pouvons arrêter d'utiliser l'annotation SpringSecurityWebAuxTestConfig et faire ça.

@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")));
}

Remarquez que maintenant, au lieu que l'utilisateur manager@company.com nous obtenons la valeur par défaut fournie par @WithMockUser : utilisateur Pourtant, cela n'aura aucune importance, car ce qui nous intéresse vraiment, c'est son rôle : ROLE_MANAGER .

Conclusions

Comme vous le voyez avec des annotations comme @WithUserDetails y @WithMockUser nous pouvons basculer entre différents scénarios d'utilisateurs authentifiés sans construire des classes étrangères à notre architecture juste pour faire des tests simples. Il vous est également recommandé de voir comment @WithSecurityContext fonctionne pour encore plus de flexibilité.

89voto

GummyBear21 Points 441

Depuis Spring 4.0+, la meilleure solution est d'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'

57voto

Martin Becker Points 1291

Il s'est avéré que le SecurityContextPersistenceFilter qui fait partie de la chaîne de filtrage de Spring Security, réinitialise toujours mes SecurityContext que j'ai défini en appelant SecurityContextHolder.getContext().setAuthentication(principal) (ou en utilisant le .principal(principal) méthode). Ce filtre définit le SecurityContext dans le SecurityContextHolder avec un SecurityContext d'un SecurityContextRepository SURPRIMER celui que j'ai défini plus tôt. Le référentiel est un HttpSessionSecurityContextRepository par défaut. Le site HttpSessionSecurityContextRepository inspecte les données HttpRequest et essaie d'accéder à l'adresse correspondante HttpSession . S'il existe, il essaiera de lire le fichier SecurityContext de la HttpSession . Si cela échoue, le référentiel génère un fichier vide SecurityContext .

Ainsi, ma solution consiste à passer un HttpSession avec la demande, 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());
    }
}

32voto

GKislin Points 254

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 à 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-security-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).

Setting Up est modifié : 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 .

7voto

Jay Points 457

Voici un exemple pour ceux qui veulent tester la configuration de sécurité de Spring MockMvc en utilisant 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>

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X