SEC-2042: AbstractAuthenticationProcessingFilter supports RequestMatcher

This commit is contained in:
Rob Winch 2013-07-23 13:06:51 -05:00
parent f34b459c80
commit f5a30e55a3
14 changed files with 128 additions and 74 deletions

View File

@ -172,6 +172,7 @@ public class CasAuthenticationFilterTests {
serviceProperties.setAuthenticateAllArtifacts(true);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("ticket", "ST-1-123");
request.setRequestURI("/authenticate");
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain chain = mock(FilterChain.class);

View File

@ -33,6 +33,7 @@ import org.springframework.security.web.authentication.SimpleUrlAuthenticationFa
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.util.AntPathRequestMatcher;
import org.springframework.security.web.util.MediaTypeRequestMatcher;
import org.springframework.security.web.util.RequestMatcher;
import org.springframework.web.accept.ContentNegotiationStrategy;
@ -139,10 +140,17 @@ public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecu
*/
public T loginProcessingUrl(String loginProcessingUrl) {
this.loginProcessingUrl = loginProcessingUrl;
authFilter.setFilterProcessesUrl(loginProcessingUrl);
authFilter.setRequiresAuthenticationRequestMatcher(createLoginProcessingUrlMatcher(loginProcessingUrl));
return getSelf();
}
/**
* Create the {@link RequestMatcher} given a loginProcessingUrl
* @param loginProcessingUrl creates the {@link RequestMatcher} based upon the loginProcessingUrl
* @return the {@link RequestMatcher} to use based upon the loginProcessingUrl
*/
protected abstract RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl);
/**
* Specifies a custom {@link AuthenticationDetailsSource}. The default is {@link WebAuthenticationDetailsSource}.
*

View File

@ -15,9 +15,6 @@
*/
package org.springframework.security.config.annotation.web.configurers;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@ -26,6 +23,8 @@ import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.authentication.ui.DefaultLoginPageViewFilter;
import org.springframework.security.web.util.AntPathRequestMatcher;
import org.springframework.security.web.util.RequestMatcher;
/**
* Adds form based authentication. All attributes have reasonable defaults
@ -71,7 +70,7 @@ public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
* @see HttpSecurity#formLogin()
*/
public FormLoginConfigurer() {
super(createUsernamePasswordAuthenticationFilter(),"/login");
super(new UsernamePasswordAuthenticationFilter(),"/login");
usernameParameter("username");
passwordParameter("password");
}
@ -193,6 +192,15 @@ public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
initDefaultLoginFilter(http);
}
/* (non-Javadoc)
* @see org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer#createLoginProcessingUrlMatcher(java.lang.String)
*/
@Override
protected RequestMatcher createLoginProcessingUrlMatcher(
String loginProcessingUrl) {
return new AntPathRequestMatcher(loginProcessingUrl, "POST");
}
/**
* Gets the HTTP parameter that is used to submit the username.
*
@ -227,13 +235,4 @@ public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
loginPageGeneratingFilter.setAuthenticationUrl(getLoginProcessingUrl());
}
}
private static UsernamePasswordAuthenticationFilter createUsernamePasswordAuthenticationFilter() {
return new UsernamePasswordAuthenticationFilter() {
@Override
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
return "POST".equals(request.getMethod()) && super.requiresAuthentication(request, response);
}
};
}
}

View File

@ -49,6 +49,8 @@ import org.springframework.security.web.authentication.LoginUrlAuthenticationEnt
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.authentication.ui.DefaultLoginPageViewFilter;
import org.springframework.security.web.util.AntPathRequestMatcher;
import org.springframework.security.web.util.RequestMatcher;
/**
* Adds support for OpenID based authentication.
@ -248,6 +250,15 @@ public final class OpenIDLoginConfigurer<H extends HttpSecurityBuilder<H>> exten
super.configure(http);
}
/* (non-Javadoc)
* @see org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer#createLoginProcessingUrlMatcher(java.lang.String)
*/
@Override
protected RequestMatcher createLoginProcessingUrlMatcher(
String loginProcessingUrl) {
return new AntPathRequestMatcher(loginProcessingUrl);
}
/**
* Gets the {@link OpenIDConsumer} that was configured or defaults to an {@link OpenID4JavaConsumer}.
* @return the {@link OpenIDConsumer} to use

View File

@ -37,7 +37,7 @@ abstract class BaseWebSpecuritySpec extends BaseSpringSpec {
MockFilterChain chain
def setup() {
request = new MockHttpServletRequest()
request = new MockHttpServletRequest(method:"GET")
response = new MockHttpServletResponse()
chain = new MockFilterChain()
}

View File

@ -46,14 +46,14 @@ public class SampleWebSecurityConfigurerAdapterTests extends BaseWebSpecuritySpe
when: "fail to log in"
super.setup()
request.addHeader("Accept", "text/html")
request.requestURI = "/login"
request.servletPath = "/login"
request.method = "POST"
springSecurityFilterChain.doFilter(request,response,chain)
then: "sent to login error page"
response.getRedirectedUrl() == "/login?error"
when: "login success"
super.setup()
request.requestURI = "/login"
request.servletPath = "/login"
request.method = "POST"
request.parameters.username = ["user"] as String[]
request.parameters.password = ["password"] as String[]
@ -108,14 +108,14 @@ public class SampleWebSecurityConfigurerAdapterTests extends BaseWebSpecuritySpe
response.getRedirectedUrl() == "http://localhost/login"
when: "fail to log in"
super.setup()
request.requestURI = "/login"
request.servletPath = "/login"
request.method = "POST"
springSecurityFilterChain.doFilter(request,response,chain)
then: "sent to login error page"
response.getRedirectedUrl() == "/login?error"
when: "login success"
super.setup()
request.requestURI = "/login"
request.servletPath = "/login"
request.method = "POST"
request.parameters.username = ["user"] as String[]
request.parameters.password = ["password"] as String[]
@ -197,14 +197,14 @@ public class SampleWebSecurityConfigurerAdapterTests extends BaseWebSpecuritySpe
response.getRedirectedUrl() == "http://localhost/login"
when: "fail to log in"
super.setup()
request.requestURI = "/login"
request.servletPath = "/login"
request.method = "POST"
springSecurityFilterChain.doFilter(request,response,chain)
then: "sent to login error page"
response.getRedirectedUrl() == "/login?error"
when: "login success"
super.setup()
request.requestURI = "/login"
request.servletPath = "/login"
request.method = "POST"
request.parameters.username = ["user"] as String[]
request.parameters.password = ["password"] as String[]

View File

@ -77,7 +77,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
</form></body></html>"""
when: "fail to log in"
setup()
request.requestURI = "/login"
request.servletPath = "/login"
request.method = "POST"
springSecurityFilterChain.doFilter(request,response,chain)
then: "sent to login error page"
@ -100,7 +100,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
</form></body></html>"""
when: "login success"
setup()
request.requestURI = "/login"
request.servletPath = "/login"
request.method = "POST"
request.parameters.username = ["user"] as String[]
request.parameters.password = ["password"] as String[]

View File

@ -72,8 +72,8 @@ class FormLoginConfigurerTests extends BaseSpringSpec {
authFilter.passwordParameter == "password"
authFilter.failureHandler.defaultFailureUrl == "/login?error"
authFilter.successHandler.defaultTargetUrl == "/"
authFilter.requiresAuthentication(new MockHttpServletRequest(requestURI : "/login", method: "POST"), new MockHttpServletResponse())
!authFilter.requiresAuthentication(new MockHttpServletRequest(requestURI : "/login", method: "GET"), new MockHttpServletResponse())
authFilter.requiresAuthentication(new MockHttpServletRequest(servletPath : "/login", method: "POST"), new MockHttpServletResponse())
!authFilter.requiresAuthentication(new MockHttpServletRequest(servletPath : "/login", method: "GET"), new MockHttpServletResponse())
and: "SessionFixationProtectionStrategy is configured correctly"
SessionFixationProtectionStrategy sessionStrategy = ReflectionTestUtils.getField(authFilter,"sessionStrategy")
@ -82,7 +82,7 @@ class FormLoginConfigurerTests extends BaseSpringSpec {
and: "Exception handling is configured correctly"
AuthenticationEntryPoint authEntryPoint = filterChains[1].filters.find { it instanceof ExceptionTranslationFilter}.authenticationEntryPoint
MockHttpServletResponse response = new MockHttpServletResponse()
authEntryPoint.commence(new MockHttpServletRequest(requestURI: "/private/"), response, new BadCredentialsException(""))
authEntryPoint.commence(new MockHttpServletRequest(servletPath: "/private/"), response, new BadCredentialsException(""))
response.redirectedUrl == "http://localhost/login"
}
@ -172,7 +172,7 @@ class FormLoginConfigurerTests extends BaseSpringSpec {
loadConfig(PermitAllIgnoresFailureHandlerConfig)
FilterChainProxy springSecurityFilterChain = context.getBean(FilterChainProxy)
when: "access default failureUrl and configured explicit FailureHandler"
MockHttpServletRequest request = new MockHttpServletRequest(requestURI:"/login",queryString:"error")
MockHttpServletRequest request = new MockHttpServletRequest(servletPath:"/login",requestURI:"/login",queryString:"error",method:"GET")
MockHttpServletResponse response = new MockHttpServletResponse()
springSecurityFilterChain.doFilter(request,response,new MockFilterChain())
then: "access is not granted to the failure handler (sent to login page)"

View File

@ -50,14 +50,14 @@ public class NamespaceHttpFormLoginTests extends BaseSpringSpec {
response.getRedirectedUrl() == "http://localhost/login"
when: "fail to log in"
super.setup()
request.requestURI = "/login"
request.servletPath = "/login"
request.method = "POST"
springSecurityFilterChain.doFilter(request,response,chain)
then: "sent to login error page"
response.getRedirectedUrl() == "/login?error"
when: "login success"
super.setup()
request.requestURI = "/login"
request.servletPath = "/login"
request.method = "POST"
request.parameters.username = ["user"] as String[]
request.parameters.password = ["password"] as String[]
@ -96,14 +96,14 @@ public class NamespaceHttpFormLoginTests extends BaseSpringSpec {
response.getRedirectedUrl() == "http://localhost/authentication/login"
when: "fail to log in"
super.setup()
request.requestURI = "/authentication/login/process"
request.servletPath = "/authentication/login/process"
request.method = "POST"
springSecurityFilterChain.doFilter(request,response,chain)
then: "sent to login error page"
response.getRedirectedUrl() == "/authentication/login?failed"
when: "login success"
super.setup()
request.requestURI = "/authentication/login/process"
request.servletPath = "/authentication/login/process"
request.method = "POST"
request.parameters.j_username = ["user"] as String[]
request.parameters.j_password = ["password"] as String[]
@ -137,14 +137,14 @@ public class NamespaceHttpFormLoginTests extends BaseSpringSpec {
then: "CustomWebAuthenticationDetailsSource is used"
findFilter(UsernamePasswordAuthenticationFilter).authenticationDetailsSource.class == CustomWebAuthenticationDetailsSource
when: "fail to log in"
request.requestURI = "/login"
request.servletPath = "/login"
request.method = "POST"
springSecurityFilterChain.doFilter(request,response,chain)
then: "sent to login error page"
response.getRedirectedUrl() == "/custom/failure"
when: "login success"
super.setup()
request.requestURI = "/login"
request.servletPath = "/login"
request.method = "POST"
request.parameters.username = ["user"] as String[]
request.parameters.password = ["password"] as String[]

View File

@ -67,7 +67,7 @@ public class NamespaceHttpOpenIDLoginTests extends BaseSpringSpec {
response.getRedirectedUrl() == "http://localhost/login"
when: "fail to log in"
setup()
request.requestURI = "/login/openid"
request.servletPath = "/login/openid"
request.method = "POST"
springSecurityFilterChain.doFilter(request,response,chain)
then: "sent to login error page"
@ -118,7 +118,7 @@ public class NamespaceHttpOpenIDLoginTests extends BaseSpringSpec {
response.getRedirectedUrl() == "http://localhost/login"
when: "fail to log in"
setup()
request.requestURI = "/login/openid"
request.servletPath = "/login/openid"
request.method = "POST"
springSecurityFilterChain.doFilter(request,response,chain)
then: "sent to login error page"
@ -172,7 +172,7 @@ public class NamespaceHttpOpenIDLoginTests extends BaseSpringSpec {
response.getRedirectedUrl() == "http://localhost/authentication/login"
when: "fail to log in"
setup()
request.requestURI = "/authentication/login/process"
request.servletPath = "/authentication/login/process"
request.method = "POST"
springSecurityFilterChain.doFilter(request,response,chain)
then: "sent to login error page"
@ -205,7 +205,7 @@ public class NamespaceHttpOpenIDLoginTests extends BaseSpringSpec {
findFilter(OpenIDAuthenticationFilter).authenticationDetailsSource.class == CustomWebAuthenticationDetailsSource
findAuthenticationProvider(OpenIDAuthenticationProvider).userDetailsService == OpenIDLoginCustomRefsConfig.AUDS
when: "fail to log in"
request.requestURI = "/login/openid"
request.servletPath = "/login/openid"
request.method = "POST"
springSecurityFilterChain.doFilter(request,response,chain)
then: "sent to login error page"

View File

@ -54,24 +54,13 @@ import org.springframework.test.util.ReflectionTestUtils;
*
*/
public class NamespaceRememberMeTests extends BaseSpringSpec {
FilterChainProxy springSecurityFilterChain
MockHttpServletRequest request
MockHttpServletResponse response
MockFilterChain chain
def setup() {
request = new MockHttpServletRequest()
response = new MockHttpServletResponse()
chain = new MockFilterChain()
}
def "http/remember-me"() {
setup:
loadConfig(RememberMeConfig)
springSecurityFilterChain = context.getBean(FilterChainProxy)
when: "login with remember me"
setup()
request.requestURI = "/login"
super.setup()
request.servletPath = "/login"
request.method = "POST"
request.parameters.username = ["user"] as String[]
request.parameters.password = ["password"] as String[]
@ -81,7 +70,7 @@ public class NamespaceRememberMeTests extends BaseSpringSpec {
then: "response contains remember me cookie"
rememberMeCookie != null
when: "session expires"
setup()
super.setup()
request.setCookies(rememberMeCookie)
request.requestURI = "/abc"
springSecurityFilterChain.doFilter(request,response,chain)
@ -90,7 +79,7 @@ public class NamespaceRememberMeTests extends BaseSpringSpec {
SecurityContext context = new HttpSessionSecurityContextRepository().loadContext(new HttpRequestResponseHolder(request, response))
context.getAuthentication() instanceof RememberMeAuthenticationToken
when: "logout"
setup()
super.setup()
request.setSession(session)
request.setCookies(rememberMeCookie)
request.requestURI = "/logout"
@ -100,7 +89,7 @@ public class NamespaceRememberMeTests extends BaseSpringSpec {
response.getRedirectedUrl() == "/login?logout"
rememberMeCookie.maxAge == 0
when: "use remember me after logout"
setup()
super.setup()
request.setCookies(rememberMeCookie)
request.requestURI = "/abc"
springSecurityFilterChain.doFilter(request,response,chain)

View File

@ -40,6 +40,7 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.util.RequestMatcher;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean;
@ -53,8 +54,7 @@ import org.springframework.web.filter.GenericFilterBean;
* required to process the authentication request tokens created by implementing classes.
* <p>
* This filter will intercept a request and attempt to perform authentication from that request if
* the request URL matches the value of the <tt>filterProcessesUrl</tt> property. This behaviour can modified by
* overriding the method {@link #requiresAuthentication(HttpServletRequest, HttpServletResponse) requiresAuthentication}.
* the request matches the {@link #setRequiresAuthenticationRequestMatcher(RequestMatcher)}.
* <p>
* Authentication is performed by the {@link #attemptAuthentication(HttpServletRequest, HttpServletResponse)
* attemptAuthentication} method, which must be implemented by subclasses.
@ -116,10 +116,14 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private RememberMeServices rememberMeServices = new NullRememberMeServices();
private RequestMatcher requiresAuthenticationRequestMatcher;
/**
* The URL destination that this filter intercepts and processes (usually
* something like <code>/j_spring_security_check</code>)
* @deprecated use {@link #requiresAuthenticationRequestMatcher} instead
*/
@Deprecated
private String filterProcessesUrl;
private boolean continueChainBeforeSuccessfulAuthentication = false;
@ -137,15 +141,26 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
* @param defaultFilterProcessesUrl the default value for <tt>filterProcessesUrl</tt>.
*/
protected AbstractAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
this.requiresAuthenticationRequestMatcher = new FilterProcessUrlRequestMatcher(defaultFilterProcessesUrl);
this.filterProcessesUrl = defaultFilterProcessesUrl;
}
/**
* Creates a new instance
*
* @param requiresAuthenticationRequestMatcher
* the {@link RequestMatcher} used to determine if authentication
* is required. Cannot be null.
*/
protected AbstractAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
Assert.notNull(requiresAuthenticationRequestMatcher, "requiresAuthenticationRequestMatcher cannot be null");
this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
}
//~ Methods ========================================================================================================
@Override
public void afterPropertiesSet() {
Assert.hasLength(filterProcessesUrl, "filterProcessesUrl must be specified");
Assert.isTrue(UrlUtils.isValidRedirectUrl(filterProcessesUrl), filterProcessesUrl + " isn't a valid redirect URL");
Assert.notNull(authenticationManager, "authenticationManager must be specified");
if (rememberMeServices == null) {
@ -231,21 +246,11 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
* Subclasses may override for special requirements, such as Tapestry integration.
*
* @return <code>true</code> if the filter should attempt authentication, <code>false</code> otherwise.
* @deprecated use {@link #setRequiresAuthenticationRequestMatcher(RequestMatcher)} instead
*/
@Deprecated
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
String uri = request.getRequestURI();
int pathParamIndex = uri.indexOf(';');
if (pathParamIndex > 0) {
// strip everything after the first semi-colon
uri = uri.substring(0, pathParamIndex);
}
if ("".equals(request.getContextPath())) {
return uri.endsWith(filterProcessesUrl);
}
return uri.endsWith(request.getContextPath() + filterProcessesUrl);
return requiresAuthenticationRequestMatcher.matches(request);
}
/**
@ -358,14 +363,29 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
this.authenticationManager = authenticationManager;
}
@Deprecated
public String getFilterProcessesUrl() {
return filterProcessesUrl;
}
/**
* Sets the URL that determines if authentication is required
*
* @param filterProcessesUrl
* @deprecated use {@link #setRequiresAuthenticationRequestMatcher(RequestMatcher)} instead
*/
@Deprecated
public void setFilterProcessesUrl(String filterProcessesUrl) {
this.requiresAuthenticationRequestMatcher = new FilterProcessUrlRequestMatcher(filterProcessesUrl);
this.filterProcessesUrl = filterProcessesUrl;
}
public final void setRequiresAuthenticationRequestMatcher(RequestMatcher requestMatcher) {
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
this.filterProcessesUrl = null;
this.requiresAuthenticationRequestMatcher = requestMatcher;
}
public RememberMeServices getRememberMeServices() {
return rememberMeServices;
}
@ -439,4 +459,31 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
protected AuthenticationFailureHandler getFailureHandler() {
return failureHandler;
}
private static final class FilterProcessUrlRequestMatcher implements RequestMatcher {
private final String filterProcessesUrl;
private FilterProcessUrlRequestMatcher(String filterProcessesUrl) {
Assert.hasLength(filterProcessesUrl, "filterProcessesUrl must be specified");
Assert.isTrue(UrlUtils.isValidRedirectUrl(filterProcessesUrl), filterProcessesUrl + " isn't a valid redirect URL");
this.filterProcessesUrl = filterProcessesUrl;
}
@Override
public boolean matches(HttpServletRequest request) {
String uri = request.getRequestURI();
int pathParamIndex = uri.indexOf(';');
if (pathParamIndex > 0) {
// strip everything after the first semi-colon
uri = uri.substring(0, pathParamIndex);
}
if ("".equals(request.getContextPath())) {
return uri.endsWith(filterProcessesUrl);
}
return uri.endsWith(request.getContextPath() + filterProcessesUrl);
}
}
}

View File

@ -30,7 +30,7 @@ import org.springframework.security.web.WebAttributes;
import org.springframework.web.filter.GenericFilterBean;
/**
* This class generates a default login page if one was not specified. The class is quite similar
* This class generates a default login page if one was not specified.
*
* @author Rob Winch
* @since 3.2
@ -202,7 +202,7 @@ public class DefaultLoginPageViewFilter extends GenericFilterBean {
}
private boolean matches(HttpServletRequest request, String url) {
if(!"GET".equals(request.getMethod())) {
if(!"GET".equals(request.getMethod()) || url == null) {
return false;
}
String uri = request.getRequestURI();

View File

@ -213,10 +213,9 @@ public class AbstractAuthenticationProcessingFilterTests {
filter.setAuthenticationFailureHandler(failureHandler);
filter.setAuthenticationManager(mock(AuthenticationManager.class));
filter.setAuthenticationSuccessHandler(successHandler);
filter.setFilterProcessesUrl(null);
try {
filter.afterPropertiesSet();
filter.setFilterProcessesUrl(null);
fail("Should have thrown IllegalArgumentException");
} catch (IllegalArgumentException expected) {
assertEquals("filterProcessesUrl must be specified", expected.getMessage());