Pregunta

Mi empresa ha estado evaluando Spring MVC para determinar si debemos 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 o debemos usar.

Nuestros requisitos de seguridad son bastante básicos;un usuario sólo 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.) a las que 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 que inició sesión, por ejemplo.Estoy buscando reemplazar esta lógica local con Spring Security, lo que tendría el buen beneficio de eliminar cualquier tipo de "¿Cómo seguimos el seguimiento de los usuarios?" y "¿Cómo autenticamos a los usuarios?" de mi controlador/código de negocio.

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();

...lo cual 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 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 requieran 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?

El SecurityContextHolder El objeto en sí parece muy poco primaveral...

¿Fue útil?

Solución

El problema es que la primavera de Seguridad no hace el objeto de autenticación disponible como un grano en el contenedor, lo que no hay manera de inyectar fácilmente o Autowire fuera de la caja.

Antes de empezar a utilizar la primavera de Seguridad, crearíamos un bean de sesión con ámbito en el recipiente para almacenar el director, inyectar este en un "AuthenticationService" (Singleton) y luego inyectar este frijol en otros servicios que necesitan los conocimientos de la Principal actual.

Si va a implementar su propio servicio de autenticación, se puede hacer básicamente lo mismo: crear un bean de sesión con ámbito con una propiedad "principal", se inyecta esto en su servicio de autenticación, tener el servicio de autenticación establece la propiedad de autenticación exitosa , y luego hacer que el servicio de autenticación a disposición de otros granos como usted lo necesite.

No me sentiría muy mal por el uso de SecurityContextHolder. aunque. Sé que se trata de una estática / Singleton y que la primavera desalienta el uso de este tipo de cosas, pero su aplicación se encarga de comportarse adecuadamente en función del entorno: ámbito de sesión en un contenedor de servlets, hilo con ámbito en una prueba unitaria, etc. El factor limitante de bienes de un Singleton es cuando se proporciona una implementación que es inflexible a diferentes ambientes.

Otros consejos

Sólo hazlo de la forma habitual y luego insertarlo 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);

Tiene toda la razón para estar preocupados - llamadas a métodos estáticos son particularmente problemáticos para las pruebas unitarias como no se puede burlar fácilmente dependencias. Lo que voy a mostrar es cómo hacer que el contenedor IoC Primavera haga el trabajo sucio por ti, dejándole con código limpio, comprobable. SecurityContextHolder es una clase de marco y si bien puede ser aceptable para su código de seguridad de bajo nivel para estar atado a ella, es probable que desee para exponer una interfaz más limpia a los componentes de interfaz de usuario (es decir, los controladores).

cliff.meyers mencionan una manera alrededor de ella - crear su propio tipo "principal" e inyectar una instancia en consumidores. La primavera << a href = "http://static.springframework.org/spring/docs/2.5.x/reference/beans.html#beans-factory-scopes-other-injection" rel = "noreferrer"> AOP: de ámbito-proxy /> tag introducido en 2.x combinado con una definición de frijol solicitud ámbito de aplicación, y el soporte de fábrica-método puede ser el billete para el código más legible.

Podría funcionar como siguiente:

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
}

No hay nada complicado hasta ahora, ¿verdad? De hecho es probable que tenías que hacer la mayor parte de esto ya. A continuación, en el contexto de frijol definir un grano de petición con ámbito de mantener al director:

<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 AOP: Etiqueta de ámbito de proxy, los getUserDetails método estático será llamado cada vez que una nueva petición HTTP llega y todas las referencias a la propiedad CurrentUser será resuelto correctamente. Ahora la unidad de pruebas convertido en algo trivial:

protected void setUp() {
    // existing init code

    MyUserDetails user = new MyUserDetails();
    // set up user as you wish
    controller.setCurrentUser(user);
}

Espero que esto ayude!

Sin responder a la pregunta acerca de cómo crear e inyectar objetos de autenticación, Spring Security 4.0 ofrece algunas alternativas de bienvenida cuando se trata de pruebas. El @WithMockUser anotación permite al desarrollador especificar un usuario maqueta (con las 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 utilizar @WithUserDetails para emular un UserDetails volvió de la UserDetailsService, por ejemplo.

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

Más detalles se pueden encontrar en el @ WithMockUser y la @ WithUserDetails capítulos en los documentos de referencia de seguridad de resorte (de las que se copian los ejemplos anteriores)

En lo personal me acaba de utilizar Powermock junto con Mockito o EasyMock burlarse de la SecurityContextHolder.getSecurityContext estática () en su unidad de prueba / 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 de placa de la caldera aquí es decir, se burlan de un objeto de autenticación, burlarse de un SecurityContext para devolver la autenticación y, finalmente, burlarse de la SecurityContextHolder para obtener el SecurityContext, sin embargo es muy flexible y permite realizar pruebas unitarias para escenarios como objetos nulos de autenticación etc., sin tener que cambiar su (no de prueba) código

El uso de un estático en este caso es la mejor manera de escribir código seguro.

Sí, la estática son generalmente mal - por lo general, pero en este caso, la estática es lo que quiere. Dado el contexto de seguridad asocia un director con el hilo actualmente en ejecución, el código más seguro sería acceder a la estática del hilo lo más directamente posible. Ocultar el acceso detrás de una clase de contenedor que se inyecta permitir a un atacante más puntos para atacar. No necesitarían acceso al código (que tendrían un tiempo difícil cambiar si se firmó el frasco), sólo necesitan una manera de anular la configuración, lo que se puede hacer en tiempo de ejecución o se deslice algún XML en la ruta de clase. Incluso el uso de la inyección de anotación sería reemplazable con XML externo. Tal XML podría inyectar el sistema que ejecuta con un director sin escrúpulos.

Me hice la misma pregunta a mí mismo sobre aquí , y acaba de publicar una respuesta que he encontrado recientemente. respuesta corta es: inyectar un SecurityContext, y se refieren a SecurityContextHolder sólo en su configuración de Primavera para obtener el <=>

Me gustaría echar un vistazo a las clases de prueba abstractos de primavera y objetos simulados que se hablaba de aquí . Ellos constituyen una forma eficaz de auto-cableado de los objetos gestionados de Spring unidad de creación e integración de las pruebas más sencillas.

General

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

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

Análisis

En la prueba de unidad es obvio que puede llamar a este método directamente. En las pruebas de integración usando org.springframework.test.web.servlet.MockMvc puede utilizar org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user() para inyectar el usuario así:

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

Esto sin embargo sólo tiene que rellenar directamente el SecurityContext. Si desea asegurarse de que el usuario se carga de una sesión en la prueba, puede utilizar lo siguiente:

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;
        }
    };
}

La autenticación es una propiedad de un subproceso 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 una configuración inconveniente y una sobrecarga de cableado sin ningún beneficio.

En cuanto a la autenticación de prueba, hay varias formas de hacerle la vida más fácil.Mi favorito es hacer una anotación personalizada. @Authenticated y el oyente de ejecución de pruebas, que lo gestiona.Controlar DirtiesContextTestExecutionListener por inspiración.

Después de un buen montón de trabajo que era capaz de reproducir el comportamiento deseado. Yo había emulado el inicio de sesión a través MockMvc. Es demasiado pesado para la mayoría de las pruebas unitarias, pero útil para las pruebas de integración.

Por supuesto que estoy dispuesto a ver las nuevas características en la primavera de Seguridad 4.0 que hará que nuestras pruebas más fácil.

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 
}
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top