starter java security unit-testing spring spring-security

java - spring boot starter security



Pruebas unitarias con Spring Security (11)

General

Mientras tanto (desde la versión 3.2, en el año 2013, gracias a SEC-2298 ) la autenticación puede ser inyectada en métodos MVC utilizando la anotación @AuthenticationPrincipal :

@Controller class Controller { @RequestMapping("/somewhere") public void doStuff(@AuthenticationPrincipal UserDetails myUser) { } }

Pruebas

En su prueba de unidad, obviamente puede llamar a este Método directamente. En las pruebas de integración que usan org.springframework.test.web.servlet.MockMvc , puede usar org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user() para inyectar al usuario de esta manera:

mockMvc.perform(get("/somewhere").with(user(myUserDetails)));

Sin embargo, esto solo llenará directamente el SecurityContext. Si desea asegurarse de que el usuario se carga desde una sesión en su prueba, puede usar esto:

mockMvc.perform(get("/somewhere").with(sessionUser(myUserDetails))); /* ... */ private static RequestPostProcessor sessionUser(final UserDetails userDetails) { return new RequestPostProcessor() { @Override public MockHttpServletRequest postProcessRequest(final MockHttpServletRequest request) { final SecurityContext securityContext = new SecurityContextImpl(); securityContext.setAuthentication( new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()) ); request.getSession().setAttribute( HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, securityContext ); return request; } }; }

Mi compañía ha estado evaluando Spring MVC para determinar si deberíamos usarlo en uno de nuestros próximos proyectos. Hasta ahora, me encanta lo que he visto, y ahora estoy mirando el módulo Spring Security para determinar si es algo que podemos / deberíamos usar.

Nuestros requisitos de seguridad son bastante básicos; un usuario solo necesita poder proporcionar un nombre de usuario y contraseña para poder acceder a ciertas partes del sitio (como para obtener información sobre su cuenta); y hay un puñado de páginas en el sitio (Preguntas frecuentes, Soporte, etc.) donde un usuario anónimo debe tener acceso.

En el prototipo que he estado creando, he estado almacenando un objeto "LoginCredentials" (que solo contiene nombre de usuario y contraseña) en Session para un usuario autenticado; algunos de los controladores verifican si este objeto está en sesión para obtener una referencia al nombre de usuario registrado, por ejemplo. Estoy buscando reemplazar esta lógica local con Spring Security, lo que tendría el beneficio de eliminar cualquier tipo de "¿cómo rastreamos a los usuarios que han iniciado sesión?" y "¿cómo autenticamos a los usuarios?" desde mi controlador / código comercial.

Parece que Spring Security proporciona un objeto de "contexto" (por hilo) para poder acceder al nombre de usuario / información principal desde cualquier lugar de su aplicación ...

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

... que parece muy poco primaveral, ya que este objeto es un singleton (global), en cierto modo.

Mi pregunta es esta: si esta es la forma estándar de acceder a la información sobre el usuario autenticado en Spring Security, ¿cuál es la forma aceptada de insertar un objeto de autenticación en el SecurityContext para que esté disponible para mis pruebas unitarias cuando las pruebas de la unidad requieren una usuario autenticado

¿Debo conectar esto en el método de inicialización de cada caso de prueba?

protected void setUp() throws Exception { ... SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken(testUser.getLogin(), testUser.getPassword())); ... }

Esto parece excesivamente detallado. hay una manera mas facil?

El objeto SecurityContextHolder parece muy poco parecido a Spring ...


Después de mucho trabajo, pude reproducir el comportamiento deseado. Había emulado el inicio de sesión a través de MockMvc. Es demasiado pesado para la mayoría de las pruebas unitarias pero útil para pruebas de integración.

Por supuesto, estoy dispuesto a ver esas nuevas características en Spring Security 4.0 que facilitarán nuestras pruebas.

package [myPackage] import static org.junit.Assert.*; import javax.inject.Inject; import javax.servlet.http.HttpSession; import org.junit.Before; import org.junit.Test; import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; 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; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; @ContextConfiguration(locations={[my config file locations]}) @WebAppConfiguration @RunWith(SpringJUnit4ClassRunner.class) public static class getUserConfigurationTester{ private MockMvc mockMvc; @Autowired private FilterChainProxy springSecurityFilterChain; @Autowired private MockHttpServletRequest request; @Autowired private WebApplicationContext webappContext; @Before public void init() { mockMvc = MockMvcBuilders.webAppContextSetup(webappContext) .addFilters(springSecurityFilterChain) .build(); } @Test public void testTwoReads() throws Exception{ HttpSession session = mockMvc.perform(post("/j_spring_security_check") .param("j_username", "admin_001") .param("j_password", "secret007")) .andDo(print()) .andExpect(status().isMovedTemporarily()) .andExpect(redirectedUrl("/index")) .andReturn() .getRequest() .getSession(); request.setSession(session); SecurityContext securityContext = (SecurityContext) session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); SecurityContextHolder.setContext(securityContext); // Your test goes here. User is logged with }


Echaré un vistazo a las clases de pruebas abstractas de Spring y a los objetos de simulación de los que se habla here . Proporcionan una forma poderosa de auto-cablear sus objetos gestionados de Spring haciendo que la unidad y las pruebas de integración sean más fáciles.


El problema es que Spring Security no hace que el objeto Autenticación esté disponible como un frijol en el contenedor, por lo que no hay forma de inyectarlo fácilmente o de conectarlo de forma automática.

Antes de comenzar a usar Spring Security, creábamos un bean con ámbito de sesión en el contenedor para almacenar el Principal, inyectarlo en un "AuthenticationService" (singleton) y luego inyectar este bean en otros servicios que necesitaban conocimiento del Principal actual.

Si está implementando su propio servicio de autenticación, básicamente podría hacer lo mismo: crear un bean con ámbito de sesión con una propiedad "principal", insertar esto en su servicio de autenticación, hacer que el servicio de autenticación establezca la propiedad en la autenticación exitosa y luego haga que el servicio de autenticación esté disponible para otros beans cuando lo necesite.

No me sentiría tan mal con el uso de SecurityContextHolder. aunque. Sé que es un Singleton estático y que Spring recomienda no usar tales cosas, pero su implementación se comporta de forma adecuada según el entorno: sesión en un contenedor Servlet, ámbito de subprocesos en una prueba JUnit, etc. El factor limitante real de Singleton es cuando proporciona una implementación que es inflexible a diferentes entornos.


Hice la misma pregunta por here , y acabo de publicar una respuesta que encontré recientemente. La respuesta breve es: inyecte un SecurityContext , y refiérase a SecurityContextHolder solo en su configuración de Spring para obtener el SecurityContext


La autenticación es una propiedad de un hilo en el entorno del servidor de la misma manera que es una propiedad de un proceso en el sistema operativo. Tener una instancia de bean para acceder a la información de autenticación sería inconveniente para la configuración y la sobrecarga del cableado sin ningún beneficio.

En cuanto a la autenticación de prueba, hay varias formas de facilitarle la vida. Mi favorito es crear una anotación personalizada @Authenticated y probar la escucha de ejecución, que la administra. Consulte DirtiesContextTestExecutionListener para obtener inspiración.


Personalmente, solo usaría Powermock junto con Mockito o Easymock para simular el SecurityContextHolder.getSecurityContext () estático en su unidad / prueba de integración, por ej.

@RunWith(PowerMockRunner.class) @PrepareForTest(SecurityContextHolder.class) public class YourTestCase { @Mock SecurityContext mockSecurityContext; @Test public void testMethodThatCallsStaticMethod() { // Set mock behaviour/expectations on the mockSecurityContext when(mockSecurityContext.getAuthentication()).thenReturn(...) ... // Tell mockito to use Powermock to mock the SecurityContextHolder PowerMockito.mockStatic(SecurityContextHolder.class); // use Mockito to set up your expectation on SecurityContextHolder.getSecurityContext() Mockito.when(SecurityContextHolder.getSecurityContext()).thenReturn(mockSecurityContext); ... } }

Es cierto que hay bastante código de placa de caldera aquí, es decir, simula un objeto de autenticación, se burla de un SecurityContext para devolver la autenticación y finalmente se burla de SecurityContextHolder para obtener el SecurityContext, sin embargo es muy flexible y permite probar escenarios como objetos de autenticación null. etc. sin tener que cambiar su código (sin prueba)


Simplemente hazlo de la manera habitual y luego insértala usando SecurityContextHolder.setContext() en tu clase de prueba, por ejemplo:

Controlador:

Authentication a = SecurityContextHolder.getContext().getAuthentication();

Prueba:

Authentication authentication = Mockito.mock(Authentication.class); // Mockito.whens() for your authorization object SecurityContext securityContext = Mockito.mock(SecurityContext.class); Mockito.when(securityContext.getAuthentication()).thenReturn(authentication); SecurityContextHolder.setContext(securityContext);


Sin responder la pregunta sobre cómo crear e inyectar objetos de autenticación, Spring Security 4.0 ofrece algunas alternativas de bienvenida cuando se trata de pruebas. La anotación @WithMockUser permite al desarrollador especificar un usuario simulado (con autoridades opcionales, nombre de usuario, contraseña y roles) de una forma clara:

@Test @WithMockUser(username = "admin", authorities = { "ADMIN", "USER" }) public void getMessageWithMockUserCustomAuthorities() { String message = messageService.getMessage(); ... }

También existe la opción de usar @WithUserDetails para emular un UserDetails devuelto por el UserDetailsService , por ejemplo

@Test @WithUserDetails("customUsername") public void getMessageWithUserDetailsCustomUsername() { String message = messageService.getMessage(); ... }

Se pueden encontrar más detalles en el @WithMockUser y los capítulos @WithUserDetails en los documentos de referencia de Spring Security (de los cuales se copiaron los ejemplos anteriores)


Tiene razón en estar preocupado: las llamadas a métodos estáticos son particularmente problemáticas para las pruebas unitarias, ya que no puede burlarse fácilmente de sus dependencias. Lo que voy a mostrarle es cómo dejar que el contenedor Spring IoC haga el trabajo sucio por usted, dejándole un código claro y comprobable. SecurityContextHolder es una clase de infraestructura y, aunque puede estar bien que su código de seguridad de bajo nivel esté vinculado a él, probablemente desee exponer una interfaz más ordenada a sus componentes de interfaz de usuario (es decir, controladores).

cliff.meyers mencionó una forma de evitarlo: crea tu propio tipo de "principal" e inyecta una instancia en los consumidores. La etiqueta Spring < aop:scoped-proxy /> introducida en 2.x combinada con una definición de bean del ámbito de solicitud, y la compatibilidad con el método de fábrica puede ser el ticket para el código más legible.

Podría funcionar de la siguiente manera:

public class MyUserDetails implements UserDetails { // this is your custom UserDetails implementation to serve as a principal // implement the Spring methods and add your own methods as appropriate } public class MyUserHolder { public static MyUserDetails getUserDetails() { Authentication a = SecurityContextHolder.getContext().getAuthentication(); if (a == null) { return null; } else { return (MyUserDetails) a.getPrincipal(); } } } public class MyUserAwareController { MyUserDetails currentUser; public void setCurrentUser(MyUserDetails currentUser) { this.currentUser = currentUser; } // controller code }

Nada complicado hasta ahora, ¿verdad? De hecho, probablemente ya tengas que hacer la mayor parte de esto. A continuación, en su contexto de bean defina un bean con ámbito de solicitud para contener el principal:

<bean id="userDetails" class="MyUserHolder" factory-method="getUserDetails" scope="request"> <aop:scoped-proxy/> </bean> <bean id="controller" class="MyUserAwareController"> <property name="currentUser" ref="userDetails"/> <!-- other props --> </bean>

Gracias a la magia de la etiqueta aop: scoped-proxy, se llamará al método estático getUserDetails cada vez que entre una nueva solicitud HTTP y cualquier referencia a la propiedad actual del usuario se resolverá correctamente. Ahora las pruebas unitarias se vuelven triviales:

protected void setUp() { // existing init code MyUserDetails user = new MyUserDetails(); // set up user as you wish controller.setCurrentUser(user); }

¡Espero que esto ayude!


Usar una estática en este caso es la mejor manera de escribir código de seguridad.

Sí, la estática generalmente es mala, en general, pero en este caso, lo estático es lo que quieres. Dado que el contexto de seguridad asocia un Principal con el hilo que se está ejecutando actualmente, el código más seguro accedería a la estática del hilo lo más directamente posible. Ocultar el acceso detrás de una clase de contenedor que se inyecta proporciona a un atacante más puntos para atacar. No necesitarían acceder al código (lo cual les sería difícil cambiar si el contenedor estuviera firmado), solo necesitan una forma de anular la configuración, lo que se puede hacer en tiempo de ejecución o deslizando algunos XML en el classpath. Incluso el uso de la inyección de anotación sería reemplazable con XML externo. Tal XML podría inyectar el sistema en ejecución con un principal deshonesto.