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 SecurityContextRepository
SOBRESCRIBIR 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.