Multiple login pages with Spring Security

I ran into this problem with spring security recently. My application has two different sides, administrator side and user side. I want them to be completely separated with their own login pages and all. I'm writing this tutorial since I couldn't find comprehensive instructions on how to do it anywhere. Also note that this tutorial has been written for Spring Security 2.0.

First I had to define the springSecurityFilterChain in my security.xml and define the filter chains for the url patterns for /user/** and /admin/**. The list of filters is quite long so I'm going to skip the uninteresting ones but you can see the whole configuration in the sources.

<bean id="springSecurityFilterChain" class="org.springframework.security.util.FilterChainProxy">
        <security:filter-chain-map path-type="ant">
            <security:filter-chain pattern="/user/**"
                                   filters="httpSessionContextIntegrationFilter, anonymousProcessingFilter, userLogoutFilter,
                   userAuthenticationProcessingFilter, securityContextHolderAwareRequestFilter,
                   userExceptionTranslationFilter, sessionFixationProtectionFilter,
                   userFilterSecurityInterceptor"/>
            <security:filter-chain pattern="/admin/**"
                                   filters="httpSessionContextIntegrationFilter, anonymousProcessingFilter, adminLogoutFilter,
                   adminAuthenticationProcessingFilter, securityContextHolderAwareRequestFilter,
                   adminExceptionTranslationFilter, sessionFixationProtectionFilter,
                   adminFilterSecurityInterceptor"/>
            <security:filter-chain pattern="/**"
                                   filters="httpSessionContextIntegrationFilter, anonymousProcessingFilter, securityContextHolderAwareRequestFilter"/>
        </security:filter-chain-map>
    </bean>

Then we have the admin specific part of the configuration. Here we define the login and logout urls and filters for the admin side of the application.

<bean id="adminAuthenticationProcessingFilter"
          class="org.springframework.security.ui.webapp.AuthenticationProcessingFilter">
        <property name="invalidateSessionOnSuccessfulAuthentication" value="true"/>
        <property name="authenticationManager" ref="_authenticationManager"/>
        <property name="authenticationFailureUrl" value="/admin/login.htm?error=1"/>
        <property name="defaultTargetUrl" value="/admin/index.htm"/>
        <property name="filterProcessesUrl" value="/admin/j_spring_security_check"/>
    </bean>

    <bean id="adminLogoutFilter" class="org.springframework.security.ui.logout.LogoutFilter">
        <constructor-arg value="/"/>
        <constructor-arg>
            <list>
                <bean class="org.springframework.security.ui.logout.SecurityContextLogoutHandler"/>
            </list>
        </constructor-arg>
        <property name="filterProcessesUrl" value="/admin/j_spring_security_logout"/>
    </bean>

    <bean id="adminExceptionTranslationFilter" class="org.springframework.security.ui.ExceptionTranslationFilter">
        <property name="authenticationEntryPoint">
            <bean class="org.springframework.security.ui.webapp.AuthenticationProcessingFilterEntryPoint">
                <property name="loginFormUrl" value="/admin/login.htm"/>
                <property name="forceHttps" value="false"/>
            </bean>
        </property>
        <property name="accessDeniedHandler" ref="accessDeniedHandler"/>
    </bean>

    <bean id="adminFilterSecurityInterceptor"
          class="org.springframework.security.intercept.web.FilterSecurityInterceptor">
        <property name="accessDecisionManager" ref="accessDecisionManager"/>
        <property name="authenticationManager" ref="_authenticationManager"/>
        <property name="objectDefinitionSource" ref="adminInterceptUrlDefinition"/>
    </bean>

Then the user side is just a copy of the admin configuration.

<bean id="userAuthenticationProcessingFilter"
          class="org.springframework.security.ui.webapp.AuthenticationProcessingFilter">
        <property name="invalidateSessionOnSuccessfulAuthentication" value="true"/>
        <property name="authenticationManager" ref="_authenticationManager"/>
        <property name="authenticationFailureUrl" value="/user/login.htm?error=1"/>
        <property name="defaultTargetUrl" value="/user/index.htm"/>
        <property name="filterProcessesUrl" value="/user/j_spring_security_check"/>
    </bean>

    <bean id="userLogoutFilter" class="org.springframework.security.ui.logout.LogoutFilter">
        <constructor-arg value="/"/>
        <constructor-arg>
            <list>
                <bean class="org.springframework.security.ui.logout.SecurityContextLogoutHandler"/>
            </list>
        </constructor-arg>
        <property name="filterProcessesUrl" value="/user/j_spring_security_logout"/>
    </bean>

    <bean id="userExceptionTranslationFilter" class="org.springframework.security.ui.ExceptionTranslationFilter">
        <property name="authenticationEntryPoint">
            <bean class="org.springframework.security.ui.webapp.AuthenticationProcessingFilterEntryPoint">
                <property name="loginFormUrl" value="/user/login.htm"/>
                <property name="forceHttps" value="false"/>
            </bean>
        </property>
        <property name="accessDeniedHandler" ref="accessDeniedHandler"/>
    </bean>

    <bean id="userFilterSecurityInterceptor"
          class="org.springframework.security.intercept.web.FilterSecurityInterceptor">
        <property name="accessDecisionManager" ref="accessDecisionManager"/>
        <property name="authenticationManager" ref="_authenticationManager"/>
        <property name="objectDefinitionSource" ref="userInterceptUrlDefinition"/>
    </bean>

And then we have the urls mapped to the required roles and the users used in this tutorial.

<security:filter-invocation-definition-source id="adminInterceptUrlDefinition">
        <security:intercept-url pattern="/admin/login*" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
        <security:intercept-url pattern="/admin/logout*" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
        <security:intercept-url pattern="/admin/**" access="ROLE_ADMINISTRATOR"/>
    </security:filter-invocation-definition-source>

    <security:filter-invocation-definition-source id="userInterceptUrlDefinition">
        <security:intercept-url pattern="/user/login*" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
        <security:intercept-url pattern="/user/logout*" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
        <security:intercept-url pattern="/user/**" access="ROLE_USER"/>
    </security:filter-invocation-definition-source>

    <security:authentication-provider>
        <security:user-service>
          <security:user name="jimi" password="password" authorities="ROLE_USER, ROLE_ADMIN" />
          <security:user name="bob" password="password" authorities="ROLE_USER" />
        </security:user-service>
    </security:authentication-provider>

You can download the sources from here. To run the sample using the maven jetty plugin use:
mvn org.mortbay.jetty:maven-jetty-plugin:6.1.26RC0:run

4 comments:

  1. Hi..

    I'm very interested in this article..

    Just wondering, do you have the same solution for Spring Security 3?

    Thanks
    Julius

    ReplyDelete
  2. Sorry for the late reply.

    I haven't tried this approach with Spring Security 3, but I'd imagine you could use something similar.

    ReplyDelete
  3. Did you found solution with Spring 3.0.x?

    ReplyDelete
  4. Sorry, I haven't had time to try it out with Spring 3.0. I might try it out and post my findings.

    ReplyDelete