109

I am building a web application with Spring Security that will live on Amazon EC2 and use Amazon's Elastic Load Balancers. Unfortunately, ELB does not support sticky sessions, so I need to ensure my application works properly without sessions.

So far, I have setup RememberMeServices to assign a token via a cookie, and this works fine, but I want the cookie to expire with the browser session (e.g. when the browser closes).

I have to imagine I'm not the first one to want to use Spring Security without sessions... any suggestions?

Jarrod Carlson
  • 1,967
  • 4
  • 16
  • 20

8 Answers8

137

In Spring Security 3 with Java Config, you can use HttpSecurity.sessionManagement():

@Override
protected void configure(final HttpSecurity http) throws Exception {
    http
        .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
Ben Hutchison
  • 4,823
  • 4
  • 26
  • 25
  • 2
    This is the correct answer for Java config, mirroring what @sappenin correctly stated for xml config in a comment on the accepted answer. We use this method and indeed our application is sessionless. – Paul Jul 28 '14 at 15:58
  • This has a side effect. The Tomcat container will append ";jsessionid=..." to requests for images, stylesheets, etc, because Tomcat doesn't like to be stateless, and Spring Security will then block these assets on the first load because "the URL contained a potentially malicious String ';'". – workerjoe Mar 05 '20 at 17:36
  • @workerjoe So, what you are trying to say by this java configuration, the sessions are not created by spring security rather tomcat? – Vishwas Atrey May 08 '20 at 20:50
  • @VishwasAtrey In my understanding (which may be wrong), Tomcat creates and maintains the sessions. Spring takes advantage of them, adding its own data. I tried to make a stateless web application and it didn't work, as I mentioned above. See [this answer to my own question](https://stackoverflow.com/a/60290177/3938965) for more. – workerjoe May 08 '20 at 23:38
28

It seems to be even easier in Spring Securitiy 3.0. If you're using namespace configuration, you can simply do as follows:

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

Or you could configure the SecurityContextRepository as null, and nothing would ever get saved that way as well.

yetAnotherSE
  • 3,178
  • 6
  • 26
  • 33
Jarrod Carlson
  • 1,967
  • 4
  • 16
  • 20
  • 5
    This didn't work as I thought it would. Instead, there's a comment below that distinguishes between "never" and "stateless". Using "never", my app was still creating sessions. Using "stateless", my app actually went stateless, and I didn't need to implement any of the overrides mentioned in other answers. See the JIRA issue here: https://jira.springsource.org/browse/SEC-1424 – sappenin Jan 19 '13 at 23:06
28

We worked on the same issue (injecting a custom SecurityContextRepository to SecurityContextPersistenceFilter) for 4-5 hours today. Finally, we figured it out. First of all, in the section 8.3 of Spring Security ref. doc, there is a SecurityContextPersistenceFilter bean definition

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

And after this definition, there is this explanation: "Alternatively you could provide a null implementation of the SecurityContextRepository interface, which will prevent the security context from being stored, even if a session has already been created during the request."

We needed to inject our custom SecurityContextRepository into the SecurityContextPersistenceFilter. So we simply changed the bean definition above with our custom impl and put it into the security context.

When we run the application, we traced the logs and saw that SecurityContextPersistenceFilter was not using our custom impl, it was using the HttpSessionSecurityContextRepository.

After a few other things we tried, we figured out that we had to give our custom SecurityContextRepository impl with the "security-context-repository-ref" attribute of "http" namespace. If you use "http" namespace and want to inject your own SecurityContextRepository impl, try "security-context-repository-ref" attribute.

When "http" namespace is used, a seperate SecurityContextPersistenceFilter definition is ignored. As I copied above, the reference doc. does not state that.

Please correct me if I misunderstood the things.

Basri Kahveci
  • 391
  • 3
  • 7
  • Thanks, this is valuable information. I will try it out in my application. – Jeff Evans Oct 07 '11 at 20:03
  • Thanks, that's what I needed with spring 3.0 – Justin Ludwig Oct 12 '11 at 03:19
  • 1
    You are quite accurate when you say that the http namespace doesn't allow for a custom SecurityContextPersistenceFilter, it took me a couple of hours of debugging to figure it out – Jaime Hablutzel Feb 14 '12 at 23:33
  • Thank you very much for posting this! I was about to tear out what little hair I have. I was wondering why the setSecurityContextRepository method of SecurityContextPersistenceFilter was deprecated (the docs saying to use constructor injection, which is not right either). – fool4jesus Jul 03 '13 at 12:58
11

Take a look at SecurityContextPersistenceFilter class. It defines how the SecurityContextHolder is populated. By default it uses HttpSessionSecurityContextRepository to store security context in http session.

I have implemented this mechanism quite easily, with custom SecurityContextRepository.

See the securityContext.xml below:

<?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
  • 186
  • 1
  • 4
  • 1
    Hi Lukas, can you give any more details of your security context repository implementation? – Jim Downing Oct 07 '10 at 14:18
  • 1
    class TokenSecurityContextRepository contains HashMap contextMap. In loadContext() method checks whether exists SecurityContext for session hash code passed by either requestParameter sid, or cookie, or custom requestHeader or combination of any above. Returns SecurityContextHolder.createEmptyContext() if context could not be resolved. Method saveContext puts resolved context to contextMap. – Lukas Herman Nov 11 '10 at 09:47
8

Actually create-session="never" doesn't mean being completely stateless. There's an issue for that in Spring Security issue management.

hleinone
  • 4,470
  • 4
  • 35
  • 49
4

EDIT: As of Spring Security 3.1, there is a STATELESS option that can be used instead of all this. See the other answers. Original answer kept below for posterity.

After struggling with the numerous solutions posted in this answer, to try to get something working when using the <http> namespace config, I finally found an approach that actually works for my use case. I don't actually require that Spring Security doesn't start a session (because I use session in other parts of the application), just that it doesn't "remember" authentication in the session at all (it should be re-checked every request).

To begin with, I wasn't able to figure out how to do the "null implementation" technique described above. It wasn't clear whether you are supposed to set the securityContextRepository to null or to a no-op implementation. The former does not work because a NullPointerException gets thrown within SecurityContextPersistenceFilter.doFilter(). As for the no-op implementation, I tried implementing in the simplest way I could imagine:

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

}

This doesn't work in my application, because of some strange ClassCastException having to do with the response_ type.

Even assuming I did manage to find an implementation that works (by simply not storing the context in session), there is still the problem of how to inject that into the filters built by the <http> configuration. You cannot simply replace the filter at the SECURITY_CONTEXT_FILTER position, as per the docs. The only way I found to hook into the SecurityContextPersistenceFilter that is created under the covers was to write an ugly ApplicationContextAware bean:

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

            }
        }
    }
}

Anyway, to the solution that actually does work, albeit very hackish. Simply use a Filter that deletes the session entry that the HttpSessionSecurityContextRepository looks for when it does its thing:

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

Then in the configuration:

<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
  • 1,257
  • 1
  • 15
  • 31
  • 1
    Nine years later, this is still the right answer. Now we can use Java configuration instead of XML. I added the custom filter in my `WebSecurityConfigurerAdapter` with "`http.addFilterBefore(new SpringSecuritySessionDeletingFilter(), SecurityContextPersistenceFilter.class)`" – workerjoe Mar 05 '20 at 18:17
  • If you use SessionCreationPolicy.STATELESS as described in another answer, this shouldn't be necessary. You must have something else going on. – rougou Apr 20 '21 at 02:53
  • `STATELESS` appears to have been added in 3.1. At the time this answer was written, the latest released version was 3.0. So that explains it. – Jeff Evans Apr 20 '21 at 20:48
  • Thanks @JeffEvans, SpringSecuritySessionDeletingFilter save me a lot of time. I had a problem when in some cases i need stateless behavior, while in another cases not – Volatile Jan 13 '22 at 12:52
3

Just a quick note: it's "create-session" rather than "create-sessions"

create-session

Controls the eagerness with which an HTTP session is created.

If not set, defaults to "ifRequired". Other options are "always" and "never".

The setting of this attribute affect the allowSessionCreation and forceEagerSessionCreation properties of HttpSessionContextIntegrationFilter. allowSessionCreation will always be true unless this attribute is set to "never". forceEagerSessionCreation is "false" unless it is set to "always".

So the default configuration allows session creation but does not force it. The exception is if concurrent session control is enabled, when forceEagerSessionCreation will be set to true, regardless of what the setting is here. Using "never" would then cause an exception during the initialization of HttpSessionContextIntegrationFilter.

For specific details of the session usage, there is some good documentation in the HttpSessionSecurityContextRepository javadoc.

yetAnotherSE
  • 3,178
  • 6
  • 26
  • 33
Jon Vaughan
  • 2,058
  • 1
  • 18
  • 11
  • These are all great answers, but I've been beating my head against the wall trying to figure out how to achieve this when using the config element. Even with `auto-config=false`, you apparently can't replace what's in the `SECURITY_CONTEXT_FILTER` position with your own. I've been hacking around trying to disable it with some `ApplicationContextAware` bean (using reflection to force the `securityContextRepository` to a null implementation in `SessionManagementFilter`) but no dice. And sadly, I can't switch to spring-security 3.1 year which would provide `create-session=stateless`. – Jeff Evans Oct 01 '11 at 15:23
  • Please visit this site, always informative. Hope this helps you and other as well "http://www.baeldung.com/spring-security-session" • always – a session will always be created if one doesn’t already exist • ifRequired – a session will be created only if required (default) • never – the framework will never create a session itself but it will use one if it already exists • stateless – no session will be created or used by Spring Security – Java_Fire_Within Sep 19 '16 at 00:08
0

Now ELB supports sticky sessions, I think from 2016. But also it's possible to store your sessions in Redis.

  • 1
    This does not provide an answer to the question. Once you have sufficient [reputation](https://stackoverflow.com/help/whats-reputation) you will be able to [comment on any post](https://stackoverflow.com/help/privileges/comment); instead, [provide answers that don't require clarification from the asker](https://meta.stackexchange.com/questions/214173/why-do-i-need-50-reputation-to-comment-what-can-i-do-instead). - [From Review](/review/late-answers/29844424) – Subhashis Pandey Sep 17 '21 at 07:15