spring-mvcspring-bootspring-securitytomcat7session-replication

Tomcat 7 clustering - session replication not working with spring boot and spring security


I have a problem regarding our Tomcat session replication configuration.

At our company, we are using Tomcat 7 servlet containers behind Apache HTTPD 2.4.6 configured for load balancing with mod_jk/tomcat-connectors 1.2.37 (running on CentOS 7 x64). Session replication is working with the Tomcat Manager, meaning if we kill one of our Tomcats after logging in to the HTML manager, we don't need to log in again (we have set it to be <distributable /> in the web.xml. We can see the servers are successfully discovering each other from the catalina.log:

2017.10.06 09:25:15 [INFO] org.apache.catalina.ha.tcp.SimpleTcpCluster memberAdded: Replication member added:org.apache.catalina.tribes.membership.StaticMember[tcp://10.35.217.77:4444,10.35.217.77,4444, alive=0, securePort=-1, UDP Port=-1, id={1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 }, payload={}, command={}, domain={100 101 108 116 97 45 115 116 97 ...(12)}, ]
2017.10.06 09:25:15 [INFO] org.apache.catalina.tribes.group.interceptors.TcpFailureDetector performBasicCheck: Suspect member, confirmed alive.[org.apache.catalina.tribes.membership.StaticMember[tcp://10.35.217.77:4444,10.35.217.77,4444, alive=0, securePort=-1, UDP Port=-1, id={1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 }, payload={}, command={}, domain={100 101 108 116 97 45 115 116 97 ...(12)}, ]]

However, when we deploy a sample Spring Boot application, and kill the Tomcat in which we authenticated a user, the login prompt appears again. We have been trying to resolve the issue for a long time and it's driving us crazy. What are we missing? The configuration is as follows:

Tomcat/context.xml:

<Context>
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <Manager className="org.apache.catalina.ha.session.DeltaManager"
         expireSessionsOnShutdown="false" 
         notifyListenersOnReplication="true" />
    <ResourceLink name="jdbc/postgres" global="jdbc/postgres"
         type="javax.sql.DataSource" />
</Context>

Tomcat/server.xml:

<Server port="8005" shutdown="SHUTDOWN" address="10.35.217.77">
    <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
    <Listener className="org.apache.catalina.core.JasperListener" />
    <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
    <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
    <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
    <GlobalNamingResources>
        <Resource name="UserDatabase" auth="Container"
            type="org.apache.catalina.UserDatabase"
            description="User database that can be updated and saved"
            factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
            pathname="conf/tomcat-users.xml" />
        <Resource name="jdbc/postgres" auth="Container" type="javax.sql.DataSource"
            username="postgres" password=""
            url="jdbc:postgresql://127.0.0.1:5432/postgres"
            driverClassName="org.postgresql.Driver"
            initialSize="5" maxWait="5000"
            maxActive="120" maxIdle="5"
            validationQuery="select 1"
            poolPreparedStatements="true".
            factory="org.apache.commons.dbcp.BasicDataSourceFactory" />
    </GlobalNamingResources>
    <Service name="Catalina">
        <Connector address="0.0.0.0" port="8080" protocol="HTTP/1.1" 
            connectionTimeout="30000" redirectPort="8443" 
            enableLookups="false" maxPostSize="20000"
            executor="tcThreadPool" />
        <Connector address="10.35.217.77" port="8009" protocol="AJP/1.3"
            redirectPort="8443" />
        <Engine name="Catalina" defaultHost="cluster" jvmRoute="node1">
            <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"
                channelSendOptions="6" channelStartOptions="3">
                <Channel className="org.apache.catalina.tribes.group.GroupChannel">
                    <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
                       autoBind="9" selectorTimeout="5000" maxThreads="6"
                       address="10.35.217.77" port="4444" />
                    <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
                        <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender" />
                    </Sender>
                    <Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpPingInterceptor" />
                    <Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector" />
                    <Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor" />
                    <Interceptor className="org.apache.catalina.tribes.group.interceptors.StaticMembershipInterceptor">
                        <Member className="org.apache.catalina.tribes.membership.StaticMember" securePort="-1"
                            host="10.35.217.79" port="4444" domain="delta-static" 
                            uniqueId="{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}" />
                    </Interceptor>
                </Channel>
                <Valve className="org.apache.catalina.ha.tcp.ReplicationValve" filter=".*\.gif;.*\.jpg;.*\.png;.*\.css" />
                <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve" />
                <ClusterListener className="org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener" />
                <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener" />
            </Cluster>
            <Realm className="org.apache.catalina.realm.LockOutRealm">
                <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
                   resourceName="UserDatabase"/>
            </Realm>
            <Host name="cluster"  appBase="webapps"
                unpackWARs="true" autoDeploy="true">
                <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
                    prefix="localhost_access_log." suffix="log" rotatable="false"
                    pattern="%h %l %u %t &quot;%r&quot; %s %b" />
            </Host>
        </Engine>
    </Service>
</Server>

The other server.xml is similar, with the .77 and .79 IP adresses being switched (and of course the uniqueId is changed).

SpringBootApp/WebSecurityConfig.java

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    private static final Logger logger = Logger.getLogger(WebSecurityConfig.class.getName());

    @Bean
    public UserDetailsContextMapperImpl contextMapper() {
        return new UserDetailsContextMapperImpl();
    }

    @Autowired
    PreferenceDao preferenceDao;

    @Bean
    public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
        ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider(
                preferenceDao.findByKey("ldap.domain").getValue(),
                preferenceDao.findByKey("ldap.host").getValue()
        );
        logger.info("Connected to LDAP.");
        provider.setConvertSubErrorCodesToExceptions(true);
        provider.setUseAuthenticationRequestCredentials(true);
        provider.setSearchFilter("(sAMAccountName={0})");
        provider.setUserDetailsContextMapper(contextMapper());
        return provider;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         auth.inMemoryAuthentication()
         .withUser("user")
         .password("s3cr3t")
         .roles("USER");
    }
    protected void configure(HttpSecurity http) throws Exception {
        http.
                authorizeRequests()
                .antMatchers("/css/**", "/js/**", "/img/**", "/font/**", "/", "/login?logout").permitAll()
                .anyRequest().hasAuthority("ROLE_USER")
                .and()
                .formLogin().loginPage("/login").permitAll()
                .and()
                .logout().logoutSuccessUrl("/login?logout").permitAll();
    }

    protected class UserDetailsContextMapperImpl implements UserDetailsContextMapper, Serializable {
        @Override
        public UserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection<? extends GrantedAuthority> authorities) {
            List<GrantedAuthority> mappedAuthorities = new ArrayList<>();
            mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER"));
            return new User(username, "", true, true, true, true, mappedAuthorities);
        }

        @Override
        public void mapUserToContext(UserDetails user, DirContextAdapter ctx) {
            //do nothing
        }
    }
}

Any help would be greatly appreciated.


Solution

  • Seems like I've finally solved it. One of the machines in the cluster was running a previous version of Tomcat. After I've upgraded it from v7.0.54 to v7.0.69 (also updating java), everything works fine.