Переглянути джерело

MvcRequestMatcher servletPath Polish / XML Config

Fixes gh-4014
Joe Grandja 9 роки тому
батько
коміт
e080905a79

+ 6 - 59
config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java

@@ -15,15 +15,6 @@
  */
 package org.springframework.security.config.annotation.web;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.List;
-
-import javax.servlet.ServletContext;
-import javax.servlet.ServletRegistration;
-
-import org.springframework.beans.factory.SmartInitializingSingleton;
 import org.springframework.context.ApplicationContext;
 import org.springframework.http.HttpMethod;
 import org.springframework.security.config.annotation.ObjectPostProcessor;
@@ -34,9 +25,12 @@ import org.springframework.security.web.util.matcher.AnyRequestMatcher;
 import org.springframework.security.web.util.matcher.RegexRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.util.ClassUtils;
-import org.springframework.web.context.ServletContextAware;
 import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
 /**
  * A base class for registering {@link RequestMatcher}'s. For example, it might allow for
  * specifying which {@link RequestMatcher} require a certain level of authorization.
@@ -171,12 +165,9 @@ public abstract class AbstractRequestMatcherRegistry<C> {
 		List<MvcRequestMatcher> matchers = new ArrayList<MvcRequestMatcher>(
 				mvcPatterns.length);
 		for (String mvcPattern : mvcPatterns) {
-			MvcRequestMatcher matcher;
-			if(isServlet30) {
-				matcher = new ServletPathValidatingtMvcRequestMatcher(introspector, mvcPattern);
+			MvcRequestMatcher matcher = new MvcRequestMatcher(introspector, mvcPattern);
+			if (isServlet30) {
 				opp.postProcess(matcher);
-			} else {
-				matcher = new MvcRequestMatcher(introspector, mvcPattern);
 			}
 			if (method != null) {
 				matcher.setMethod(method);
@@ -316,48 +307,4 @@ public abstract class AbstractRequestMatcherRegistry<C> {
 		}
 	}
 
-	static class ServletPathValidatingtMvcRequestMatcher extends MvcRequestMatcher implements SmartInitializingSingleton, ServletContextAware {
-		private ServletContext servletContext;
-
-		/**
-		 * @param introspector
-		 * @param pattern
-		 */
-		public ServletPathValidatingtMvcRequestMatcher(HandlerMappingIntrospector introspector,
-				String pattern) {
-			super(introspector, pattern);
-		}
-
-		/* (non-Javadoc)
-		 * @see org.springframework.beans.factory.SmartInitializingSingleton#afterSingletonsInstantiated()
-		 */
-		@Override
-		public void afterSingletonsInstantiated() {
-			if(getServletPath() != null) {
-				return;
-			}
-			Collection<? extends ServletRegistration> registrations = servletContext.getServletRegistrations().values();
-			for(ServletRegistration registration : registrations) {
-				Collection<String> mappings = registration.getMappings();
-				for(String mapping : mappings) {
-					if(mapping.startsWith("/") && mapping.length() > 1) {
-						throw new IllegalStateException(
-								"servletPath must not be null for mvcPattern \"" + getMvcPattern()
-										+ "\" when providing a servlet mapping of "
-										+ mapping + " for servlet "
-										+ registration.getClassName());
-					}
-				}
-			}
-		}
-
-		/* (non-Javadoc)
-		 * @see org.springframework.web.context.ServletContextAware#setServletContext(javax.servlet.ServletContext)
-		 */
-		@Override
-		public void setServletContext(ServletContext servletContext) {
-			this.servletContext = servletContext;
-		}
-
-	}
 }

+ 1 - 1
config/src/main/resources/org/springframework/security/config/spring-security-4.1.xsd

@@ -1351,7 +1351,7 @@
                 'mvc'. In addition, the value is only required in the following 2 use cases: 1) There are
                 2 or more HttpServlet's registered in the ServletContext that have mappings starting with
                 '/' and are different; 2) The pattern starts with the same value of a registered
-                HttpServlet path, excluding the default (root) HttpServlet '/'
+                HttpServlet path, excluding the default (root) HttpServlet '/'.
                 </xs:documentation>
          </xs:annotation>
       </xs:attribute>

+ 3 - 0
config/src/main/resources/org/springframework/security/config/spring-security-4.2.rnc

@@ -381,6 +381,9 @@ intercept-url.attlist &=
 intercept-url.attlist &=
 	## Used to specify that a URL must be accessed over http or https, or that there is no preference. The value should be "http", "https" or "any", respectively.
 	attribute requires-channel {xsd:token}?
+intercept-url.attlist &=
+	## The path to the servlet. This attribute is only applicable when 'request-matcher' is 'mvc'. In addition, the value is only required in the following 2 use cases: 1) There are 2 or more HttpServlet's registered in the ServletContext that have mappings starting with '/' and are different; 2) The pattern starts with the same value of a registered HttpServlet path, excluding the default (root) HttpServlet '/'.
+	attribute servlet-path {xsd:token}?
 
 logout =
 	## Incorporates a logout processing filter. Most web applications require a logout filter, although you may not require one if you write a controller to provider similar logic.

+ 10 - 0
config/src/main/resources/org/springframework/security/config/spring-security-4.2.xsd

@@ -1345,6 +1345,16 @@
                 </xs:documentation>
          </xs:annotation>
       </xs:attribute>
+      <xs:attribute name="servlet-path" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>The path to the servlet. This attribute is only applicable when 'request-matcher' is
+                'mvc'. In addition, the value is only required in the following 2 use cases: 1) There are
+                2 or more HttpServlet's registered in the ServletContext that have mappings starting with
+                '/' and are different; 2) The pattern starts with the same value of a registered
+                HttpServlet path, excluding the default (root) HttpServlet '/'.
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
   </xs:attributeGroup>
   
   <xs:attributeGroup name="logout.attlist">

+ 33 - 11
config/src/test/groovy/org/springframework/security/config/AbstractXmlConfigTests.groovy

@@ -1,26 +1,25 @@
 package org.springframework.security.config
 
 import groovy.xml.MarkupBuilder
-
-import org.mockito.Mockito;
-import org.springframework.context.support.AbstractXmlApplicationContext
+import org.mockito.Mockito
+import org.springframework.context.ApplicationListener
+import org.springframework.context.support.AbstractRefreshableApplicationContext
+import org.springframework.mock.web.MockServletContext
+import org.springframework.security.CollectingAppListener
 import org.springframework.security.config.util.InMemoryXmlApplicationContext
+import org.springframework.security.config.util.InMemoryXmlWebApplicationContext
 import org.springframework.security.core.context.SecurityContextHolder
 import spock.lang.Specification
-import static org.springframework.security.config.ConfigTestUtils.AUTH_PROVIDER_XML
-import org.springframework.context.ApplicationListener
-import org.springframework.context.ApplicationEvent
-import org.springframework.security.authentication.event.AbstractAuthenticationEvent
-import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent
-import org.springframework.security.access.event.AbstractAuthorizationEvent
-import org.springframework.security.CollectingAppListener
 
+import javax.servlet.ServletContext
+
+import static org.springframework.security.config.ConfigTestUtils.AUTH_PROVIDER_XML
 /**
  *
  * @author Luke Taylor
  */
 abstract class AbstractXmlConfigTests extends Specification {
-	AbstractXmlApplicationContext appContext;
+	AbstractRefreshableApplicationContext appContext;
 	Writer writer;
 	MarkupBuilder xml;
 	ApplicationListener appListener;
@@ -81,4 +80,27 @@ abstract class AbstractXmlConfigTests extends Specification {
 		appContext = new InMemoryXmlApplicationContext(writer.toString() + extraXml);
 		appContext.addApplicationListener(appListener);
 	}
+
+	def createWebAppContext() {
+		createWebAppContext(AUTH_PROVIDER_XML);
+	}
+
+	def createWebAppContext(ServletContext servletContext) {
+		createWebAppContext(AUTH_PROVIDER_XML, servletContext);
+	}
+
+	def createWebAppContext(String extraXml) {
+		createWebAppContext(extraXml, null);
+	}
+
+	def createWebAppContext(String extraXml, ServletContext servletContext) {
+		appContext = new InMemoryXmlWebApplicationContext(writer.toString() + extraXml);
+		appContext.addApplicationListener(appListener);
+		if (servletContext != null) {
+			appContext.setServletContext(servletContext);
+		} else {
+			appContext.setServletContext(new MockServletContext());
+		}
+		appContext.refresh();
+	}
 }

+ 106 - 79
config/src/test/groovy/org/springframework/security/config/doc/XsdDocumentedTests.groovy

@@ -15,13 +15,11 @@
  */
 package org.springframework.security.config.doc
 
-import groovy.util.slurpersupport.GPathResult;
-import groovy.util.slurpersupport.NodeChild
+import groovy.util.slurpersupport.GPathResult
+import spock.lang.*
 
 import org.springframework.security.config.http.SecurityFilters
 
-import spock.lang.*
-
 /**
  * Tests to ensure that the xsd is properly documented.
  *
@@ -29,7 +27,15 @@ import spock.lang.*
  */
 class XsdDocumentedTests extends Specification {
 
-	def ignoredIds = ['nsa-any-user-service','nsa-any-user-service-parents','nsa-authentication','nsa-websocket-security','nsa-ldap','nsa-method-security','nsa-web']
+	def ignoredIds = [
+		'nsa-any-user-service',
+		'nsa-any-user-service-parents',
+		'nsa-authentication',
+		'nsa-websocket-security',
+		'nsa-ldap',
+		'nsa-method-security',
+		'nsa-web'
+	]
 	@Shared def reference = new File('../docs/manual/src/docs/asciidoc/index.adoc')
 
 	@Shared File schema31xDocument = new File('src/main/resources/org/springframework/security/config/spring-security-3.1.xsd')
@@ -52,32 +58,53 @@ class XsdDocumentedTests extends Specification {
 
 	def 'SEC-2139: named-security-filter are all defined and ordered properly'() {
 		setup:
-			def expectedFilters = (EnumSet.allOf(SecurityFilters) as List).sort { it.order }
+		def expectedFilters = (EnumSet.allOf(SecurityFilters) as List).sort { it.order }
 		when:
-			def nsf = schemaRootElement.simpleType.find { it.@name == 'named-security-filter' }
-			def nsfValues = nsf.children().children().collect { c ->
-				Enum.valueOf(SecurityFilters, c.@value.toString())
-			}
+		def nsf = schemaRootElement.simpleType.find { it.@name == 'named-security-filter' }
+		def nsfValues = nsf.children().children().collect { c ->
+			Enum.valueOf(SecurityFilters, c.@value.toString())
+		}
 		then:
-			expectedFilters == nsfValues
+		expectedFilters == nsfValues
 	}
 
 	def 'SEC-2139: 3.1.x named-security-filter are all defined and ordered properly'() {
 		setup:
-			def expectedFilters = ["FIRST", "CHANNEL_FILTER", "SECURITY_CONTEXT_FILTER", "CONCURRENT_SESSION_FILTER", "LOGOUT_FILTER", "X509_FILTER",
-				"PRE_AUTH_FILTER", "CAS_FILTER", "FORM_LOGIN_FILTER", "OPENID_FILTER", "LOGIN_PAGE_FILTER", "DIGEST_AUTH_FILTER","BASIC_AUTH_FILTER",
-				"REQUEST_CACHE_FILTER", "SERVLET_API_SUPPORT_FILTER", "JAAS_API_SUPPORT_FILTER", "REMEMBER_ME_FILTER", "ANONYMOUS_FILTER",
-				"SESSION_MANAGEMENT_FILTER", "EXCEPTION_TRANSLATION_FILTER", "FILTER_SECURITY_INTERCEPTOR", "SWITCH_USER_FILTER", "LAST"].collect {
-				Enum.valueOf(SecurityFilters, it)
-			}
-			def schema31xRootElement = new XmlSlurper().parse(schema31xDocument)
+		def expectedFilters = [
+			"FIRST",
+			"CHANNEL_FILTER",
+			"SECURITY_CONTEXT_FILTER",
+			"CONCURRENT_SESSION_FILTER",
+			"LOGOUT_FILTER",
+			"X509_FILTER",
+			"PRE_AUTH_FILTER",
+			"CAS_FILTER",
+			"FORM_LOGIN_FILTER",
+			"OPENID_FILTER",
+			"LOGIN_PAGE_FILTER",
+			"DIGEST_AUTH_FILTER",
+			"BASIC_AUTH_FILTER",
+			"REQUEST_CACHE_FILTER",
+			"SERVLET_API_SUPPORT_FILTER",
+			"JAAS_API_SUPPORT_FILTER",
+			"REMEMBER_ME_FILTER",
+			"ANONYMOUS_FILTER",
+			"SESSION_MANAGEMENT_FILTER",
+			"EXCEPTION_TRANSLATION_FILTER",
+			"FILTER_SECURITY_INTERCEPTOR",
+			"SWITCH_USER_FILTER",
+			"LAST"
+		].collect {
+			Enum.valueOf(SecurityFilters, it)
+		}
+		def schema31xRootElement = new XmlSlurper().parse(schema31xDocument)
 		when:
-			def nsf = schema31xRootElement.simpleType.find { it.@name == 'named-security-filter' }
-			def nsfValues = nsf.children().children().collect { c ->
-				Enum.valueOf(SecurityFilters, c.@value.toString())
-			}
+		def nsf = schema31xRootElement.simpleType.find { it.@name == 'named-security-filter' }
+		def nsfValues = nsf.children().children().collect { c ->
+			Enum.valueOf(SecurityFilters, c.@value.toString())
+		}
 		then:
-			expectedFilters == nsfValues
+		expectedFilters == nsfValues
 	}
 
 	/**
@@ -100,22 +127,22 @@ class XsdDocumentedTests extends Specification {
 	 */
 	def 'the entire schema is included in the appendix documentation'() {
 		setup: 'get all the documented ids and the expected ids'
-			def documentedIds = []
-			reference.eachLine { line ->
-				if(line.matches("\\[\\[(nsa-.*)\\]\\]")) {
-					documentedIds.add(line.substring(2,line.length() - 2))
-				}
+		def documentedIds = []
+		reference.eachLine { line ->
+			if(line.matches("\\[\\[(nsa-.*)\\]\\]")) {
+				documentedIds.add(line.substring(2,line.length() - 2))
 			}
+		}
 		when: 'the schema is compared to the appendix documentation'
-			def expectedIds = [] as Set
-			elementNameToElement*.value*.ids*.each { expectedIds.addAll it }
-			documentedIds.removeAll ignoredIds
-			expectedIds.removeAll ignoredIds
-			def undocumentedIds = (expectedIds - documentedIds)
-			def shouldNotBeDocumented = (documentedIds - expectedIds)
+		def expectedIds = [] as Set
+		elementNameToElement*.value*.ids*.each { expectedIds.addAll it }
+		documentedIds.removeAll ignoredIds
+		expectedIds.removeAll ignoredIds
+		def undocumentedIds = (expectedIds - documentedIds)
+		def shouldNotBeDocumented = (documentedIds - expectedIds)
 		then: 'all the elements and attributes are documented'
-			shouldNotBeDocumented.empty
-			undocumentedIds.empty
+		shouldNotBeDocumented.empty
+		undocumentedIds.empty
 	}
 
 	/**
@@ -125,55 +152,55 @@ class XsdDocumentedTests extends Specification {
 	 */
 	def 'validate parents and children are linked in the appendix documentation'() {
 		when: "get all the links for each element's children and parents"
-			def docAttrNameToChildren = [:]
-			def docAttrNameToParents = [:]
-
-			def currentDocAttrNameToElmt
-			def docAttrName
-
-			reference.eachLine { line ->
-				if(line.matches('^\\[\\[.*\\]\\]$')) {
-					def id = line.substring(2,line.length() - 2)
-					if(id.endsWith("-children")) {
-						docAttrName = id.substring(0,id.length() - 9)
-						currentDocAttrNameToElmt = docAttrNameToChildren
-					} else if(id.endsWith("-parents")) {
-						docAttrName = id.substring(0,id.length() - 8)
-						currentDocAttrNameToElmt = docAttrNameToParents
-					} else if(docAttrName && !id.startsWith(docAttrName)) {
-						currentDocAttrNameToElmt = null
-						docAttrName = null
-					}
-				}
-
-				if(docAttrName) {
-					def expression = '^\\* <<(nsa-.*),.*>>$'
-					if(line.matches(expression)) {
-						String elmtId = line.replaceAll(expression, '$1')
-						currentDocAttrNameToElmt.get(docAttrName, []).add(elmtId)
-					}
+		def docAttrNameToChildren = [:]
+		def docAttrNameToParents = [:]
+
+		def currentDocAttrNameToElmt
+		def docAttrName
+
+		reference.eachLine { line ->
+			if(line.matches('^\\[\\[.*\\]\\]$')) {
+				def id = line.substring(2,line.length() - 2)
+				if(id.endsWith("-children")) {
+					docAttrName = id.substring(0,id.length() - 9)
+					currentDocAttrNameToElmt = docAttrNameToChildren
+				} else if(id.endsWith("-parents")) {
+					docAttrName = id.substring(0,id.length() - 8)
+					currentDocAttrNameToElmt = docAttrNameToParents
+				} else if(docAttrName && !id.startsWith(docAttrName)) {
+					currentDocAttrNameToElmt = null
+					docAttrName = null
 				}
 			}
 
-			def schemaAttrNameToParents = [:]
-			def schemaAttrNameToChildren = [:]
-			elementNameToElement.each { entry ->
-				def key = 'nsa-'+entry.key
-				if(ignoredIds.contains(key)) {
-					return
-				}
-				def parentIds = entry.value.allParentElmts.values()*.id.findAll { !ignoredIds.contains(it) }.sort()
-				if(parentIds) {
-					schemaAttrNameToParents.put(key,parentIds)
-				}
-				def childIds = entry.value.allChildElmts.values()*.id.findAll { !ignoredIds.contains(it) }.sort()
-				if(childIds) {
-					schemaAttrNameToChildren.put(key,childIds)
+			if(docAttrName) {
+				def expression = '^\\* <<(nsa-.*),.*>>$'
+				if(line.matches(expression)) {
+					String elmtId = line.replaceAll(expression, '$1')
+					currentDocAttrNameToElmt.get(docAttrName, []).add(elmtId)
 				}
 			}
+		}
+
+		def schemaAttrNameToParents = [:]
+		def schemaAttrNameToChildren = [:]
+		elementNameToElement.each { entry ->
+			def key = 'nsa-'+entry.key
+			if(ignoredIds.contains(key)) {
+				return
+			}
+			def parentIds = entry.value.allParentElmts.values()*.id.findAll { !ignoredIds.contains(it) }.sort()
+			if(parentIds) {
+				schemaAttrNameToParents.put(key,parentIds)
+			}
+			def childIds = entry.value.allChildElmts.values()*.id.findAll { !ignoredIds.contains(it) }.sort()
+			if(childIds) {
+				schemaAttrNameToChildren.put(key,childIds)
+			}
+		}
 		then: "the expected parents and children are all documented"
-			schemaAttrNameToChildren.sort() == docAttrNameToChildren.sort()
-			schemaAttrNameToParents.sort() == docAttrNameToParents.sort()
+		schemaAttrNameToChildren.sort() == docAttrNameToChildren.sort()
+		schemaAttrNameToParents.sort() == docAttrNameToParents.sort()
 	}
 
 	/**

+ 56 - 3
config/src/test/groovy/org/springframework/security/config/http/InterceptUrlConfigTests.groovy

@@ -15,17 +15,25 @@
  */
 package org.springframework.security.config.http
 
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.stubbing.Answer
+import org.springframework.beans.factory.BeanCreationException
 import org.springframework.beans.factory.parsing.BeanDefinitionParsingException
 import org.springframework.mock.web.MockFilterChain
 import org.springframework.mock.web.MockHttpServletRequest
 import org.springframework.mock.web.MockHttpServletResponse
+import org.springframework.mock.web.MockServletContext
 import org.springframework.security.access.SecurityConfig
 import org.springframework.security.crypto.codec.Base64
 import org.springframework.security.web.access.intercept.FilterSecurityInterceptor
 import org.springframework.web.bind.annotation.RequestMapping
 import org.springframework.web.bind.annotation.RestController
 
+import javax.servlet.ServletContext
+import javax.servlet.ServletRegistration
 import javax.servlet.http.HttpServletResponse
+
+import static org.mockito.Mockito.*
 /**
  *
  * @author Rob Winch
@@ -199,6 +207,7 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests {
 
 	def "intercept-url supports mvc matchers"() {
 		setup:
+		MockServletContext servletContext = mockServletContext();
 		MockHttpServletRequest request = new MockHttpServletRequest(method:'GET')
 		MockHttpServletResponse response = new MockHttpServletResponse()
 		MockFilterChain chain = new MockFilterChain()
@@ -209,7 +218,7 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests {
 		bean('pathController',PathController)
 		xml.'mvc:annotation-driven'()
 
-		createAppContext()
+		createWebAppContext(servletContext)
 		when:
 		request.servletPath = "/path"
 		springSecurityFilterChain.doFilter(request, response, chain)
@@ -235,6 +244,7 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests {
 
 	def "intercept-url mvc supports path variables"() {
 		setup:
+		MockServletContext servletContext = mockServletContext();
 		MockHttpServletRequest request = new MockHttpServletRequest(method:'GET')
 		MockHttpServletResponse response = new MockHttpServletResponse()
 		MockFilterChain chain = new MockFilterChain()
@@ -242,7 +252,7 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests {
 			'http-basic'()
 			'intercept-url'(pattern: '/user/{un}/**', access: "#un == 'user'")
 		}
-		createAppContext()
+		createWebAppContext(servletContext)
 		when: 'user can access'
 		request.servletPath = '/user/user/abc'
 		springSecurityFilterChain.doFilter(request,response,chain)
@@ -266,6 +276,7 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests {
 
 	def "intercept-url mvc matchers with servlet path"() {
 		setup:
+		MockServletContext servletContext = mockServletContext("/spring");
 		MockHttpServletRequest request = new MockHttpServletRequest(method:'GET')
 		MockHttpServletResponse response = new MockHttpServletResponse()
 		MockFilterChain chain = new MockFilterChain()
@@ -275,7 +286,7 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests {
 		}
 		bean('pathController',PathController)
 		xml.'mvc:annotation-driven'()
-		createAppContext()
+		createWebAppContext(servletContext)
 		when:
 		request.servletPath = "/spring"
 		request.requestURI = "/spring/path"
@@ -302,6 +313,30 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests {
 		response.status == HttpServletResponse.SC_UNAUTHORIZED
 	}
 
+	def "intercept-url mvc matchers servlet path required"() {
+		when:
+		MockServletContext servletContext = mockServletContext("/spring");
+		xml.http('request-matcher':'mvc') {
+			'http-basic'()
+			'intercept-url'(pattern: '/path', access: "denyAll")
+		}
+		createWebAppContext(servletContext)
+		then:
+		thrown(BeanCreationException)
+	}
+
+	def "intercept-url mvc matchers servlet path NOT required"() {
+		when:
+		MockServletContext servletContext = mockServletContext();
+		xml.http('request-matcher':'mvc') {
+			'http-basic'()
+			'intercept-url'(pattern: '/path', access: "denyAll")
+		}
+		createWebAppContext(servletContext)
+		then:
+		noExceptionThrown()
+	}
+
 	def "intercept-url ant matcher with servlet path fails"() {
 		when:
 		xml.http('request-matcher':'ant') {
@@ -352,6 +387,24 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests {
 		}
 	}
 
+	private ServletContext mockServletContext() {
+		return mockServletContext("/");
+	}
+
+	private ServletContext mockServletContext(String servletPath) {
+		MockServletContext servletContext = spy(new MockServletContext());
+		final ServletRegistration registration = mock(ServletRegistration.class);
+		when(registration.getMappings()).thenReturn(Collections.singleton(servletPath));
+		Answer<Map<String, ? extends ServletRegistration>> answer = new Answer<Map<String, ? extends ServletRegistration>>() {
+			@Override
+			public Map<String, ? extends ServletRegistration> answer(InvocationOnMock invocation) throws Throwable {
+				return Collections.<String, ServletRegistration>singletonMap("spring", registration);
+			}
+		};
+		when(servletContext.getServletRegistrations()).thenAnswer(answer);
+		return servletContext;
+	}
+
 	def login(MockHttpServletRequest request, String username, String password) {
 		String toEncode = username + ':' + password
 		request.addHeader('Authorization','Basic ' + new String(Base64.encode(toEncode.getBytes('UTF-8'))))

+ 16 - 21
config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java → config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java

@@ -16,14 +16,6 @@
 
 package org.springframework.security.config.annotation.web;
 
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Map;
-
-import javax.servlet.Registration;
-import javax.servlet.ServletContext;
-import javax.servlet.ServletRegistration;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -31,14 +23,17 @@ import org.mockito.Mock;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.runners.MockitoJUnitRunner;
 import org.mockito.stubbing.Answer;
-
 import org.springframework.mock.web.MockServletContext;
-import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry.ServletPathValidatingtMvcRequestMatcher;
+import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
 import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
 
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.when;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletRegistration;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+
+import static org.mockito.Mockito.*;
 
 /**
  * @author Rob Winch
@@ -49,34 +44,34 @@ public class AbstractRequestMatcherRegistryTests {
 	@Mock
 	HandlerMappingIntrospector introspector;
 
-	ServletPathValidatingtMvcRequestMatcher matcher;
+	MvcRequestMatcher matcher;
 
 	ServletContext servletContext;
 
 	@Before
 	public void setup() {
 		servletContext = spy(new MockServletContext());
-		matcher = new ServletPathValidatingtMvcRequestMatcher(introspector, "/foo");
+		matcher = new MvcRequestMatcher(introspector, "/foo");
 		matcher.setServletContext(servletContext);
 	}
 
 	@Test(expected = IllegalStateException.class)
-	public void servletPathValidatingtMvcRequestMatcherAfterSingletonsIntantiatedFailsWithSpringServlet() {
+	public void servletPathValidatingMvcRequestMatcherAfterPropertiesSetFailsWithSpringServlet() throws Exception {
 		setMappings("/spring");
-		matcher.afterSingletonsInstantiated();
+		matcher.afterPropertiesSet();
 	}
 
 	@Test
-	public void servletPathValidatingtMvcRequestMatcherAfterSingletonsIntantiatedWithSpringServlet() {
+	public void servletPathValidatingMvcRequestMatcherAfterPropertiesSetWithSpringServlet() throws Exception {
 		matcher.setServletPath("/spring");
 		setMappings("/spring");
-		matcher.afterSingletonsInstantiated();
+		matcher.afterPropertiesSet();
 	}
 
 	@Test
-	public void servletPathValidatingtMvcRequestMatcherAfterSingletonsIntantiatedDefaultServlet() {
+	public void servletPathValidatingMvcRequestMatcherAfterPropertiesSetDefaultServlet() throws Exception {
 		setMappings("/");
-		matcher.afterSingletonsInstantiated();
+		matcher.afterPropertiesSet();
 	}
 
 	private void setMappings(String... mappings) {

+ 3 - 2
config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java

@@ -27,6 +27,7 @@ import org.junit.Test;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
 
+import org.springframework.beans.factory.BeanCreationException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -449,7 +450,7 @@ public class AuthorizeRequestsTests {
 		}
 	}
 
-	@Test(expected = IllegalStateException.class)
+	@Test(expected = BeanCreationException.class)
 	public void mvcMatcherServletPathRequired() throws Exception {
 		final ServletRegistration registration = mock(ServletRegistration.class);
 		when(registration.getMappings()).thenReturn(Collections.singleton("/spring"));
@@ -502,4 +503,4 @@ public class AuthorizeRequestsTests {
 
 		this.context.getAutowireCapableBeanFactory().autowireBean(this);
 	}
-}
+}

+ 6 - 4
config/src/test/java/org/springframework/security/config/util/InMemoryXmlApplicationContext.java

@@ -25,7 +25,7 @@ import org.springframework.security.util.InMemoryResource;
  * @author Luke Taylor
  */
 public class InMemoryXmlApplicationContext extends AbstractXmlApplicationContext {
-	private static final String BEANS_OPENING = "<b:beans xmlns='http://www.springframework.org/schema/security'\n"
+	static final String BEANS_OPENING = "<b:beans xmlns='http://www.springframework.org/schema/security'\n"
 			+ "    xmlns:context='http://www.springframework.org/schema/context'\n"
 			+ "    xmlns:b='http://www.springframework.org/schema/beans'\n"
 			+ "    xmlns:aop='http://www.springframework.org/schema/aop'\n"
@@ -38,16 +38,18 @@ public class InMemoryXmlApplicationContext extends AbstractXmlApplicationContext
 			+ "http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd\n"
 			+ "http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd\n"
 			+ "http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-";
-	private static final String BEANS_CLOSE = "</b:beans>\n";
+	static final String BEANS_CLOSE = "</b:beans>\n";
+
+	static final String SPRING_SECURITY_VERSION = "4.1";
 
 	Resource inMemoryXml;
 
 	public InMemoryXmlApplicationContext(String xml) {
-		this(xml, "4.1", null);
+		this(xml, SPRING_SECURITY_VERSION, null);
 	}
 
 	public InMemoryXmlApplicationContext(String xml, ApplicationContext parent) {
-		this(xml, "4.1", parent);
+		this(xml, SPRING_SECURITY_VERSION, parent);
 	}
 
 	public InMemoryXmlApplicationContext(String xml, String secVersion,

+ 60 - 0
config/src/test/java/org/springframework/security/config/util/InMemoryXmlWebApplicationContext.java

@@ -0,0 +1,60 @@
+/*
+ * Copyright 2012-2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.config.util;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.io.Resource;
+import org.springframework.security.util.InMemoryResource;
+import org.springframework.web.context.support.AbstractRefreshableWebApplicationContext;
+
+import java.io.IOException;
+
+import static org.springframework.security.config.util.InMemoryXmlApplicationContext.BEANS_CLOSE;
+import static org.springframework.security.config.util.InMemoryXmlApplicationContext.BEANS_OPENING;
+import static org.springframework.security.config.util.InMemoryXmlApplicationContext.SPRING_SECURITY_VERSION;
+
+/**
+ * @author Joe Grandja
+ */
+public class InMemoryXmlWebApplicationContext extends AbstractRefreshableWebApplicationContext {
+	private Resource inMemoryXml;
+
+	public InMemoryXmlWebApplicationContext(String xml) {
+		this(xml, SPRING_SECURITY_VERSION, null);
+	}
+
+	public InMemoryXmlWebApplicationContext(String xml, ApplicationContext parent) {
+		this(xml, SPRING_SECURITY_VERSION, parent);
+	}
+
+	public InMemoryXmlWebApplicationContext(String xml, String secVersion,
+											ApplicationContext parent) {
+		String fullXml = BEANS_OPENING + secVersion + ".xsd'>\n" + xml + BEANS_CLOSE;
+		inMemoryXml = new InMemoryResource(fullXml);
+		setAllowBeanDefinitionOverriding(true);
+		setParent(parent);
+	}
+
+	@Override
+	protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
+		XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
+		reader.loadBeanDefinitions(new Resource[] { inMemoryXml });
+	}
+
+}

+ 47 - 1
web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java

@@ -16,17 +16,24 @@
 
 package org.springframework.security.web.servlet.util.matcher;
 
+import org.springframework.beans.factory.InitializingBean;
 import org.springframework.http.HttpMethod;
+import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.security.web.util.matcher.RequestVariablesExtractor;
 import org.springframework.util.AntPathMatcher;
+import org.springframework.util.ClassUtils;
 import org.springframework.util.PathMatcher;
+import org.springframework.web.context.ServletContextAware;
 import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
 import org.springframework.web.servlet.handler.MatchableHandlerMapping;
 import org.springframework.web.servlet.handler.RequestMatchResult;
 import org.springframework.web.util.UrlPathHelper;
 
+import javax.servlet.ServletContext;
+import javax.servlet.ServletRegistration;
 import javax.servlet.http.HttpServletRequest;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
 
@@ -45,13 +52,18 @@ import java.util.Map;
  * @since 4.1.1
  */
 public class MvcRequestMatcher
-		implements RequestMatcher, RequestVariablesExtractor {
+		implements RequestMatcher, RequestVariablesExtractor, InitializingBean, ServletContextAware {
+
+	private static final boolean isServlet30 = ClassUtils.isPresent(
+		"javax.servlet.ServletRegistration", MvcRequestMatcher.class.getClassLoader());
+
 	private final DefaultMatcher defaultMatcher = new DefaultMatcher();
 
 	private final HandlerMappingIntrospector introspector;
 	private final String pattern;
 	private HttpMethod method;
 	private String servletPath;
+	private ServletContext servletContext;
 
 	public MvcRequestMatcher(HandlerMappingIntrospector introspector, String pattern) {
 		this.introspector = introspector;
@@ -100,6 +112,40 @@ public class MvcRequestMatcher
 				: result.extractUriTemplateVariables();
 	}
 
+	/* (non-Javadoc)
+	 * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
+	 */
+	@Override
+	public void afterPropertiesSet() throws Exception {
+		// servletPath is required when at least one registered Servlet
+		// is mapped to a path other than the default (root) path '/'
+		if (this.servletContext == null || !isServlet30) {
+			return;
+		}
+		if (this.getServletPath() != null) {
+			return;
+		}
+		for (ServletRegistration registration : this.servletContext.getServletRegistrations().values()) {
+			for (String mapping : registration.getMappings()) {
+				if (mapping.startsWith("/") && mapping.length() > 1) {
+					throw new IllegalStateException(
+						"servletPath must not be null for mvcPattern \"" + this.getMvcPattern()
+							+ "\" when providing a servlet mapping of "
+							+ mapping + " for servlet "
+							+ registration.getClassName());
+				}
+			}
+		}
+	}
+
+	/* (non-Javadoc)
+	 * @see org.springframework.web.context.ServletContextAware#setServletContext(javax.servlet.ServletContext)
+	 */
+	@Override
+	public void setServletContext(ServletContext servletContext) {
+		this.servletContext = servletContext;
+	}
+
 	/**
 	 * @param method the method to set
 	 */