Como posso usar o Spring Security sem sessões?

99

Estou construindo um aplicativo da web com Spring Security que ficará no Amazon EC2 e usará os Elastic Load Balancers da Amazon. Infelizmente, o ELB não oferece suporte a sessões persistentes, portanto, preciso garantir que meu aplicativo funcione corretamente sem sessões.

Até agora, configurei o RememberMeServices para atribuir um token por meio de um cookie, e isso funciona bem, mas quero que o cookie expire com a sessão do navegador (por exemplo, quando o navegador fecha).

Tenho que imaginar que não sou o primeiro a querer usar o Spring Security sem sessões ... alguma sugestão?

Jarrod Carlson
fonte

Respostas:

124

No Spring Security 3 com Java Config , você pode usar HttpSecurity.sessionManagement () :

@Override
protected void configure(final HttpSecurity http) throws Exception {
    http
        .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
Ben Hutchison
fonte
2
Esta é a resposta correta para a configuração Java, espelhando o que @sappenin afirmou corretamente para a configuração xml em um comentário sobre a resposta aceita. Usamos esse método e, de fato, nosso aplicativo não tem sessão.
Paul
Isso tem um efeito colateral. O contêiner Tomcat anexará "; jsessionid = ..." às ​​solicitações de imagens, folhas de estilo, etc, porque o Tomcat não gosta de ser sem estado, e o Spring Security irá então bloquear esses ativos no primeiro carregamento porque "a URL continha um String potencialmente malicioso ';' ".
workerjoe
@workerjoe Então, o que você está tentando dizer com essa configuração java, as sessões não são criadas pelo spring security, e sim pelo tomcat?
Vishwas Atrey
@VishwasAtrey No meu entendimento (o que pode estar errado), o Tomcat cria e mantém as sessões. Spring tira proveito deles, adicionando seus próprios dados. Tentei fazer um aplicativo da web sem estado e não funcionou, como mencionei acima. Veja esta resposta à minha própria pergunta para mais informações.
workerjoe
28

Parece ser ainda mais fácil no Spring Securitiy 3.0. Se estiver usando a configuração de namespace, você pode simplesmente fazer o seguinte:

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

Ou você pode configurar o SecurityContextRepository como nulo, e nada jamais será salvo dessa forma também .

Jarrod Carlson
fonte
5
Isso não funcionou como eu esperava. Em vez disso, há um comentário abaixo que distingue entre "nunca" e "sem estado". Usando "nunca", meu aplicativo ainda estava criando sessões. Usando "sem estado", meu aplicativo ficou sem estado e não precisei implementar nenhuma das substituições mencionadas em outras respostas. Veja o problema do JIRA aqui: jira.springsource.org/browse/SEC-1424
sappenin
27

Trabalhamos no mesmo problema (injetando um SecurityContextRepository personalizado no SecurityContextPersistenceFilter) por 4 a 5 horas hoje. Finalmente, nós descobrimos. Em primeiro lugar, na seção 8.3 do Spring Security ref. doc, há uma definição 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>

E depois dessa definição, há esta explicação: "Alternativamente, você pode fornecer uma implementação nula da interface SecurityContextRepository, que impedirá o contexto de segurança de ser armazenado, mesmo se uma sessão já tiver sido criada durante a solicitação."

Precisávamos injetar nosso SecurityContextRepository personalizado no SecurityContextPersistenceFilter. Portanto, simplesmente mudamos a definição do bean acima com nosso impl customizado e o colocamos no contexto de segurança.

Quando executamos o aplicativo, rastreamos os logs e vimos que SecurityContextPersistenceFilter não estava usando nosso impl customizado, estava usando o HttpSessionSecurityContextRepository.

Depois de algumas outras coisas que tentamos, descobrimos que tínhamos que fornecer nosso impl SecurityContextRepository personalizado com o atributo "security-context-repository-ref" do namespace "http". Se você usar o namespace "http" e quiser injetar seu próprio impl SecurityContextRepository, tente o atributo "security-context-repository-ref".

Quando o namespace "http" é usado, uma definição separada de SecurityContextPersistenceFilter é ignorada. Como copiei acima, o documento de referência. não afirma isso.

Por favor, me corrija se eu entendi mal as coisas.

Basri Kahveci
fonte
Obrigado, esta é uma informação valiosa. Vou experimentar em meu aplicativo.
Jeff Evans,
Obrigado, era isso que eu precisava com o Spring 3.0
Justin Ludwig
1
Você é bastante preciso quando diz que o namespace http não permite um SecurityContextPersistenceFilter personalizado. Levei algumas horas de depuração para descobrir isso
Jaime Hablutzel
Muito obrigado por postar isso! Eu estava prestes a arrancar o pouco cabelo que tenho. Eu estava me perguntando por que o método setSecurityContextRepository de SecurityContextPersistenceFilter foi preterido (os documentos dizendo para usar injeção de construtor, o que também não está certo).
silly4jesus
10

Dê uma olhada na SecurityContextPersistenceFilteraula. Ele define como o SecurityContextHolderé preenchido. Por padrão, ele usa HttpSessionSecurityContextRepositorypara armazenar o contexto de segurança na sessão http.

Implementei esse mecanismo com bastante facilidade, com personalização SecurityContextRepository.

Veja securityContext.xmlabaixo:

<?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
fonte
1
Olá Lukas, você pode fornecer mais detalhes sobre a implementação do repositório de contexto de segurança?
Jim Downing,
1
a classe TokenSecurityContextRepository contém HashMap <String, SecurityContext> contextMap. Em loadContext (), o método verifica se existe SecurityContext para o código hash da sessão passado por requestParameter sid, ou cookie, ou requestHeader personalizado ou combinação de qualquer um dos acima. Retorna SecurityContextHolder.createEmptyContext () se o contexto não puder ser resolvido. O método saveContext coloca o contexto resolvido no contextMap.
Lukas Herman
8

Na verdade create-session="never", não significa ser completamente sem estado. Há um problema para isso no gerenciamento de problemas do Spring Security.

hleinone
fonte
3

Depois de lutar com as inúmeras soluções postadas nesta resposta, para tentar fazer algo funcionar ao usar a <http>configuração do namespace, finalmente encontrei uma abordagem que realmente funciona para meu caso de uso. Na verdade, não exijo que o Spring Security não inicie uma sessão (porque eu uso a sessão em outras partes do aplicativo), apenas que ele não "lembre" de autenticação na sessão (deve ser verificado novamente cada pedido).

Para começar, não consegui descobrir como fazer a técnica de "implementação nula" descrita acima. Não estava claro se você deveria definir securityContextRepository como nullou como uma implementação autônoma. O primeiro não funciona porque um NullPointerExceptioné jogado dentro SecurityContextPersistenceFilter.doFilter(). Quanto à implementação no-op, tentei implementar da maneira mais simples que poderia 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;
    }

}

Isso não funciona no meu aplicativo, por causa de alguma coisa estranha ClassCastExceptionrelacionada ao response_tipo.

Mesmo supondo que consegui encontrar uma implementação que funcione (simplesmente não armazenando o contexto na sessão), ainda há o problema de como injetar isso nos filtros construídos pela <http>configuração. Você não pode simplesmente substituir o filtro na SECURITY_CONTEXT_FILTERposição, conforme os documentos . A única maneira que encontrei de me conectar com o SecurityContextPersistenceFilterque é criado sob as cobertas foi escrever um ApplicationContextAwarefeijão feio :

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 qualquer forma, para a solução que realmente funciona, embora muito hackeada. Basta usar um Filterque exclua a entrada de sessão que HttpSessionSecurityContextRepositoryprocura quando faz o que deve:

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

Então, na configuração:

<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
fonte
Nove anos depois, essa ainda é a resposta certa. Agora podemos usar a configuração Java em vez de XML. Eu adicionei o filtro personalizado no meu WebSecurityConfigurerAdaptercom " http.addFilterBefore(new SpringSecuritySessionDeletingFilter(), SecurityContextPersistenceFilter.class)"
workerjoe
3

Apenas uma nota rápida: é "criar sessão" em vez de "criar sessões"

criar sessão

Controla a ansiedade com que uma sessão HTTP é criada.

Se não for definido, o padrão é "ifRequired". Outras opções são "sempre" e "nunca".

A configuração desse atributo afeta as propriedades allowSessionCreation e forceEagerSessionCreation de HttpSessionContextIntegrationFilter. allowSessionCreation sempre será true, a menos que este atributo seja definido como "never". forceEagerSessionCreation é "false", a menos que seja definido como "always".

Portanto, a configuração padrão permite a criação de sessão, mas não a força. A exceção é se o controle de sessão simultânea estiver habilitado, quando forceEagerSessionCreation será definido como verdadeiro, independentemente da configuração aqui. Usar "nunca" causaria uma exceção durante a inicialização de HttpSessionContextIntegrationFilter.

Para obter detalhes específicos sobre o uso da sessão, há uma boa documentação no javadoc HttpSessionSecurityContextRepository.

Jon Vaughan
fonte
Todas essas são ótimas respostas, mas tenho batido minha cabeça contra a parede tentando descobrir como fazer isso ao usar o elemento de configuração <http>. Mesmo assim auto-config=false, aparentemente você não pode substituir o que está na SECURITY_CONTEXT_FILTERposição pelo seu. Tenho tentado desabilitá-lo com algum ApplicationContextAwarebean (usando reflexão para forçar o securityContextRepositorya uma implementação nula SessionManagementFilter), mas sem dados. E, infelizmente, não posso mudar para o spring-security 3.1 ano que forneceria create-session=stateless.
Jeff Evans,
Visite este site, sempre informativo. Espero que isso ajude você e outros também " baeldung.com/spring-security-session " • sempre - uma sessão será sempre criada, se ainda não existir • ifRequired - uma sessão será criada apenas se necessário (padrão) • nunca - o framework nunca criará uma sessão por si só, mas usará uma se já existir • sem estado - nenhuma sessão será criada ou usada por Spring Security
Java_Fire_Within