Use multiple HttpSessionIdResolver with Spring

Tags: , ,



I want to use the HTTPSessionIdResolver for everything located under “/api**” and for everything else the standard CookieResolver.

How is this possible, so that the two configurations use different resolvers? With my current approach everything uses X-AUTH.

I tried to understand the implementation within Spring and I end up in the SessionRepositoryFilter, but of this filter only one instance is created, so der exists only one resolver.

    @EnableWebSecurity
    public class TestConfig {

    @EnableSpringHttpSession
    @Configuration
    @Order(1)
    public static class Abc extends WebSecurityConfigurerAdapter {

        @Bean
        @Primary
        public HeaderHttpSessionIdResolver xAuth() {
            return HeaderHttpSessionIdResolver.xAuthToken();
        }

        @Bean
        @Primary
        public MapSessionRepository mapSessionRepository(){
            return new MapSessionRepository(new HashMap<>());
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.antMatcher("/service/json/**")
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .httpBasic()
                .and()
                .csrf()
                .disable();
        }

    }

    @EnableSpringHttpSession
    @Configuration
    @Order(2)
    public static class WebSecurityConfig extends WebSecurityConfigurerAdapter {

        @ConfigurationProperties(prefix = "spring.datasource")
        @Bean
        @Primary
        public DataSource dataSource() {
            return DataSourceBuilder
                    .create()
                    .build();
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                .antMatchers("/css/**", "/user/registration", "/webfonts/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
                .logout()
                .permitAll();
        }

        @Bean
        public BCryptPasswordEncoder bcrypt() {
            return new BCryptPasswordEncoder();
        }

        @Bean
        public JdbcUserDetailsManager userDetailsManager() {
            JdbcUserDetailsManager manager = new UserDetailsManager(dataSource());
            manager.setUsersByUsernameQuery("select username,password,enabled from users where username=?");
            manager.setAuthoritiesByUsernameQuery("select username,authority from authorities where username = ?");
            return manager;
        }

        @Autowired
        public void initialize(AuthenticationManagerBuilder builder) throws Exception {
            builder.userDetailsService(userDetailsManager()).passwordEncoder(bcrypt());
        }
     }
  }

I could move the logic into one resolver which delegates the work to the existing resolvers, but this seems hacky?

public class SmartHttpSessionIdResolver implements HttpSessionIdResolver {

    private static final String HEADER_X_AUTH_TOKEN = "X-Auth-Token";
    private static final CookieHttpSessionIdResolver cookie = new CookieHttpSessionIdResolver();
    private static final HeaderHttpSessionIdResolver xauth = HeaderHttpSessionIdResolver.xAuthToken();

    @Override
    public List<String> resolveSessionIds(HttpServletRequest request) {
        if (isXAuth(request)) {
            return xauth.resolveSessionIds(request);
        }
        return cookie.resolveSessionIds(request);
    }

    @Override
    public void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) {
        if (isXAuth(request)) {
            xauth.setSessionId(request, response, sessionId);
        } else {
            cookie.setSessionId(request, response, sessionId);
        }
    }

    @Override
    public void expireSession(HttpServletRequest request, HttpServletResponse response) {
        if (isXAuth(request)) {
            xauth.expireSession(request, response);
        } else {
            cookie.expireSession(request, response);
        }
    }

    private boolean isXAuth(HttpServletRequest request) {
        return request.getHeader(HEADER_X_AUTH_TOKEN) != null;
    }
}

Answer

After attempting the solution provided in the question (which works fine, to be honest), I also attempted to do this by providing two different filters. However, when @EnableSpringHttpSession is added, a SessionRepositoryFilter is added automatically and adding two more of those in the servlet filter chain seems odd. Therefore, I thought they would have to go in the security filter chain instead, which is good because then we can use the URL matching made there as well (instead of having to implement that elsewhere as well).

Since other security filters use the HttpSession, we have to manually place the SessionRepositoryFilter first in this chain. This is what I came up with (in Kotlin) which works well for me:

@EnableWebSecurity
class SecurityConfig() {

    private val sessionStore = ConcurrentHashMap<String, Session>()
    private val sessionRepo = MapSessionRepository(sessionStore)

    @Configuration
    @Order(1)
    inner class XAuthConfig(): WebSecurityConfigurerAdapter() {

        override fun configure(http: HttpSecurity) {
            http
                .requestMatchers()
                    .antMatchers("/api**")
                    .and()
                .addFilterBefore(
                    SessionRepositoryFilter(sessionRepo).apply{
                        setHttpSessionIdResolver(
                            HeaderHttpSessionIdResolver.xAuthToken();
                        )
                    }, WebAsyncManagerIntegrationFilter::class.java)
        }
    }

    @Configuration
    @Order(2)
    inner class DefaultConfig(): WebSecurityConfigurerAdapter() {

        override fun configure(http: HttpSecurity) {
            http
                .addFilterBefore(
                    SessionRepositoryFilter(sessionRepo).apply{
                        setHttpSessionIdResolver(
                            CookieHttpSessionIdResolver()
                        )
                    }, WebAsyncManagerIntegrationFilter::class.java)
            }
        }
    }

}

Note that the annotation @EnableSpringHttpSession has been removed. Instead, we add the SessionRepositoryFilters manually before the WebAsyncManagerIntegrationFilters (the first filter in the security filter chain). The function of the SessionRepositoryFilter is to replace the existing HttpSession with Spring’s HttpSession which it will do no matter if we place it manually or if it’s put in place automatically by means of autoconfiguration. As long as no other filter before the security filter chain makes use of the session, this should work out. Otherwise, some filter rearrangement might still do the trick.



Source: stackoverflow