spring-mvcspring-securityignitedistributed-systemdistributed-cache

Apache Ignite Spring secutiry error


I'm trying to develop an application with spring security login functionality while user session is distributed using apache Ignite.

I have two errors in my application.

  1. Logging an exception when logging out from the application. Apart from that session invalidation is done as expected.

12-Aug-2017 14:09:01.580 SEVERE [http-nio-8080-exec-2] org.apache.ignite.logger.java.JavaLogger.error Failed to update web session: null java.lang.NullPointerException at org.apache.ignite.cache.websession.WebSessionFilter$RequestWrapperV2.getSession(WebSessionFilter.java:1001) at org.apache.ignite.cache.websession.WebSessionFilter.doFilterV2(WebSessionFilter.java:564) at org.apache.ignite.cache.websession.WebSessionFilter.doFilterDispatch(WebSessionFilter.java:407) at org.apache.ignite.cache.websession.WebSessionFilter.doFilter(WebSessionFilter.java:383) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:478) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:80) at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:624) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342) at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:799) at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:861) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1455) at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(Thread.java:748)

  1. When two tomcat servers with same application deployments started up in two different ports, can access login page only from one server. (If I accessed login page from first server page is loaded fine as expected. But if I tried to access login page from second server again it gives error. However once logged in application worked as expected and could access distributed session from both servers.
    1. First access

screenshot

  1. Second access from another server

screenshot

My configuration files are as follows.

  1. Spring context configuration

    <beans xmlns.... >
    <context:component-scan base-package="test.ignite.spring"/>
    <mvc:annotation-driven/>
    <context:property-placeholder location="classpath:system.properties" ignore-resource-not-found="true" ignore-unresolvable="true"/>
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/pages/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
    <mvc:interceptors>
        <mvc:interceptor>
            <!-- Cache of HTML pages -->
            <mvc:mapping path="/**"/>
            <bean class="org.springframework.web.servlet.mvc.WebContentInterceptor">
                <property name="cacheSeconds" value="0"/>
            </bean>
        </mvc:interceptor>
    </mvc:interceptors>
    </beans>
    
  2. Login Controller

    @Controller
    public class LoginController {
    
    @RequestMapping(value = {"login", "/"})
    public String login() {
    
        try {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            if (!(auth instanceof AnonymousAuthenticationToken)) {
                return "home";
            }
            return "login";
        } catch (Exception e) {
            return "redirect:/error/500";
        }
    }
    @RequestMapping(value = "/home")
    public String home() {
        return "home";
    }
    }
    

----UPDATED----

  1. Ignite configuration (whole file content)

       <beans xmlns.... >
       <bean abstract="true" id="ignite.cfg" class="org.apache.ignite.configuration.IgniteConfiguration">
        <!-- Set to true to enable distributed class loading for examples, default is false. -->
        <property name="peerClassLoadingEnabled" value="true"/>
    
        <property name="cacheConfiguration">
            <list>
                <bean class="org.apache.ignite.configuration.CacheConfiguration">
                    <property name="name" value="example"/>
                    <property name="cacheMode" value="PARTITIONED"/>
                </bean>
            </list>
        </property>
    
        <!-- Enable task execution events for examples. -->
        <property name="includeEventTypes">
            <list>
                <!--Task execution events-->
                <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_STARTED"/>
                <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_FINISHED"/>
                <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_FAILED"/>
                <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_TIMEDOUT"/>
                <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_SESSION_ATTR_SET"/>
                <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_REDUCED"/>
    
                <!--Cache events-->
                <util:constant static-field="org.apache.ignite.events.EventType.EVT_CACHE_OBJECT_PUT"/>
                <util:constant static-field="org.apache.ignite.events.EventType.EVT_CACHE_OBJECT_READ"/>
                <util:constant static-field="org.apache.ignite.events.EventType.EVT_CACHE_OBJECT_REMOVED"/>
            </list>
        </property>
    
        <!-- Explicitly configure TCP discovery SPI to provide list of initial nodes. -->
        <property name="discoverySpi">
            <bean class="org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi">
                <property name="ipFinder">
                    <!--
                        Ignite provides several options for automatic discovery that can be used
                        instead os static IP based discovery. For information on all options refer
                        to our documentation: http://apacheignite.readme.io/docs/cluster-config
                    -->
                    <!-- Uncomment static IP finder to enable static-based discovery of initial nodes. -->
                    <!--<bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder">-->
                    <bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.multicast.TcpDiscoveryMulticastIpFinder">
                        <property name="addresses">
                            <list>
                                <!-- In distributed environment, replace with actual host IP address. -->
                                <value>127.0.0.1:47500..47509</value>
                            </list>
                        </property>
                    </bean>
                </property>
            </bean>
        </property>
    
    </bean>
    

  2. Spring security configuration

    <beans:beans xmlns.... >
    <http auto-config="true" create-session="always" use-expressions="true" >
       <form-login login-page="/login" default-target-url="/home" authentication-failure-url="/login?error" username-parameter="username" password-parameter="password" always-use-default-target="true"/>
    
       <logout invalidate-session="true" logout-success-url="/login" delete-cookies="JSESSIONID"/>
    
       <session-management session-fixation-protection="newSession" invalid-session-url="/" session-authentication-error-url="/login">
           <concurrency-control session-registry-alias="sessionRegistry" max-sessions="10" expired-url="/" error-if-maximum-exceeded="true"/>
       </session-management>
    
       <access-denied-handler error-page="/403"/>
    </http>
    <authentication-manager>
       <authentication-provider user-service-ref="userDetailsService">
       </authentication-provider>
    </authentication-manager>
    </beans:beans>
    
  3. web.xml

    <web-app xmlns... >
    <listener>
        <listener-class>org.apache.ignite.startup.servlet.ServletContextListenerStartup</listener-class>
    </listener>
    
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <listener>
        <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
    </listener>
    
    <filter>
        <filter-name>IgniteWebSessionsFilter</filter-name>
        <filter-class>org.apache.ignite.cache.websession.WebSessionFilter</filter-class>
    </filter>
    
    <!-- You can also specify a custom URL pattern. -->
    <filter-mapping>
        <filter-name>IgniteWebSessionsFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    
    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    
    <!-- Specify Ignite configuration (relative to META-INF folder or Ignite_HOME). -->
    <context-param>
        <param-name>IgniteConfigurationFilePath</param-name>
        <param-value>example-ignite.xml</param-value>
    </context-param>
    
    <!-- Specify the name of Ignite cache for web sessions. -->
    <context-param>
        <param-name>IgniteWebSessionsCacheName</param-name>
        <param-value>example</param-value>
    </context-param>
    
    <!--SERVLETS-->
    <servlet>
        <servlet-name>mvc-dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:mvc-dispatcher-servlet.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>mvc-dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath:mvc-dispatcher-servlet.xml,
            classpath:security-config.xml
        </param-value>
    </context-param>
    </web-app>
    

I would be very thankful if you could provide any solution / idea to solve problem.


Solution

  • As both of your deployments are on localhost, they're going to share JSESSIONID cookie. So far so good.

    However there seems to be a mismatch between Tomcat, Spring Security and Ignite which causes Spring Security to regard sessions coming from Ignite as invalid when they're anonymous (i.e. not logged on). I don't understand yet how this relates to your case where you're supposed to already be logged on.

    You can probably use a workaround: Remove invalid-session-url="/" from your Spring Security configuration. This will prevent the redirect looping behavior. This will also cause users to be silently logged off instead to being guided to /login when their cookie expires.

    I've deleted my previous answer as it was missing the point.