Saltar al contenido

Spring Test & Security: ¿Cómo simular la autenticación?

Este grupo de expertos despúes de algunos días de investigación y recopilar de información, dimos con los datos necesarios, deseamos que todo este artículo sea de utilidad para tu trabajo.

Solución:

Buscando una respuesta, no pude encontrar ninguna que fuera fácil y flexible al mismo tiempo, luego encontré Spring Security Reference y me di cuenta de que hay soluciones casi perfectas. Las soluciones AOP a menudo son las mejores para probar, y Spring las proporciona @WithMockUser, @WithUserDetails y @WithSecurityContext, en este artefacto:


    org.springframework.security
    spring-security-test
    4.2.2.RELEASE
    test

En la mayoría de los casos, @WithUserDetails reúne la flexibilidad y el poder que necesito.

¿Cómo funciona @WithUserDetails?

Básicamente, solo necesita crear un UserDetailsService con todos los perfiles de usuarios posibles que quieras probar. P.ej

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

Ahora tenemos a nuestros usuarios listos, así que imagina que queremos probar el control de acceso a esta función del controlador:

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

Aquí tenemos un obtener la función mapeada a la ruta / foo / salute y estamos probando una seguridad basada en roles con el @Secured anotación, aunque puedes probar @PreAuthorize y @PostAuthorize así como. Creemos dos pruebas, una para verificar si un usuario válido puede ver esta respuesta de saludo y la otra para verificar si en realidad está prohibida.

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

Como ves, importamos SpringSecurityWebAuxTestConfig para proporcionar a nuestros usuarios para la prueba. Cada uno usado en su caso de prueba correspondiente simplemente usando una anotación sencilla, reduciendo el código y la complejidad.

Utilice mejor @WithMockUser para una seguridad basada en roles más sencilla

Como ves @WithUserDetails tiene toda la flexibilidad que necesita para la mayoría de sus aplicaciones. Le permite utilizar usuarios personalizados con cualquier autoridad otorgada, como roles o permisos. Pero si solo está trabajando con roles, las pruebas pueden ser aún más fáciles y podría evitar la construcción de una UserDetailsService. En tales casos, especifique una combinación simple de usuario, contraseña y roles con @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";

La anotación define valores predeterminados para un usuario muy básico. Como en nuestro caso, la ruta que estamos probando solo requiere que el usuario autenticado sea un administrador, podemos dejar de usar SpringSecurityWebAuxTestConfig y haz esto.

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

Observe que ahora en lugar del usuario [email protected] obtenemos el valor predeterminado proporcionado por @WithMockUser: usuario; sin embargo, no importará porque lo que realmente nos importa es su papel: ROLE_MANAGER.

Conclusiones

Como ves con anotaciones como @WithUserDetails y @WithMockUser podemos cambiar entre diferentes escenarios de usuarios autenticados sin construir clases alejadas de nuestra arquitectura solo por hacer pruebas simples. También se recomienda que vea cómo funciona @WithSecurityContext para obtener aún más flexibilidad.

Desde Spring 4.0+, la mejor solución es anotar el método de prueba con @WithMockUser

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

Recuerde agregar la siguiente dependencia a su proyecto

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

Resultó que el SecurityContextPersistenceFilter, que es parte de la cadena de filtros de Spring Security, siempre restablece mi SecurityContext, que puse llamando SecurityContextHolder.getContext().setAuthentication(principal) (o usando el .principal(principal) método). Este filtro establece el SecurityContext en el SecurityContextHolder con un SecurityContext a partir de una SecurityContextRepositorySOBRESCRIBIR el que configuré antes. El repositorio es un HttpSessionSecurityContextRepository por defecto. los HttpSessionSecurityContextRepository inspecciona lo dado HttpRequest e intenta acceder al correspondiente HttpSession. Si existe, intentará leer el SecurityContext desde el HttpSession. Si esto falla, el repositorio genera un vacío SecurityContext.

Por tanto, mi solución es pasar un HttpSession junto con la solicitud, que contiene el 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());
    

Recuerda dar difusión a este enunciado si si solucionó tu problema.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *