with tutorial example español ejemplos ejemplo authorizerequests spring security model-view-controller testing junit

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í:

  • aquí:
  • o 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 en SecurityServiceImpl con el método getCurrentPrincipal que implementa SecurityService interfaz SecurityService y luego en sus pruebas puede simplemente crear una implementación simulada de esta interfaz que devuelve el principal deseado sin acceso a SecurityContextHolder .

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/…