tutorial - Spring Test & Security: ¿Cómo simular la autenticación?
spring security tutorial (7)
Estaba tratando de averiguar cómo probar la unidad si mis URL de mis controladores están aseguradas adecuadamente. En caso de que alguien cambie las cosas y elimine accidentalmente la configuración de seguridad.
Mi método de controlador se ve así:
@RequestMapping("/api/v1/resource/test")
@Secured("ROLE_USER")
public @ResonseBody String test() {
return "test";
}
Configuré un WebTestEnvironment como tal:
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();
}
}
En mi prueba real intenté hacer algo como esto:
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());
}
}
Recogí esto aquí:
Sin embargo, si uno mira de cerca esto solo ayuda cuando no se envían solicitudes reales a las URL, pero solo cuando se prueban servicios en un nivel de función. En mi caso, se lanzó una excepción de "acceso denegado":
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]
...
Los dos mensajes de registro siguientes son dignos de mención y básicamente dicen que ningún usuario fue autenticado, lo que indica que la configuración del Principal
no funcionó o que se sobrescribió.
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
Agregar en pom.xml:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>4.0.0.RC2</version>
</dependency>
y use org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
para la solicitud de autorización. Consulte el uso de la muestra en https://github.com/rwinch/spring-security-test-blog ( https://jira.spring.io/browse/SEC-2592 ).
Actualizar:
4.0.0.RC2 funciona para seguridad de primavera 3.x. Para spring-security 4 spring-security-test se convierte en parte de spring-security ( http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test , la versión es la misma )
La configuración ha cambiado: 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();
}
Ejemplo de autenticación básica: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#testing-http-basic-authentication .
Aquí hay un ejemplo para aquellos que quieren probar Spring MockMvc Security Config usando la autenticación básica 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());
Dependencia de Maven
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.3</version>
</dependency>
Buscando respuestas no pude encontrar ninguna que fuera fácil y flexible al mismo tiempo, luego encontré http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-mockmvc y me di cuenta de que hay soluciones casi perfectas. Las soluciones AOP suelen ser las mejores para probar, y Spring las proporciona con @WithMockUser
, @WithUserDetails
y @WithSecurityContext
, en este artefacto:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>4.2.2.RELEASE</version>
<scope>test</scope>
</dependency>
En la mayoría de los casos, @WithUserDetails
reúne la flexibilidad y la potencia que necesito.
¿Cómo funciona @WithUserDetails?
Básicamente, solo necesita crear un UserDetailsService
personalizado con todos los perfiles de usuarios posibles que desea 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 listos a nuestros usuarios, 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 una función de asignar a la ruta / foo / saludo y estamos probando una seguridad basada en roles con la anotación @Secured
, aunque también puede probar @PreAuthorize
y @PostAuthorize
. Vamos a crear dos pruebas, una para verificar si un usuario válido puede ver esta respuesta de saludo y la otra para verificar si está realmente 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 puede ver, SpringSecurityWebAuxTestConfig
para proporcionar SpringSecurityWebAuxTestConfig
a nuestros usuarios. Cada uno utilizó en su caso de prueba correspondiente simplemente usando una anotación directa, reduciendo el código y la complejidad.
Mejor uso @WithMockUser para una seguridad basada en roles más simple
Como puede ver, @WithUserDetails
tiene toda la flexibilidad que necesita para la mayoría de sus aplicaciones. Le permite usar usuarios personalizados con cualquier GrantedAuthority, como roles o permisos. Pero si solo está trabajando con roles, las pruebas pueden ser incluso más fáciles y podría evitar construir un servicio de UserDetailsService
personalizado. 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 los 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 hacer 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")));
}
Tenga en cuenta que ahora en lugar del usuario [email protected] estamos obteniendo el valor predeterminado proporcionado por @WithMockUser
: usuario ; sin embargo, no importará porque lo que realmente nos importa es su rol: ROLE_MANAGER
.
Conclusiones
Como puede ver con anotaciones como @WithUserDetails
y @WithMockUser
, podemos cambiar entre distintos escenarios de usuarios autenticados sin crear clases alienadas de nuestra arquitectura solo para hacer pruebas simples. También se recomienda que vea cómo funciona @WithSecurityContext para una mayor 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''
Opciones para evitar el uso de SecurityContextHolder en las pruebas:
- Opción 1 : usar simulaciones - Me refiero a simulacro de
SecurityContextHolder
usando alguna biblioteca simulada - EasyMock por ejemplo - Opción 2 : envuelva la llamada
SecurityContextHolder.get...
en su código en algún servicio, por ejemplo enSecurityServiceImpl
con el métodogetCurrentPrincipal
que implementaSecurityService
interfazSecurityService
y luego en sus pruebas puede simplemente crear una implementación simulada de esta interfaz que devuelve el principal deseado sin acceso aSecurityContextHolder
.
Resultó que SecurityContextPersistenceFilter
, que es parte de la cadena de filtros Spring Security, siempre restablece mi SecurityContext
, que configuro llamando a SecurityContextHolder.getContext().setAuthentication(principal)
(o usando el .principal(principal)
). Este filtro establece SecurityContext
en SecurityContextHolder
con SecurityContext
desde SecurityContextRepository
OVERWRITING que establecí anteriormente. El repositorio es un HttpSessionSecurityContextRepository
de forma predeterminada. El HttpSessionSecurityContextRepository
inspecciona la HttpRequest
dada e intenta acceder a la HttpSession
correspondiente. Si existe, intentará leer el SecurityContext
de HttpSession
. Si esto falla, el repositorio genera un SecurityContext
vacío.
Por lo tanto, mi solución es pasar una 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());
}
}
Respuesta corta:
@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;
}
Después de realizar formLogin
partir de la prueba de seguridad de primavera, cada una de sus solicitudes se llamará automáticamente como usuario conectado.
Respuesta larga:
Verifique esta solución (la respuesta es para la primavera 4): .com/questions/14308341/…