Cómo configurar manualmente un usuario autenticado en Spring Security / SpringMVC

107

Después de que un nuevo usuario envía un formulario de 'Nueva cuenta', quiero iniciar sesión manualmente con ese usuario para que no tenga que iniciar sesión en la página siguiente.

La página de inicio de sesión de formulario normal que pasa por el interceptor de seguridad de primavera funciona bien.

En el controlador de formulario de cuenta nueva, estoy creando un UsernamePasswordAuthenticationToken y configurándolo en SecurityContext manualmente:

SecurityContextHolder.getContext().setAuthentication(authentication);

En esa misma página, luego verifico que el usuario haya iniciado sesión con:

SecurityContextHolder.getContext().getAuthentication().getAuthorities();

Esto devuelve las autoridades que establecí anteriormente en la autenticación. Todo está bien.

Pero cuando se llama a este mismo código en la página siguiente que cargo, el token de autenticación es solo UserAnonymous.

No tengo claro por qué no mantuvo la autenticación que configuré en la solicitud anterior. ¿Alguna idea?

  • ¿Podría tener que ver con que los ID de sesión no están configurados correctamente?
  • ¿Hay algo que posiblemente esté sobrescribiendo mi autenticación de alguna manera?
  • ¿Quizás solo necesito otro paso para guardar la autenticación?
  • ¿O hay algo que deba hacer para declarar la autenticación en toda la sesión en lugar de una sola solicitud de alguna manera?

Solo busco algunas ideas que puedan ayudarme a ver lo que está sucediendo aquí.

David Parks
fuente
1
Puede seguir mi respuesta a stackoverflow.com/questions/4824395/…
AlexK
2
Los lectores, tengan cuidado de las respuestas a esta pregunta si le dicen que lo haga: SecurityContextHolder.getContext().setAuthentication(authentication). Funciona y es común, pero existen serias deficiencias de funcionalidad que encontrará si lo hace. Para obtener más información, consulte mi pregunta y la respuesta: stackoverflow.com/questions/47233187/…
cabra

Respuestas:

62

Tuve el mismo problema que tú hace un tiempo. No recuerdo los detalles, pero el siguiente código hizo que las cosas me funcionaran. Este código se usa dentro de un flujo de Spring Webflow, de ahí las clases RequestContext y ExternalContext. Pero la parte que es más relevante para usted es el método doAutoLogin.

public String registerUser(UserRegistrationFormBean userRegistrationFormBean,
                           RequestContext requestContext,
                           ExternalContext externalContext) {

    try {
        Locale userLocale = requestContext.getExternalContext().getLocale();
        this.userService.createNewUser(userRegistrationFormBean, userLocale, Constants.SYSTEM_USER_ID);
        String emailAddress = userRegistrationFormBean.getChooseEmailAddressFormBean().getEmailAddress();
        String password = userRegistrationFormBean.getChoosePasswordFormBean().getPassword();
        doAutoLogin(emailAddress, password, (HttpServletRequest) externalContext.getNativeRequest());
        return "success";

    } catch (EmailAddressNotUniqueException e) {
        MessageResolver messageResolvable 
                = new MessageBuilder().error()
                                      .source(UserRegistrationFormBean.PROPERTYNAME_EMAIL_ADDRESS)
                                      .code("userRegistration.emailAddress.not.unique")
                                      .build();
        requestContext.getMessageContext().addMessage(messageResolvable);
        return "error";
    }

}


private void doAutoLogin(String username, String password, HttpServletRequest request) {

    try {
        // Must be called from request filtered by Spring Security, otherwise SecurityContextHolder is not updated
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
        token.setDetails(new WebAuthenticationDetails(request));
        Authentication authentication = this.authenticationProvider.authenticate(token);
        logger.debug("Logging in with [{}]", authentication.getPrincipal());
        SecurityContextHolder.getContext().setAuthentication(authentication);
    } catch (Exception e) {
        SecurityContextHolder.getContext().setAuthentication(null);
        logger.error("Failure in autoLogin", e);
    }

}
Kevin Stembridge
fuente
2
Gracias, el código es muy útil para ayudarme a saber que estoy solucionando problemas en el área correcta. Parece que tengo una pistola humeante, está creando una nueva identificación de sesión después de la autenticación manual, pero la antigua identificación de sesión todavía se identifica a partir de la cookie. Tengo que averiguar por qué ahora, pero al menos estoy claramente encaminado. ¡Gracias!
David Parks
4
Cualquiera que siga esta guía también debería ver este problema relacionado: stackoverflow.com/questions/4824395/…
David Parks
14
¿Puede explicar cómo obtiene el
proveedor de
1
@ s1moner3d, debería poder inyectarlo a través de IoC -> \ @Autowired
Hartmut
1
@Configuration public class WebConfig extends WebSecurityConfigurerAdapter { @Bean @Override public AuthenticationManager authenticationProvider() throws Exception { return super.authenticationManagerBean(); } }
slisnychyi
66

No pude encontrar ninguna otra solución completa, así que pensé en publicar la mía. Esto puede ser un truco, pero resolvió el problema anterior:

public void login(HttpServletRequest request, String userName, String password)
{

    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(userName, password);

    // Authenticate the user
    Authentication authentication = authenticationManager.authenticate(authRequest);
    SecurityContext securityContext = SecurityContextHolder.getContext();
    securityContext.setAuthentication(authentication);

    // Create a new session and add the security context.
    HttpSession session = request.getSession(true);
    session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
}
Stuart McIntyre
fuente
7
+1 - ¡Esto me ayudó! Me faltaba la actualización SPRING_SECURITY_CONTEXT. ... Pero, ¿qué tan "sucio" es esto?
l3dx
12
¿De dónde obtiene authenticationManagera partir?
Isaac
2
AuthenticationManager está conectado automáticamente en su clase como este @Autowired AuthenticationServiceImpl authenticationManager. Y también debe haber una inyección de bean en su configuración xml, para que Spring sepa qué inyectar.
1
¿Dónde está la implementación de AuthenticationServiceImpl? ¿Qué contiene esta clase?
Pra_A
3
¿Por qué es necesario crear una nueva sesión? ¿SecurityContext no maneja eso?
Vlad Manuel Mureșan
17

Finalmente descubrió la raíz del problema.

Cuando creo el contexto de seguridad manualmente, no se crea ningún objeto de sesión. Solo cuando la solicitud termina de procesarse, el mecanismo de Spring Security se da cuenta de que el objeto de sesión es nulo (cuando intenta almacenar el contexto de seguridad en la sesión después de que se haya procesado la solicitud).

Al final de la solicitud, Spring Security crea un nuevo objeto de sesión e ID de sesión. Sin embargo, esta nueva ID de sesión nunca llega al navegador porque ocurre al final de la solicitud, después de que se haya dado la respuesta al navegador. Esto hace que la nueva ID de sesión (y por lo tanto el contexto de seguridad que contiene mi usuario que inició sesión manualmente) se pierda cuando la siguiente solicitud contiene la ID de sesión anterior.

David Parks
fuente
4
Honestamente, esto se siente como un defecto de diseño en la seguridad de primavera más que nada. Hay muchos marcos escritos en otros lenguajes que no tendrían ningún problema con esto, pero Spring Security simplemente se rompe.
chubbsondubs
3
y la solución es?
s1moner3d
2
¿y cual es la solución?
Thiago
6

Active el registro de depuración para tener una mejor idea de lo que está sucediendo.

Puede saber si las cookies de sesión se están configurando utilizando un depurador del lado del navegador para ver los encabezados devueltos en las respuestas HTTP. (También hay otras formas).

Una posibilidad es que SpringSecurity esté configurando cookies de sesión segura y la siguiente página solicitada tenga una URL "http" en lugar de una URL "https". (El navegador no enviará una cookie segura para una URL "http").

Stephen C
fuente
Gracias, todas fueron sugerencias muy útiles y relevantes.
David Parks
5

La nueva función de filtrado en Servlet 2.4 básicamente alivia la restricción de que los filtros solo pueden operar en el flujo de solicitudes antes y después del procesamiento real de solicitudes por parte del servidor de aplicaciones. En cambio, los filtros de Servlet 2.4 ahora pueden interactuar con el despachador de solicitudes en cada punto de envío. Esto significa que cuando un recurso web reenvía una solicitud a otro recurso (por ejemplo, un servlet que envía la solicitud a una página JSP en la misma aplicación), un filtro puede estar funcionando antes de que el recurso de destino maneje la solicitud. También significa que si un recurso web incluye la salida o función de otros recursos web (por ejemplo, una página JSP que incluye la salida de varias otras páginas JSP), los filtros de Servlet 2.4 pueden funcionar antes y después de cada uno de los recursos incluidos. .

Para activar esa función, necesita:

web.xml

<filter>   
    <filter-name>springSecurityFilterChain</filter-name>   
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> 
</filter>  
<filter-mapping>   
    <filter-name>springSecurityFilterChain</filter-name>   
    <url-pattern>/<strike>*</strike></url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
</filter-mapping>

Controlador de registro

return "forward:/login?j_username=" + registrationModel.getUserEmail()
        + "&j_password=" + registrationModel.getPassword();
AlexK
fuente
Buena información, pero poner el nombre de usuario y la contraseña en la URL es malo. 1) no se realiza ningún escape, por lo que es probable que se rompa un nombre de usuario o contraseña con caracteres especiales o, lo que es peor, se utilice como vector de vulnerabilidad de seguridad. 2) las contraseñas en las URL son malas porque las URL a menudo se registran en el disco, lo cual es bastante malo para la seguridad: todas tus contraseñas en texto plano simplemente ahí.
cabra
1

Estaba tratando de probar una aplicación extjs y después de configurar con éxito un testingAuthenticationToken, esto de repente dejó de funcionar sin una causa obvia.

No pude hacer que las respuestas anteriores funcionaran, por lo que mi solución fue omitir este poco de primavera en el entorno de prueba. Introduje una costura alrededor de la primavera como esta:

public class SpringUserAccessor implements UserAccessor
{
    @Override
    public User getUser()
    {
        SecurityContext context = SecurityContextHolder.getContext();
        Authentication authentication = context.getAuthentication();
        return (User) authentication.getPrincipal();
    }
}

El usuario es un tipo personalizado aquí.

Luego lo estoy envolviendo en una clase que solo tiene una opción para que el código de prueba cambie el resorte.

public class CurrentUserAccessor
{
    private static UserAccessor _accessor;

    public CurrentUserAccessor()
    {
        _accessor = new SpringUserAccessor();
    }

    public User getUser()
    {
        return _accessor.getUser();
    }

    public static void UseTestingAccessor(User user)
    {
        _accessor = new TestUserAccessor(user);
    }
}

La versión de prueba se ve así:

public class TestUserAccessor implements UserAccessor
{
    private static User _user;

    public TestUserAccessor(User user)
    {
        _user = user;
    }

    @Override
    public User getUser()
    {
        return _user;
    }
}

En el código de llamada, sigo usando un usuario adecuado cargado desde la base de datos:

    User user = (User) _userService.loadUserByUsername(username);
    CurrentUserAccessor.UseTestingAccessor(user);

Obviamente, esto no será adecuado si realmente necesita usar la seguridad, pero estoy ejecutando una configuración sin seguridad para la implementación de prueba. Pensé que alguien más podría encontrarse con una situación similar. Este es un patrón que he usado antes para simular dependencias estáticas. La otra alternativa es que puede mantener la estática de la clase contenedora, pero yo prefiero esta, ya que las dependencias del código son más explícitas, ya que debe pasar CurrentUserAccessor a las clases donde sea necesario.

JonnyRaa
fuente