Pruebas unitarias con Spring Security

9 minutos de lectura

avatar de usuario
mate b

Mi empresa 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 mismo estoy echando un vistazo al módulo Spring Security para determinar si es algo que podemos/debemos usar.

Nuestros requisitos de seguridad son bastante básicos; un usuario solo necesita poder proporcionar un nombre de usuario y una 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 se debe dar acceso a un usuario anónimo.

En el prototipo que he estado creando, he estado almacenando un objeto “LoginCredentials” (que solo contiene nombre de usuario y contraseña) en Sesión 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 en su lugar, lo que tendría el gran beneficio de eliminar cualquier tipo de “¿cómo hacemos un seguimiento de los usuarios registrados?” y “¿cómo autenticamos a los usuarios?” desde mi controlador/código comercial.

Parece que Spring Security proporciona un objeto de “contexto” (por subproceso) 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 la siguiente: 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 inyectar un objeto de autenticación en SecurityContext para que esté disponible para mis pruebas unitarias cuando las pruebas unitarias requieren un usuario autenticado?

¿Necesito 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 demasiado detallado. hay una manera mas facil?

los SecurityContextHolder el objeto en sí parece muy poco parecido a un resorte…

avatar de usuario
leonardo eloy

Simplemente hágalo de la manera habitual y luego insértelo usando SecurityContextHolder.setContext() en su 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);

  • @Leonardo ¿dónde debería estar esto? Authentication a ser agregado en el controlador? Como puedo entender en cada método de invocación? ¿Está bien que “forma de primavera” solo lo agregue, en lugar de inyectarlo?

    – Oleg Kuts

    25/04/2016 a las 20:14

  • Pero recuerde que no va a funcionar con TestNG porque SecurityContextHolder tiene una variable de subproceso local para que luego comparta esta variable entre las pruebas…

    – Łukasz Woźniczka

    14 de marzo de 2017 a las 9:46

  • hazlo en @BeforeEach(JUnit5) o @Before(Junta 4). Bueno y sencillo.

    – WesternGun

    26 de noviembre de 2019 a las 11:37

  • Exactamente lo que necesitaba. ¡Gracias!

    – jksevend

    1 de julio de 2021 a las 17:17

avatar de usuario
Matsev

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

@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 regresado de la UserDetailsServicep.ej

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

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

  • Ese es el enfoque más directo hoy en día y funciona perfectamente.

    – sonido de los miembros

    24 de agosto de 2021 a las 7:45

El problema es que Spring Security no hace que el objeto de Autenticación esté disponible como un bean en el contenedor, por lo que no hay forma de inyectarlo o autoconectarlo fácilmente fuera de la caja.

Antes de que comenzáramos a usar Spring Security, crearíamos un bean con ámbito de sesión en el contenedor para almacenar el Principal, lo inyectaríamos en un “AuthenticationService” (singleton) y luego inyectaríamos 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”, inyectar esto en su servicio de autenticación, hacer que el servicio de autenticación establezca la propiedad en 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 por usar SecurityContextHolder. aunque. Sé que es un Singleton estático y que Spring desaconseja el uso de tales cosas, pero su implementación se encarga de comportarse adecuadamente según el entorno: con ámbito de sesión en un contenedor Servlet, con ámbito de subprocesos en una prueba JUnit, etc. El factor limitante real de un Singleton es cuando proporciona una implementación que es inflexible a diferentes entornos.

  • Gracias, este es un consejo útil. Lo que he hecho hasta ahora es básicamente proceder a llamar a SecurityContextHolder.getContext() (a través de algunos métodos de envoltorio propios, por lo que al menos solo se llama desde una clase).

    – mate b

    16 de diciembre de 2008 a las 19:56

  • Aunque solo una nota, no creo que ServletContextHolder tenga ningún concepto de HttpSession o una forma de saber si está operando en un entorno de servidor web, usa ThreadLocal a menos que lo configure para usar otra cosa (los únicos otros dos modos integrados son InheritableThreadLocal y globales)

    – mate b

    16 de diciembre de 2008 a las 19:57

  • El único inconveniente de usar beans con ámbito de sesión/solicitud en Spring es que fallarán en una prueba JUnit. Lo que puede hacer es implementar un alcance personalizado que usará la sesión/solicitud si está disponible y si es necesario volver al hilo. Supongo que Spring Security está haciendo algo similar…

    – cliff.meyers

    16 de diciembre de 2008 a las 21:30

  • Mi objetivo es construir una API Rest sin sesiones. Quizás con un token actualizable. Si bien esto no respondió a mi pregunta, ayudó. Gracias

    – Pomagranito

    16 de agosto de 2017 a las 14:12

Tiene razón en preocuparse: 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 mostrarte es cómo dejar que el contenedor Spring IoC haga el trabajo sucio por ti, dejándote con un código limpio y comprobable. SecurityContextHolder es una clase de marco y, si bien 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: cree su propio tipo “principal” e inyecte una instancia en los consumidores. La Primavera <aop: proxy de alcance/> etiqueta introducida en 2.x combinada con una definición de bean de alcance de solicitud, y el soporte del método de fábrica puede ser el boleto 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 hayas tenido que hacer la mayor parte de esto. A continuación, en el contexto de su 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 ingrese una nueva solicitud HTTP y cualquier referencia a la propiedad currentUser 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!

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 ejemplo

@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 un poco de código repetitivo aquí, es decir, simular un objeto de autenticación, simular un SecurityContext para devolver la autenticación y finalmente simular el SecurityContextHolder para obtener el SecurityContext, sin embargo, es muy flexible y le permite realizar pruebas unitarias para escenarios como objetos de autenticación nulos. etc. sin tener que cambiar su código (no de prueba)

  • Sé que esto es antiguo, pero no tiene sentido usar PowerMock cuando solo puedes llamar SecurityContextHolder.setContext() o .setStrategyName(className) para lograr lo mismo.

    – Didier L.

    20 de enero a las 17:52

avatar de usuario
miguel bushe

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

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

  • Sé que esto es antiguo, pero no tiene sentido usar PowerMock cuando solo puedes llamar SecurityContextHolder.setContext() o .setStrategyName(className) para lograr lo mismo.

    – Didier L.

    20 de enero a las 17:52

avatar de usuario
Comunidad

Hice la misma pregunta aquí y acabo de publicar una respuesta que encontré recientemente. La respuesta corta es: inyectar un SecurityContexty hacer referencia a SecurityContextHolder solo en su configuración de Spring para obtener el SecurityContext

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad