I've been working with Spring Security for two weeks now and it's working well except for anonymous users and session timeouts.
Use Case #1
- An anonymous user can visit the site and view public pages (/home, /about, /signup, etc.) for as long as they want (no session timeouts).
- If the user selects a protected page the login screen appears.
Use Case #2
A registered user logs in and can view protected pages.
If their session times out, the invalidSessionUrl page is displayed and the user is directed to log in.
I've read a ton of SO and blog posts but I can't seem to find the solution to Use Case #1. I'm using Spring 4.1.6.RELEASE, Spring Security 4.0.2 RELEASE and Tomcat8.
Here is my security config:
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home", "/about", "/login**", "/thankyou", "/user/signup**", "/errors/**").permitAll()
.regexMatchers("/user/signup/.*").permitAll()
.antMatchers("/recipe/listRecipes*").hasAuthority("GUEST")
.antMatchers("/recipe/addRecipe*").hasAuthority("AUTHOR")
.antMatchers("/admin/**").hasAuthority("ADMIN")
.anyRequest().authenticated()
.expressionHandler(secExpressionHandler())
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login?err=1")
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/thankyou")
.deleteCookies( "JSESSIONID" )
.invalidateHttpSession(false)
.and()
.exceptionHandling()
.accessDeniedPage("/errors/403")
.and()
.rememberMe()
.key("recipeOrganizer")
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(960) //.tokenValiditySeconds(1209600)
.rememberMeParameter("rememberMe");
http
.sessionManagement()
.sessionAuthenticationErrorUrl("/errors/402")
.invalidSessionUrl("/errors/invalidSession")
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.expiredUrl("/errors/expiredSession")
.and()
.sessionFixation().migrateSession();
Any action by an anonymous user after the session expires results in an invalid session exception. The problem is in the security filter chain (see log below) the AnonymousAuthenticationFilter
(#11) creates a new session, but the SessionManagementFilter
(#12) retrieves the prior expired session, compares it to the new one and throws the invalidsession
exception. I want this to happen for logged in users, but not for anonymous users. However, at the time the exception is thrown the security context has been destroyed so I can't know whether the prior session was from an anonymous or logged in user.
Solutions I've considered are:
- Set the global session timeout to 24 hours or more then adjust the timeout for registered users in a LoginSuccessHandler and RememberMeSuccessHandler.
- Turn off security for the public pages.
- Create a separate cookie to indicate the type of user (anon vs. logged in), and query that in the InvalidSessionStrategy to redirect anon users to the /home page.
- Create a custom filter that is executed first to identify an anon user (possible?) and simply extend the current session.
- Write some javascript or jquery to periodically check the session timeout and reset it for anon users.
Solution #1 may result in issues with expired sessions on the server (not sure if this is a big deal). #2 doesn't feel right to me, especially if a public page may include any info about a logged in user. #3 might work except that the anon user could click on a public link other than the home page but get redirected to the home page because I don't think there's a way to tell in the InvalidSessionStrategy which link they were trying to access? I'm not sure if #4 will work or not - haven't tried it yet. #5 might also work but it increases network traffic?
I'm hoping that someone can point me to a practical solution. This has to be something that many sites deal with but I'm going around in circles trying to solve. Thanks in advance for any advice or tips.
Here's a portion of a log to illustrate what happens. I've set the session timeout to 60 seconds for testing purposes.
Initial access to website
20:49:29.823 DEBUG: org.springframework.security.web.FilterChainProxy - / at position 11 of 14 in additional filter chain; firing Filter: 'AnonymousAuthenticationFilter'
20:49:29.839 DEBUG: org.springframework.security.web.authentication.AnonymousAuthenticationFilter - Populated SecurityContextHolder with anonymous token: 'org.springframework.security.authentication.AnonymousAuthenticationToken@9055c2bc: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@b364: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS'
20:49:29.839 DEBUG: org.springframework.security.web.FilterChainProxy - / at position 12 of 14 in additional filter chain; firing Filter: 'SessionManagementFilter'
20:49:29.839 DEBUG: org.springframework.security.web.FilterChainProxy - / at position 13 of 14 in additional filter chain; firing Filter: 'ExceptionTranslationFilter'
20:49:29.839 DEBUG: org.springframework.security.web.FilterChainProxy - / at position 14 of 14 in additional filter chain; firing Filter: 'FilterSecurityInterceptor'
20:49:29.839 DEBUG: org.springframework.security.web.util.matcher.AntPathRequestMatcher - Checking match of request : '/'; against '/'
20:49:29.839 DEBUG: org.springframework.security.web.access.intercept.FilterSecurityInterceptor - Secure object: FilterInvocation: URL: /; Attributes: [permitAll]
20:49:29.854 DEBUG: org.springframework.security.web.access.intercept.FilterSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken@9055c2bc: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@b364: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS
20:49:29.871 DEBUG: org.springframework.security.access.vote.AffirmativeBased - Voter: org.springframework.security.web.access.expression.WebExpressionVoter@ff577, returned: 1
20:49:29.871 DEBUG: org.springframework.security.web.access.intercept.FilterSecurityInterceptor - Authorization successful
20:49:30.136 DEBUG: net.mycompany.myapp.config.SessionListener - AuthenticationSuccess - principal: anonymousUser
20:49:30.167 DEBUG: net.mycompany.myapp.config.SessionListener - AuthenticationSuccess - authority contains: ROLE_ANONYMOUS
20:49:30.167 INFO : net.mycompany.myapp.config.SessionListener - sessionCreated: Session created on: Mon Sep 21 20:49:30 CDT 2015
20:49:30.167 INFO : net.mycompany.myapp.config.SessionListener - sessionCreated: Session last accessed on: Mon Sep 21 20:49:30 CDT 2015
20:49:30.167 INFO : net.mycompany.myapp.config.SessionListener - sessionCreated: Session expires after: 60 seconds
20:49:30.167 INFO : net.mycompany.myapp.config.SessionListener - sessionCreated: Session ID: CA34FE74B56B8EF94181B1231A7D4FF6
Session times out after 60 seconds
20:50:46.015 INFO : net.mycompany.myapp.config.SessionDestroyedListener - destroyedEvent
20:50:46.015 INFO : net.mycompany.myapp.config.SessionDestroyedListener - object:org.springframework.security.web.session.HttpSessionDestroyedEvent[source=org.apache.catalina.session.StandardSessionFacade@17827f3e]
20:50:46.015 INFO : net.mycompany.myapp.config.SessionListener - sessionDestroyed: Session created on: Mon Sep 21 20:49:30 CDT 2015
20:50:46.015 INFO : net.mycompany.myapp.config.SessionListener - sessionDestroyed: Session last accessed on: Mon Sep 21 20:49:32 CDT 2015
20:50:46.015 INFO : net.mycompany.myapp.config.SessionListener - sessionDestroyed: Session expires after: 60 seconds
20:50:46.015 INFO : net.mycompany.myapp.config.SessionListener - sessionDestroyed: Session ID: CA34FE74B56B8EF94181B1231A7D4FF6
Click on "/home" link (unsecured)
20:50:53.927 DEBUG: org.springframework.security.web.util.matcher.AntPathRequestMatcher - Checking match of request : '/home'; against '/resources/**'
20:50:53.927 DEBUG: org.springframework.security.web.FilterChainProxy - /home at position 1 of 14 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
20:50:53.927 DEBUG: org.springframework.security.web.FilterChainProxy - /home at position 2 of 14 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
20:50:53.927 DEBUG: org.springframework.security.web.context.HttpSessionSecurityContextRepository - No HttpSession currently exists
20:50:53.927 DEBUG: org.springframework.security.web.context.HttpSessionSecurityContextRepository - No SecurityContext was available from the HttpSession: null. A new one will be created.
20:50:53.927 DEBUG: org.springframework.security.web.FilterChainProxy - /home at position 3 of 14 in additional filter chain; firing Filter: 'HeaderWriterFilter'
20:50:53.927 DEBUG: org.springframework.security.web.header.writers.HstsHeaderWriter - Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@4c668dec
20:50:53.927 DEBUG: org.springframework.security.web.FilterChainProxy - /home at position 4 of 14 in additional filter chain; firing Filter: 'CsrfFilter'
20:50:53.927 DEBUG: org.springframework.security.web.FilterChainProxy - /home at position 5 of 14 in additional filter chain; firing Filter: 'LogoutFilter'
20:50:53.927 DEBUG: org.springframework.security.web.util.matcher.AntPathRequestMatcher - Request 'GET /home' doesn't match 'POST /logout
20:50:53.927 DEBUG: org.springframework.security.web.FilterChainProxy - /home at position 6 of 14 in additional filter chain; firing Filter: 'UsernamePasswordAuthenticationFilter'
20:50:53.927 DEBUG: org.springframework.security.web.util.matcher.AntPathRequestMatcher - Request 'GET /home' doesn't match 'POST /login
20:50:53.927 DEBUG: org.springframework.security.web.FilterChainProxy - /home at position 7 of 14 in additional filter chain; firing Filter: 'ConcurrentSessionFilter'
20:50:53.927 DEBUG: org.springframework.security.web.FilterChainProxy - /home at position 8 of 14 in additional filter chain; firing Filter: 'RequestCacheAwareFilter'
20:50:53.927 DEBUG: org.springframework.security.web.FilterChainProxy - /home at position 9 of 14 in additional filter chain; firing Filter: 'SecurityContextHolderAwareRequestFilter'
20:50:53.927 DEBUG: org.springframework.security.web.FilterChainProxy - /home at position 10 of 14 in additional filter chain; firing Filter: 'RememberMeAuthenticationFilter'
20:50:53.927 DEBUG: org.springframework.security.web.FilterChainProxy - /home at position 11 of 14 in additional filter chain; firing Filter: 'AnonymousAuthenticationFilter'
20:50:53.927 DEBUG: org.springframework.security.web.authentication.AnonymousAuthenticationFilter - Populated SecurityContextHolder with anonymous token: 'org.springframework.security.authentication.AnonymousAuthenticationToken@9055c2bc: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@b364: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS'
20:50:53.927 DEBUG: org.springframework.security.web.FilterChainProxy - /home at position 12 of 14 in additional filter chain; firing Filter: 'SessionManagementFilter'
20:50:53.927 DEBUG: org.springframework.security.web.session.SessionManagementFilter - Requested session ID CA34FE74B56B8EF94181B1231A7D4FF6 is invalid.
20:50:53.927 DEBUG: org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy - Starting new session (if required) and redirecting to '/errors/invalidSession'
20:50:53.927 DEBUG: net.mycompany.myapp.config.SessionListener - AuthenticationSuccess - principal: anonymousUser
20:50:53.927 DEBUG: net.mycompany.myapp.config.SessionListener - AuthenticationSuccess - authority contains: ROLE_ANONYMOUS
20:50:53.927 INFO : net.mycompany.myapp.config.SessionListener - sessionCreated: Session created on: Mon Sep 21 20:50:53 CDT 2015
20:50:53.927 INFO : net.mycompany.myapp.config.SessionListener - sessionCreated: Session last accessed on: Mon Sep 21 20:50:53 CDT 2015
20:50:53.942 INFO : net.mycompany.myapp.config.SessionListener - sessionCreated: Session expires after: 60 seconds
20:50:53.942 INFO : net.mycompany.myapp.config.SessionListener - sessionCreated: Session ID: 6F7FD6230BC075B7768648BBBC08E3F4
20:50:53.942 DEBUG: org.springframework.security.web.session.HttpSessionEventPublisher - Publishing event: org.springframework.security.web.session.HttpSessionCreatedEvent[source=org.apache.catalina.session.StandardSessionFacade@412cbe09]
20:50:53.942 DEBUG: org.springframework.security.web.DefaultRedirectStrategy - Redirecting to '/myapp/errors/invalidSession'