¿Cómo puedo usar Spring Security sin sesiones?

99

Estoy creando una aplicación web con Spring Security que vivirá en Amazon EC2 y usaré los Elastic Load Balancers de Amazon. Desafortunadamente, ELB no admite sesiones fijas, por lo que necesito asegurarme de que mi aplicación funcione correctamente sin sesiones.

Hasta ahora, he configurado RememberMeServices para asignar un token a través de una cookie, y esto funciona bien, pero quiero que la cookie caduque con la sesión del navegador (por ejemplo, cuando el navegador se cierra).

Tengo que imaginar que no soy el primero en querer usar Spring Security sin sesiones ... ¿alguna sugerencia?

Jarrod Carlson
fuente

Respuestas:

124

En Spring Security 3 con Java Config , puede usar HttpSecurity.sessionManagement () :

@Override
protected void configure(final HttpSecurity http) throws Exception {
    http
        .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
Ben Hutchison
fuente
2
Esta es la respuesta correcta para la configuración de Java, reflejando lo que @sappenin declaró correctamente para la configuración xml en un comentario sobre la respuesta aceptada. Usamos este método y, de hecho, nuestra aplicación no tiene sesión.
Paul
Esto tiene un efecto secundario. El contenedor Tomcat agregará "; jsessionid = ..." a las solicitudes de imágenes, hojas de estilo, etc., porque a Tomcat no le gusta ser sin estado, y Spring Security bloqueará estos activos en la primera carga porque "la URL contenía un Cadena potencialmente maliciosa ';' ".
workerjoe
@workerjoe Entonces, ¿qué está tratando de decir con esta configuración de java, las sesiones no son creadas por Spring Security sino por Tomcat?
Vishwas Atrey
@VishwasAtrey Según tengo entendido (lo que puede ser incorrecto), Tomcat crea y mantiene las sesiones. Spring se aprovecha de ellos, agregando sus propios datos. Intenté hacer una aplicación web sin estado y no funcionó, como mencioné anteriormente. Consulte esta respuesta a mi propia pregunta para obtener más información.
workerjoe
28

Parece ser incluso más fácil en Spring Securitiy 3.0. Si está utilizando la configuración del espacio de nombres, simplemente puede hacer lo siguiente:

<http create-session="never">
  <!-- config -->
</http>

O bien, podría configurar el SecurityContextRepository como nulo, y nunca nada se obtendría salvado de esa manera así .

Jarrod Carlson
fuente
5
Esto no funcionó como pensé. En cambio, hay un comentario a continuación que distingue entre "nunca" y "apátridas". Usando "nunca", mi aplicación todavía estaba creando sesiones. Al usar "sin estado", mi aplicación en realidad se convirtió en sin estado y no necesité implementar ninguna de las anulaciones mencionadas en otras respuestas. Vea el problema de JIRA aquí: jira.springsource.org/browse/SEC-1424
sappenin
27

Trabajamos en el mismo problema (inyectando un SecurityContextRepository personalizado en SecurityContextPersistenceFilter) durante 4-5 horas hoy. Finalmente, lo descubrimos. En primer lugar, en el apartado 8.3 de Spring Security ref. doc, hay una definición de bean SecurityContextPersistenceFilter

<bean id="securityContextPersistenceFilter" class="org.springframework.security.web.context.SecurityContextPersistenceFilter">
    <property name='securityContextRepository'>
        <bean class='org.springframework.security.web.context.HttpSessionSecurityContextRepository'>
            <property name='allowSessionCreation' value='false' />
        </bean>
    </property>
</bean>

Y después de esta definición, hay esta explicación: "Alternativamente, podría proporcionar una implementación nula de la interfaz SecurityContextRepository, lo que evitará que se almacene el contexto de seguridad, incluso si ya se ha creado una sesión durante la solicitud".

Necesitábamos inyectar nuestro SecurityContextRepository personalizado en SecurityContextPersistenceFilter. Así que simplemente cambiamos la definición de bean anterior con nuestro impl personalizado y lo pusimos en el contexto de seguridad.

Cuando ejecutamos la aplicación, rastreamos los registros y vimos que SecurityContextPersistenceFilter no estaba usando nuestro impl personalizado, estaba usando HttpSessionSecurityContextRepository.

Después de algunas otras cosas que probamos, nos dimos cuenta de que teníamos que dar nuestro impl SecurityContextRepository personalizado con el atributo "security-context-repository-ref" del espacio de nombres "http". Si usa el espacio de nombres "http" y desea inyectar su propio impl de SecurityContextRepository, pruebe el atributo "security-context-repository-ref".

Cuando se utiliza el espacio de nombres "http", se ignora una definición de SecurityContextPersistenceFilter separada. Como copié arriba, el documento de referencia. no dice eso.

Por favor corríjame si entendí mal las cosas.

Basri Kahveci
fuente
Gracias, esta es una información valiosa. Lo probaré en mi aplicación.
Jeff Evans
Gracias, eso es lo que necesitaba con Spring 3.0
Justin Ludwig
1
Usted es bastante preciso cuando dice que el espacio de nombres http no permite un SecurityContextPersistenceFilter personalizado, me tomó un par de horas de depuración para resolverlo
Jaime Hablutzel
¡Muchas gracias por publicar esto! Estaba a punto de arrancarme el poco pelo que tengo. Me preguntaba por qué el método setSecurityContextRepository de SecurityContextPersistenceFilter estaba en desuso (los documentos dicen que se debe usar la inyección del constructor, lo cual tampoco es correcto).
fool4jesus
10

Eche un vistazo a la SecurityContextPersistenceFilterclase. Define cómo SecurityContextHolderestá poblado. Por defecto usaHttpSessionSecurityContextRepository para almacenar el contexto de seguridad en la sesión http.

He implementado este mecanismo con bastante facilidad, con custom SecurityContextRepository.

Vea lo securityContext.xmlsiguiente:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:sec="http://www.springframework.org/schema/security"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
       http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
       http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.0.xsd">

    <context:annotation-config/>

    <sec:global-method-security secured-annotations="enabled" pre-post-annotations="enabled"/>

    <bean id="securityContextRepository" class="com.project.server.security.TokenSecurityContextRepository"/>

    <bean id="securityContextFilter" class="com.project.server.security.TokenSecurityContextPersistenceFilter">
        <property name="repository" ref="securityContextRepository"/>
    </bean>

    <bean id="logoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">
        <constructor-arg value="/login.jsp"/>
        <constructor-arg>
            <list>
                <bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>
            </list>
        </constructor-arg>
    </bean>

    <bean id="formLoginFilter"
          class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
        <property name="authenticationManager" ref="authenticationManager"/>
        <property name="authenticationSuccessHandler">
            <bean class="com.project.server.security.TokenAuthenticationSuccessHandler">
                <property name="defaultTargetUrl" value="/index.html"/>
                <property name="passwordExpiredUrl" value="/changePassword.jsp"/>
                <property name="alwaysUseDefaultTargetUrl" value="true"/>
            </bean>
        </property>
        <property name="authenticationFailureHandler">
            <bean class="com.project.server.modules.security.CustomUrlAuthenticationFailureHandler">
                <property name="defaultFailureUrl" value="/login.jsp?failure=1"/>
            </bean>
        </property>
        <property name="filterProcessesUrl" value="/j_spring_security_check"/>
        <property name="allowSessionCreation" value="false"/>
    </bean>

    <bean id="servletApiFilter"
          class="org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter"/>

    <bean id="anonFilter" class="org.springframework.security.web.authentication.AnonymousAuthenticationFilter">
        <property name="key" value="ClientApplication"/>
        <property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS"/>
    </bean>


    <bean id="exceptionTranslator" class="org.springframework.security.web.access.ExceptionTranslationFilter">
        <property name="authenticationEntryPoint">
            <bean class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
                <property name="loginFormUrl" value="/login.jsp"/>
            </bean>
        </property>
        <property name="accessDeniedHandler">
            <bean class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
                <property name="errorPage" value="/login.jsp?failure=2"/>
            </bean>
        </property>
        <property name="requestCache">
            <bean id="nullRequestCache" class="org.springframework.security.web.savedrequest.NullRequestCache"/>
        </property>
    </bean>

    <alias name="filterChainProxy" alias="springSecurityFilterChain"/>

    <bean id="filterChainProxy" class="org.springframework.security.web.FilterChainProxy">
        <sec:filter-chain-map path-type="ant">
            <sec:filter-chain pattern="/**"
                              filters="securityContextFilter, logoutFilter, formLoginFilter,
                                        servletApiFilter, anonFilter, exceptionTranslator, filterSecurityInterceptor"/>
        </sec:filter-chain-map>
    </bean>

    <bean id="filterSecurityInterceptor"
          class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
        <property name="securityMetadataSource">
            <sec:filter-security-metadata-source use-expressions="true">
                <sec:intercept-url pattern="/staticresources/**" access="permitAll"/>
                <sec:intercept-url pattern="/index.html*" access="hasRole('USER_ROLE')"/>
                <sec:intercept-url pattern="/rpc/*" access="hasRole('USER_ROLE')"/>
                <sec:intercept-url pattern="/**" access="permitAll"/>
            </sec:filter-security-metadata-source>
        </property>
        <property name="authenticationManager" ref="authenticationManager"/>
        <property name="accessDecisionManager" ref="accessDecisionManager"/>
    </bean>

    <bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
        <property name="decisionVoters">
            <list>
                <bean class="org.springframework.security.access.vote.RoleVoter"/>
                <bean class="org.springframework.security.web.access.expression.WebExpressionVoter"/>
            </list>
        </property>
    </bean>

    <bean id="authenticationManager" class="org.springframework.security.authentication.ProviderManager">
        <property name="providers">
            <list>
                <bean name="authenticationProvider"
                      class="com.project.server.modules.security.oracle.StoredProcedureBasedAuthenticationProviderImpl">
                    <property name="dataSource" ref="serverDataSource"/>
                    <property name="userDetailsService" ref="userDetailsService"/>
                    <property name="auditLogin" value="true"/>
                    <property name="postAuthenticationChecks" ref="customPostAuthenticationChecks"/>
                </bean>
            </list>
        </property>
    </bean>

    <bean id="customPostAuthenticationChecks" class="com.project.server.modules.security.CustomPostAuthenticationChecks"/>

    <bean name="userDetailsService" class="com.project.server.modules.security.oracle.UserDetailsServiceImpl">
        <property name="dataSource" ref="serverDataSource"/>
    </bean>

</beans>
Lukas Herman
fuente
1
Hola Lukas, ¿puede darnos más detalles sobre la implementación del repositorio de contexto de seguridad?
Jim Downing
1
la clase TokenSecurityContextRepository contiene HashMap <String, SecurityContext> contextMap. En loadContext (), el método comprueba si existe SecurityContext para el código hash de sesión pasado por requestParameter sid, cookie, requestHeader personalizado o combinación de cualquiera de los anteriores. Devuelve SecurityContextHolder.createEmptyContext () si el contexto no se pudo resolver. El método saveContext coloca el contexto resuelto en contextMap.
Lukas Herman
8

En realidad create-session="never", no significa ser completamente apátrida. Hay un problema para eso en la gestión de problemas de Spring Security.

hleinona
fuente
3

Después de luchar con las numerosas soluciones publicadas en esta respuesta, para intentar que algo funcione al usar el <http> configuración espacio de nombres, finalmente encontré un enfoque que realmente funciona para mi caso de uso. En realidad, no requiero que Spring Security no inicie una sesión (porque uso la sesión en otras partes de la aplicación), solo que no "recuerda" la autenticación en la sesión en absoluto (debe volver a verificarse cada solicitud).

Para empezar, no pude averiguar cómo hacer la técnica de "implementación nula" descrita anteriormente. No estaba claro si se suponía que debía configurar securityContextRepository en nullo en una implementación sin operación. El primero no funciona porque NullPointerExceptionse tira a dentro SecurityContextPersistenceFilter.doFilter(). En cuanto a la implementación sin operación, intenté implementar de la manera más simple que pude imaginar:

public class NullSpringSecurityContextRepository implements SecurityContextRepository {

    @Override
    public SecurityContext loadContext(final HttpRequestResponseHolder requestResponseHolder_) {
        return SecurityContextHolder.createEmptyContext();
    }

    @Override
    public void saveContext(final SecurityContext context_, final HttpServletRequest request_,
            final HttpServletResponse response_) {
    }

    @Override
    public boolean containsContext(final HttpServletRequest request_) {
        return false;
    }

}

Esto no funciona en mi aplicación, debido a algo extraño que ClassCastExceptiontiene que ver con el response_tipo.

Incluso asumiendo que logré encontrar una implementación que funcione (simplemente no almacenando el contexto en la sesión), todavía existe el problema de cómo inyectar eso en los filtros construidos por la <http>configuración. No puede simplemente reemplazar el filtro en la SECURITY_CONTEXT_FILTERposición, según los documentos . La única forma que encontré de conectarme al SecurityContextPersistenceFilterque se crea debajo de las cubiertas fue escribir un ApplicationContextAwarefrijol feo :

public class SpringSecuritySessionDisabler implements ApplicationContextAware {

    private final Logger logger = LoggerFactory.getLogger(SpringSecuritySessionDisabler.class);

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(final ApplicationContext applicationContext_) throws BeansException {
        applicationContext = applicationContext_;
    }

    public void disableSpringSecuritySessions() {
        final Map<String, FilterChainProxy> filterChainProxies = applicationContext
                .getBeansOfType(FilterChainProxy.class);
        for (final Entry<String, FilterChainProxy> filterChainProxyBeanEntry : filterChainProxies.entrySet()) {
            for (final Entry<String, List<Filter>> filterChainMapEntry : filterChainProxyBeanEntry.getValue()
                    .getFilterChainMap().entrySet()) {
                final List<Filter> filterList = filterChainMapEntry.getValue();
                if (filterList.size() > 0) {
                    for (final Filter filter : filterList) {
                        if (filter instanceof SecurityContextPersistenceFilter) {
                            logger.info(
                                    "Found SecurityContextPersistenceFilter, mapped to URL '{}' in the FilterChainProxy bean named '{}', setting its securityContextRepository to the null implementation to disable caching of authentication",
                                    filterChainMapEntry.getKey(), filterChainProxyBeanEntry.getKey());
                            ((SecurityContextPersistenceFilter) filter).setSecurityContextRepository(
                             new NullSpringSecurityContextRepository());
                        }
                    }
                }

            }
        }
    }
}

De todos modos, a la solución que realmente funciona, aunque muy pirateada. Simplemente use un Filterque borre la entrada de sesión que HttpSessionSecurityContextRepositorybusca cuando hace lo suyo:

public class SpringSecuritySessionDeletingFilter extends GenericFilterBean implements Filter {

    @Override
    public void doFilter(final ServletRequest request_, final ServletResponse response_, final FilterChain chain_)
            throws IOException, ServletException {
        final HttpServletRequest servletRequest = (HttpServletRequest) request_;
        final HttpSession session = servletRequest.getSession();
        if (session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY) != null) {
            session.removeAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
        }

        chain_.doFilter(request_, response_);
    }
}

Luego en la configuración:

<bean id="springSecuritySessionDeletingFilter"
    class="SpringSecuritySessionDeletingFilter" />

<sec:http auto-config="false" create-session="never"
    entry-point-ref="authEntryPoint">
    <sec:intercept-url pattern="/**"
        access="IS_AUTHENTICATED_REMEMBERED" />
    <sec:intercept-url pattern="/static/**" filters="none" />
    <sec:custom-filter ref="myLoginFilterChain"
        position="FORM_LOGIN_FILTER" />

    <sec:custom-filter ref="springSecuritySessionDeletingFilter"
        before="SECURITY_CONTEXT_FILTER" />
</sec:http>
Jeff Evans
fuente
Nueve años después, esta sigue siendo la respuesta correcta. Ahora podemos usar la configuración de Java en lugar de XML. WebSecurityConfigurerAdapterhttp.addFilterBefore(new SpringSecuritySessionDeletingFilter(), SecurityContextPersistenceFilter.class)
Agregué
3

Solo una nota rápida: es "crear sesión" en lugar de "crear sesiones"

crear sesión

Controla el entusiasmo con el que se crea una sesión HTTP.

Si no se establece, el valor predeterminado es "ifRequired". Otras opciones son "siempre" y "nunca".

La configuración de este atributo afecta las propiedades allowSessionCreation y forceEagerSessionCreation de HttpSessionContextIntegrationFilter. allowSessionCreation siempre será verdadero a menos que este atributo se establezca en "nunca". forceEagerSessionCreation es "falso" a menos que se establezca en "siempre".

Entonces, la configuración predeterminada permite la creación de sesiones pero no la fuerza. La excepción es si el control de sesión concurrente está habilitado, cuando forceEagerSessionCreation se establecerá en verdadero, independientemente de cuál sea la configuración aquí. El uso de "nunca" causaría una excepción durante la inicialización de HttpSessionContextIntegrationFilter.

Para obtener detalles específicos del uso de la sesión, hay buena documentación en el javadoc HttpSessionSecurityContextRepository.

Jon Vaughan
fuente
Todas estas son excelentes respuestas, pero me he estado golpeando la cabeza contra la pared tratando de descubrir cómo lograr esto cuando utilizo el elemento de configuración <http>. Incluso con auto-config=false, aparentemente no puede reemplazar lo que está en la SECURITY_CONTEXT_FILTERposición con el suyo. He estado pirateando tratando de deshabilitarlo con un ApplicationContextAwarebean (usando la reflexión para forzar securityContextRepositoryuna implementación nula SessionManagementFilter) pero sin dados. Y, lamentablemente, no puedo cambiar a la seguridad de primavera de 3.1 años que proporcionaría create-session=stateless.
Jeff Evans
Visite este sitio, siempre informativo. Espero que esto te ayude a ti y a otros también " baeldung.com/spring-security-session " • siempre: siempre se creará una sesión si no existe una • ifRequired: se creará una sesión solo si es necesario (predeterminado) • nunca: el marco nunca creará una sesión por sí mismo, pero usará una si ya existe • sin estado: Spring Security no creará ni usará ninguna sesión
Java_Fire_Within el