Quellcode durchsuchen

SEC-582: Namespace configuration implementation for remember-me support.

Luke Taylor vor 18 Jahren
Ursprung
Commit
9f2bc9a842

+ 16 - 1
core/src/main/java/org/springframework/security/config/ConfigUtils.java

@@ -4,12 +4,13 @@ import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
 import org.springframework.beans.factory.support.BeanDefinitionBuilder;
 import org.springframework.beans.factory.support.BeanDefinitionRegistry;
-import org.springframework.beans.factory.support.RootBeanDefinition;
 import org.springframework.beans.factory.support.ManagedList;
+import org.springframework.beans.factory.support.RootBeanDefinition;
 import org.springframework.beans.factory.xml.ParserContext;
 import org.springframework.security.AccessDecisionManager;
 import org.springframework.security.AuthenticationManager;
 import org.springframework.security.providers.ProviderManager;
+import org.springframework.security.userdetails.UserDetailsService;
 import org.springframework.security.vote.AffirmativeBased;
 import org.springframework.security.vote.AuthenticatedVoter;
 import org.springframework.security.vote.RoleVoter;
@@ -93,6 +94,20 @@ public abstract class ConfigUtils {
                 getAuthenticationManager(beanFactory));
     }
 
+    static UserDetailsService getUserDetailsService(ConfigurableListableBeanFactory bf) {
+        Map services = bf.getBeansOfType(UserDetailsService.class);
+
+        if (services.size() == 0) {
+            throw new IllegalArgumentException("No UserDetailsService registered.");
+
+        } else if (services.size() > 1) {
+            throw new IllegalArgumentException("More than one UserDetailsService registered. Please" +
+                    "use a specific Id in yur configuration");
+        }
+
+        return (UserDetailsService) services.values().toArray()[0];        
+    }
+
     private static AuthenticationManager getAuthenticationManager(ConfigurableListableBeanFactory bf) {
         Map authManagers = bf.getBeansOfType(AuthenticationManager.class);
 

+ 6 - 3
core/src/main/java/org/springframework/security/config/FormLoginBeanDefinitionParser.java

@@ -29,7 +29,7 @@ public class FormLoginBeanDefinitionParser implements BeanDefinitionParser {
     private static final String LOGIN_PAGE_ATTRIBUTE = "loginPage";
 
     private static final String FORM_LOGIN_TARGET_URL_ATTRIBUTE = "defaultTargetUrl";
-    private static final String DEFAULT_FORM_LOGIN_TARGET_URL = "/index";
+    private static final String DEFAULT_FORM_LOGIN_TARGET_URL = "/";
 
     private static final String FORM_LOGIN_AUTH_FAILURE_URL_ATTRIBUTE = "defaultTargetUrl";
     // TODO: Change AbstractProcessingFilter to not need a failure URL and just write a failure message
@@ -38,8 +38,13 @@ public class FormLoginBeanDefinitionParser implements BeanDefinitionParser {
 
 
     public BeanDefinition parse(Element elt, ParserContext parserContext) {
+        ConfigUtils.registerProviderManagerIfNecessary(parserContext);
+
         BeanDefinition filterBean = createFilterBean(elt);
 
+        filterBean.getPropertyValues().addPropertyValue("authenticationManager",
+                new RuntimeBeanReference(ConfigUtils.DEFAULT_AUTH_MANAGER_ID));
+
         BeanDefinitionBuilder entryPointBuilder =
                 BeanDefinitionBuilder.rootBeanDefinition(AuthenticationProcessingFilterEntryPoint.class);
 
@@ -90,8 +95,6 @@ public class FormLoginBeanDefinitionParser implements BeanDefinitionParser {
         }
 
         filterBuilder.addPropertyValue("authenticationFailureUrl", authenticationFailureUrl);
-        // Set autowire to pick up the authentication manager.
-        filterBuilder.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE);
 
         return filterBuilder.getBeanDefinition();
     }

+ 7 - 1
core/src/main/java/org/springframework/security/config/HttpSecurityBeanDefinitionParser.java

@@ -21,7 +21,6 @@ import org.springframework.util.StringUtils;
 import org.springframework.util.xml.DomUtils;
 import org.w3c.dom.Element;
 
-import javax.servlet.Filter;
 import java.util.*;
 
 /**
@@ -44,6 +43,7 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
     public static final String LOGOUT_ELEMENT = "logout";
     public static final String FORM_LOGIN_ELEMENT = "form-login";
     public static final String BASIC_AUTH_ELEMENT = "http-basic";
+    public static final String REMEMBER_ME_ELEMENT = "remember-me";    
 
     static final String PATH_PATTERN_ATTRIBUTE = "pattern";
     static final String PATTERN_TYPE_ATTRIBUTE = "pathType";
@@ -120,6 +120,12 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
             new BasicAuthenticationBeanDefinitionParser().parse(basicAuthElt, parserContext);
         }
 
+        Element rememberMeElt = DomUtils.getChildElementByTagName(element, REMEMBER_ME_ELEMENT);
+
+        if (rememberMeElt != null) {
+            new RememberMeBeanDefinitionParser().parse(rememberMeElt, parserContext);
+        }        
+
         registry.registerBeanDefinition(DEFAULT_FILTER_CHAIN_PROXY_ID, filterChainProxy);
         registry.registerBeanDefinition(DEFAULT_HTTP_SESSION_FILTER_ID, httpSCIF);
         registry.registerBeanDefinition(DEFAULT_EXCEPTION_TRANSLATION_FILTER_ID,

+ 57 - 2
core/src/main/java/org/springframework/security/config/HttpSecurityConfigPostProcessor.java

@@ -3,14 +3,17 @@ package org.springframework.security.config;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
 import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
 import org.springframework.core.OrderComparator;
 import org.springframework.core.Ordered;
-import org.springframework.security.AuthenticationManager;
+import org.springframework.security.concurrent.ConcurrentSessionFilter;
 import org.springframework.security.context.HttpSessionContextIntegrationFilter;
+import org.springframework.security.ui.AbstractProcessingFilter;
 import org.springframework.security.ui.AuthenticationEntryPoint;
+import org.springframework.security.ui.rememberme.RememberMeServices;
 import org.springframework.security.util.FilterChainProxy;
 import org.springframework.util.Assert;
 
@@ -38,11 +41,55 @@ public class HttpSecurityConfigPostProcessor implements BeanFactoryPostProcessor
 
         ConfigUtils.configureSecurityInterceptor(beanFactory, securityInterceptor);
 
+        configureRememberMeSerices(beanFactory);
+
         configureAuthenticationEntryPoint(beanFactory);
 
+        configureAuthenticationFilter(beanFactory);        
+
         configureFilterChain(beanFactory);
     }
 
+    private void configureRememberMeSerices(ConfigurableListableBeanFactory beanFactory) {
+        try {           
+            BeanDefinition rememberMeServices =
+                    beanFactory.getBeanDefinition(RememberMeBeanDefinitionParser.DEFAULT_REMEMBER_ME_SERVICES_ID);
+            rememberMeServices.getPropertyValues().addPropertyValue("userDetailsService",
+                    ConfigUtils.getUserDetailsService(beanFactory));
+
+            BeanDefinition logoutFilter =
+                    beanFactory.getBeanDefinition(HttpSecurityBeanDefinitionParser.DEFAULT_FILTER_SECURITY_INTERCEPTOR_ID);
+
+        } catch (NoSuchBeanDefinitionException e) {
+            // ignore
+        }
+    }
+
+    /**
+     * Sets the authentication manager, (and remember-me services, if required) on any instances of
+     * AbstractProcessingFilter
+     */
+    private void configureAuthenticationFilter(ConfigurableListableBeanFactory beanFactory) {
+        Map beans = beanFactory.getBeansOfType(RememberMeServices.class);
+
+        RememberMeServices rememberMeServices = null;
+
+        if (beans.size() > 0) {
+            rememberMeServices = (RememberMeServices) beans.values().toArray()[0];
+        }
+
+        Iterator authFilters = beanFactory.getBeansOfType(AbstractProcessingFilter.class).values().iterator();
+
+        while (authFilters.hasNext()) {
+            AbstractProcessingFilter filter = (AbstractProcessingFilter) authFilters.next();
+
+            if (rememberMeServices != null) {
+                logger.info("Using RememberMeServices " + rememberMeServices + " with filter " + filter);
+                filter.setRememberMeServices(rememberMeServices);
+            }
+        }
+    }
+
     /**
      * Selects the entry point that should be used in ExceptionTranslationFilter. Strategy is
      *
@@ -52,7 +99,6 @@ public class HttpSecurityConfigPostProcessor implements BeanFactoryPostProcessor
      * <li>throw an exception (for now). TODO: Examine additional beans and types and make decision</li>
      * </ol>
      *
-     * @param beanFactory
      */
     private void configureAuthenticationEntryPoint(ConfigurableListableBeanFactory beanFactory) {
         logger.info("Selecting AuthenticationEntryPoint for use in ExceptionTranslationFilter");
@@ -90,6 +136,15 @@ public class HttpSecurityConfigPostProcessor implements BeanFactoryPostProcessor
         filterMap.put(allUrlsMatch, defaultFilterChain);
 
         filterChainProxy.setFilterChainMap(filterMap);
+
+        Map sessionFilters = beanFactory.getBeansOfType(ConcurrentSessionFilter.class);
+
+        if (!sessionFilters.isEmpty()) {
+            logger.info("Concurrent session filter in use, setting 'forceEagerSessionCreation' to true");
+            HttpSessionContextIntegrationFilter scif = (HttpSessionContextIntegrationFilter)
+                    beanFactory.getBean(HttpSecurityBeanDefinitionParser.DEFAULT_HTTP_SESSION_FILTER_ID);
+            scif.setForceEagerSessionCreation(true);
+        }
     }
 
     private List orderFilters(ConfigurableListableBeanFactory beanFactory) {

+ 72 - 0
core/src/main/java/org/springframework/security/config/RememberMeBeanDefinitionParser.java

@@ -0,0 +1,72 @@
+package org.springframework.security.config;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.RuntimeBeanReference;
+import org.springframework.beans.factory.support.ManagedList;
+import org.springframework.beans.factory.support.RootBeanDefinition;
+import org.springframework.beans.factory.xml.BeanDefinitionParser;
+import org.springframework.beans.factory.xml.ParserContext;
+import org.springframework.security.ui.rememberme.JdbcTokenRepositoryImpl;
+import org.springframework.security.ui.rememberme.PersistentTokenBasedRememberMeServices;
+import org.springframework.security.ui.rememberme.RememberMeProcessingFilter;
+import org.springframework.security.ui.rememberme.TokenBasedRememberMeServices;
+import org.springframework.security.providers.rememberme.RememberMeAuthenticationProvider;
+import org.springframework.util.StringUtils;
+import org.w3c.dom.Element;
+
+/**
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class RememberMeBeanDefinitionParser implements BeanDefinitionParser {
+    protected final Log logger = LogFactory.getLog(getClass());    
+
+    public static final String DEFAULT_REMEMBER_ME_FILTER_ID = "_rememberMeFilter";
+    public static final String DEFAULT_REMEMBER_ME_SERVICES_ID = "_rememberMeServices";   
+
+    public BeanDefinition parse(Element element, ParserContext parserContext) {
+        BeanDefinition filter = new RootBeanDefinition(RememberMeProcessingFilter.class);
+        BeanDefinition services = new RootBeanDefinition(PersistentTokenBasedRememberMeServices.class);
+
+        filter.getPropertyValues().addPropertyValue("authenticationManager",
+                new RuntimeBeanReference(ConfigUtils.DEFAULT_AUTH_MANAGER_ID));
+
+        String tokenRepository = element.getAttribute("tokenRepository");
+        String dataSource = element.getAttribute("dataSource");
+
+        if (StringUtils.hasText(tokenRepository)) {
+            if (StringUtils.hasText(dataSource)) {
+                throw new SecurityConfigurationException("Specify tokenRepository or dataSource but not both");
+            }
+
+            services.getPropertyValues().addPropertyValue("tokenRepository", new RuntimeBeanReference(tokenRepository));
+
+        } else if (StringUtils.hasText(dataSource)) {
+            BeanDefinition tokenRepo = new RootBeanDefinition(JdbcTokenRepositoryImpl.class);
+            tokenRepo.getPropertyValues().addPropertyValue("dataSource", new RuntimeBeanReference(dataSource));
+        } else {
+            // Not persistent
+            services = new RootBeanDefinition(TokenBasedRememberMeServices.class);
+        }
+
+        String key = element.getAttribute("key");
+        services.getPropertyValues().addPropertyValue("key", key);
+
+        BeanDefinition authManager = ConfigUtils.registerProviderManagerIfNecessary(parserContext);
+        BeanDefinition provider = new RootBeanDefinition(RememberMeAuthenticationProvider.class);
+        provider.getPropertyValues().addPropertyValue("key", key);
+
+        ManagedList providers = (ManagedList) authManager.getPropertyValues().getPropertyValue("providers").getValue();
+        providers.add(provider);
+
+        filter.getPropertyValues().addPropertyValue("rememberMeServices",
+                new RuntimeBeanReference(DEFAULT_REMEMBER_ME_SERVICES_ID));
+
+        parserContext.getRegistry().registerBeanDefinition(DEFAULT_REMEMBER_ME_SERVICES_ID, services);
+        parserContext.getRegistry().registerBeanDefinition(DEFAULT_REMEMBER_ME_FILTER_ID, filter);
+
+        return null;
+    }
+}

+ 29 - 7
core/src/main/java/org/springframework/security/ui/rememberme/AbstractRememberMeServices.java

@@ -3,17 +3,20 @@ package org.springframework.security.ui.rememberme;
 import org.apache.commons.codec.binary.Base64;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.support.MessageSourceAccessor;
 import org.springframework.security.Authentication;
 import org.springframework.security.SpringSecurityMessageSource;
 import org.springframework.security.providers.rememberme.RememberMeAuthenticationToken;
 import org.springframework.security.ui.AuthenticationDetailsSource;
 import org.springframework.security.ui.AuthenticationDetailsSourceImpl;
+import org.springframework.security.ui.logout.LogoutHandler;
 import org.springframework.security.userdetails.UserDetails;
 import org.springframework.security.userdetails.UserDetailsService;
 import org.springframework.security.userdetails.UsernameNotFoundException;
+import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 import org.springframework.web.bind.ServletRequestUtils;
-import org.springframework.context.support.MessageSourceAccessor;
 
 import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
@@ -25,7 +28,7 @@ import javax.servlet.http.HttpServletResponse;
  * @author Luke Taylor
  * @version $Id$
  */
-public abstract class AbstractRememberMeServices implements RememberMeServices {
+public abstract class AbstractRememberMeServices implements RememberMeServices, InitializingBean, LogoutHandler {
 
     protected final Log logger = LogFactory.getLog(getClass());
 
@@ -42,7 +45,14 @@ public abstract class AbstractRememberMeServices implements RememberMeServices {
 	private String parameter = DEFAULT_PARAMETER;
     private boolean alwaysRemember;
     private String key;
-    private long tokenValiditySeconds = 1209600; // 14 days
+    private int tokenValiditySeconds = 1209600; // 14 days
+
+    public void afterPropertiesSet() throws Exception {
+        Assert.hasLength(key);        
+        Assert.hasLength(parameter);
+        Assert.hasLength(cookieName);
+        Assert.notNull(userDetailsService);        
+    }
 
     /**
      * Template implementation which locates the Spring Security cookie, decodes it into
@@ -261,13 +271,21 @@ public abstract class AbstractRememberMeServices implements RememberMeServices {
         return cookie;
     }
 
-    protected Cookie makeValidCookie(String value, HttpServletRequest request, long maxAge) {
+    protected Cookie makeValidCookie(String value, HttpServletRequest request, int maxAge) {
         Cookie cookie = new Cookie(cookieName, value);
-        cookie.setMaxAge(new Long(maxAge).intValue());
+        cookie.setMaxAge(maxAge);
         cookie.setPath(StringUtils.hasLength(request.getContextPath()) ? request.getContextPath() : "/");
 
         return cookie;
     }
+
+    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
+        if (logger.isDebugEnabled()) {
+            logger.debug( "Logout of user "
+                    + (authentication == null ? "Unknown" : authentication.getName()));
+        }
+        cancelCookie(request, response);
+    }
     
     public void setCookieName(String cookieName) {
         this.cookieName = cookieName;
@@ -281,6 +299,10 @@ public abstract class AbstractRememberMeServices implements RememberMeServices {
         this.parameter = parameter;
     }
 
+    public String getParameter() {
+        return parameter;
+    }
+
     protected UserDetailsService getUserDetailsService() {
         return userDetailsService;
     }
@@ -293,11 +315,11 @@ public abstract class AbstractRememberMeServices implements RememberMeServices {
         this.key = key;
     }
 
-    public void setTokenValiditySeconds(long tokenValiditySeconds) {
+    public void setTokenValiditySeconds(int tokenValiditySeconds) {
         this.tokenValiditySeconds = tokenValiditySeconds;
     }
 
-    public long getTokenValiditySeconds() {
+    public int getTokenValiditySeconds() {
         return tokenValiditySeconds;
     }
     

+ 34 - 0
core/src/main/java/org/springframework/security/ui/rememberme/JdbcTokenRepositoryImpl.java

@@ -0,0 +1,34 @@
+package org.springframework.security.ui.rememberme;
+
+import org.springframework.jdbc.core.support.JdbcDaoSupport;
+
+/**
+ * 
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements PersistentTokenRepository {
+    //~ Static fields/initializers =====================================================================================    
+    public static final String DEF_TOKEN_BY_SERIES_QUERY =
+            "select username,series,token from persistent_logins where series = ?";
+    public static final String DEF_INSERT_TOKEN_STATEMENT =
+            "insert into persistent_logins (username,series,token) values(?,?,?)";
+    public static final String DEF_REMOVE_USER_TOKENS_STATEMENT =
+            "delete from persistent_logins where username = ?";
+
+    //~ Instance fields ================================================================================================
+
+    private String tokensBySeriesQuery = DEF_TOKEN_BY_SERIES_QUERY;
+    private String insertTokenStatement = DEF_INSERT_TOKEN_STATEMENT;
+    private String removeUserTokensStatement = DEF_REMOVE_USER_TOKENS_STATEMENT;
+
+    public void saveToken(PersistentRememberMeToken token) {
+    }
+
+    public PersistentRememberMeToken getTokenForSeries(String seriesId) {
+        return null;
+    }
+
+    public void removeAllTokens(String username) {
+    }
+}

+ 5 - 3
core/src/main/java/org/springframework/security/ui/rememberme/PersistentTokenBasedRememberMeServices.java

@@ -109,7 +109,10 @@ public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeSe
     }
 
     private PersistentRememberMeToken createNewToken(String username, String series) {
-        logger.debug("Creating new persistent login token for user " + username);
+        if (logger.isDebugEnabled()) {
+            logger.debug(series == null ? "Creating new" : "Renewing" +
+                    " persistent login token for user " + username);
+        }
 
         if (series == null) {
             byte[] newSeries = new byte[seriesLength];
@@ -131,8 +134,7 @@ public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeSe
 
     private void addCookie(PersistentRememberMeToken token, HttpServletRequest request, HttpServletResponse response) {
         String cookieValue = encodeCookie(new String[] {token.getSeries(), token.getTokenValue()});
-        long maxAge = System.currentTimeMillis() + getTokenValiditySeconds() * 1000;
-        response.addCookie(makeValidCookie(cookieValue, request, maxAge));
+        response.addCookie(makeValidCookie(cookieValue, request, getTokenValiditySeconds()));
     }
 
     public void setTokenRepository(PersistentTokenRepository tokenRepository) {

+ 3 - 2
core/src/main/java/org/springframework/security/ui/webapp/DefaultLoginPageGeneratingFilter.java

@@ -4,6 +4,7 @@ import org.springframework.security.AuthenticationException;
 import org.springframework.security.ui.AbstractProcessingFilter;
 import org.springframework.security.ui.FilterChainOrderUtils;
 import org.springframework.security.ui.SpringSecurityFilter;
+import org.springframework.security.ui.rememberme.AbstractRememberMeServices;
 import org.springframework.security.ui.rememberme.TokenBasedRememberMeServices;
 import org.springframework.util.StringUtils;
 
@@ -35,8 +36,8 @@ public class DefaultLoginPageGeneratingFilter extends SpringSecurityFilter {
         usernameParameter = authFilter.getUsernameParameter();
         passwordParameter = authFilter.getPasswordParameter();
 
-        if (authFilter.getRememberMeServices() instanceof TokenBasedRememberMeServices) {
-            rememberMeParameter = ((TokenBasedRememberMeServices)authFilter.getRememberMeServices()).getParameter();
+        if (authFilter.getRememberMeServices() instanceof AbstractRememberMeServices) {
+            rememberMeParameter = ((AbstractRememberMeServices)authFilter.getRememberMeServices()).getParameter();
         }
     }
 

+ 8 - 2
core/src/main/resources/org/springframework/security/config/spring-security-2.0.rnc

@@ -48,7 +48,7 @@ protect.attlist &=
 
 http =
     ## Container element for HTTP security configuration
-   element http {http.attlist, intercept-url+, form-login?, http-basic?, logout?, concurrent-session-control? }
+   element http {http.attlist, (intercept-url+ & form-login? & http-basic? & logout? & concurrent-session-control? & remember-me?) }
 http.attlist &=
     ## Controls the eagerness with which an HTTP session is created.
     [ a:defaultValue = "ifRequired" ] attribute createSession {"ifRequired" | "always" | "never" }?
@@ -135,6 +135,13 @@ concurrent-sessions.attlist &=
 concurrent-sessions.attlist &=
     attribute exceptionIfMaximumExceeded {"true" | "false"}?
 
+remember-me = 
+    element remember-me {remember-me.attlist}
+remember-me.attlist &= 
+    attribute key {xsd:string}
+remember-me.attlist &= 
+    (attribute tokenRepository {xsd:string} | attribute datasource {xsd:string})?        
+    
 authentication-provider =
     element authentication-provider {authentication-provider.attlist, (user-service | jdbc-user-service)}
 authentication-provider.attlist &= empty
@@ -144,7 +151,6 @@ user-service =
 user-service.attlist &=
     attribute properties {xsd:string}*
 
-
 user =
     element user {user.attlist, empty}
 user.attlist &=

+ 18 - 7
core/src/main/resources/org/springframework/security/config/spring-security-2.0.xsd

@@ -72,13 +72,14 @@
       <xs:documentation>Container element for HTTP security configuration</xs:documentation>
     </xs:annotation>
     <xs:complexType>
-      <xs:sequence>
-        <xs:element maxOccurs="unbounded" ref="security:intercept-url"/>
-        <xs:element minOccurs="0" ref="security:form-login"/>
-        <xs:element minOccurs="0" ref="security:http-basic"/>
-        <xs:element minOccurs="0" ref="security:logout"/>
-        <xs:element minOccurs="0" ref="security:concurrent-session-control"/>
-      </xs:sequence>
+      <xs:choice minOccurs="0" maxOccurs="unbounded">
+        <xs:element ref="security:intercept-url"/>
+        <xs:element ref="security:form-login"/>
+        <xs:element ref="security:http-basic"/>
+        <xs:element ref="security:logout"/>
+        <xs:element ref="security:concurrent-session-control"/>
+        <xs:element ref="security:remember-me"/>
+      </xs:choice>
       <xs:attributeGroup ref="security:http.attlist"/>
     </xs:complexType>
   </xs:element>
@@ -241,6 +242,16 @@
       </xs:simpleType>
     </xs:attribute>
   </xs:attributeGroup>
+  <xs:element name="remember-me">
+    <xs:complexType>
+      <xs:attributeGroup ref="security:remember-me.attlist"/>
+    </xs:complexType>
+  </xs:element>
+  <xs:attributeGroup name="remember-me.attlist">
+    <xs:attribute name="key" use="required" type="xs:string"/>
+    <xs:attribute name="tokenRepository" type="xs:string"/>
+    <xs:attribute name="datasource" type="xs:string"/>
+  </xs:attributeGroup>
   <xs:element name="authentication-provider">
     <xs:complexType>
       <xs:choice>

+ 8 - 5
core/src/test/java/org/springframework/security/config/HttpSecurityBeanDefinitionParserTests.java

@@ -1,20 +1,22 @@
 package org.springframework.security.config;
 
-import org.junit.AfterClass;
-import static org.junit.Assert.assertTrue;
-import org.junit.BeforeClass;
-import org.junit.Test;
 import org.springframework.context.support.ClassPathXmlApplicationContext;
 import org.springframework.security.concurrent.ConcurrentSessionFilter;
 import org.springframework.security.context.HttpSessionContextIntegrationFilter;
 import org.springframework.security.intercept.web.FilterSecurityInterceptor;
 import org.springframework.security.ui.ExceptionTranslationFilter;
+import org.springframework.security.ui.rememberme.RememberMeProcessingFilter;
 import org.springframework.security.ui.basicauth.BasicProcessingFilter;
 import org.springframework.security.ui.logout.LogoutFilter;
 import org.springframework.security.ui.webapp.AuthenticationProcessingFilter;
 import org.springframework.security.ui.webapp.DefaultLoginPageGeneratingFilter;
 import org.springframework.security.util.FilterChainProxy;
 
+import org.junit.AfterClass;
+import static org.junit.Assert.*;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
 import java.util.Iterator;
 import java.util.List;
 
@@ -54,7 +56,7 @@ public class HttpSecurityBeanDefinitionParserTests {
 
         List filterList = filterChainProxy.getFilters("/someurl");
 
-        assertTrue("Expected 8 filters in chain", filterList.size() == 8);
+        assertEquals("Expected 9 filters in chain", 9, filterList.size());
 
         Iterator filters = filterList.iterator();
 
@@ -64,6 +66,7 @@ public class HttpSecurityBeanDefinitionParserTests {
         assertTrue(filters.next() instanceof AuthenticationProcessingFilter);
         assertTrue(filters.next() instanceof DefaultLoginPageGeneratingFilter);
         assertTrue(filters.next() instanceof BasicProcessingFilter);
+        assertTrue(filters.next() instanceof RememberMeProcessingFilter);        
         assertTrue(filters.next() instanceof ExceptionTranslationFilter);
         assertTrue(filters.next() instanceof FilterSecurityInterceptor);
     }

+ 10 - 3
core/src/test/resources/org/springframework/security/config/http-security.xml

@@ -7,20 +7,22 @@
 http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.xsd">
 
     <security:http createSession="ifRequired" pathType="ant" lowerCaseComparisons="true">
-        <security:intercept-url pattern="/unprotected" filters="none"/>
+        <security:intercept-url pattern="/unprotected" filters="none" />
         <security:intercept-url pattern="/somepath" access="ROLE_SPECIAL,ROLE_USER" />
         <security:intercept-url pattern="/**" access="ROLE_USER" />
 
         <!-- Default form login configuration. Will create filter and entry point -->
-        <security:form-login loginUrl="/j_spring_security_check"  />
+        <security:form-login loginUrl="/j_spring_security_check" />
 
         <!-- Default basic auth configuration. Will create filter and entry point -->
-        <security:http-basic realm="NamespaceTestRealm"  />
+        <security:http-basic realm="NamespaceTestRealm" />
 
         <!-- Default logout configuration -->
         <security:logout logoutUrl="/j_spring_security_logout" logoutSuccessUrl="/" invalidateSession="true" />        
 
         <security:concurrent-session-control maxSessions="1"/>
+
+        <security:remember-me key="doesntmatter" tokenRepository="tokenRepo"/>
     </security:http>
 
     <security:authentication-provider>
@@ -29,4 +31,9 @@ http://www.springframework.org/schema/security http://www.springframework.org/sc
             <security:user name="bill" password="billspassword" authorities="ROLE_A,ROLE_B,AUTH_OTHER" />
         </security:user-service>
     </security:authentication-provider>
+
+    <bean name="tokenRepo" class="org.springframework.security.ui.rememberme.InMemoryTokenRepositoryImpl"/>
+
+    <!-- bean name="rememberMeServices" class="org.springframework.security.ui.rememberme.NullRememberMeServices"/ -->
+
 </beans>

+ 2 - 0
samples/tutorial/src/main/webapp/WEB-INF/applicationContext-security-ns.xml

@@ -22,8 +22,10 @@ http://www.springframework.org/schema/security http://www.springframework.org/sc
         <security:logout />
         <security:concurrent-session-control maxSessions="1" exceptionIfMaximumExceeded="true"/>
 
+        <security:remember-me key="doesntmatter" tokenRepository="tokenRepo"/>
     </security:http>
 
+    <bean name="tokenRepo" class="org.springframework.security.ui.rememberme.InMemoryTokenRepositoryImpl"/>
 
     <security:authentication-provider>
         <security:user-service>