Skip to content

Commit afa4618

Browse files
committed
SPR-5690 - Request header filtering in @RequestMapping
1 parent b8b74db commit afa4618

File tree

5 files changed

+250
-16
lines changed

5 files changed

+250
-16
lines changed

org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java

+21
Original file line numberDiff line numberDiff line change
@@ -230,4 +230,25 @@
230230
*/
231231
String[] params() default {};
232232

233+
/**
234+
* The headers of the mapped request, narrowing the primary mapping.
235+
* <p>Same format for any environment: a sequence of "My-Header=myValue" style
236+
* expressions, with a request only mapped if each such header is found
237+
* to have the given value. "My-Header" style expressions are also supported,
238+
* with such headers having to be present in the request (allowed to have
239+
* any value). Finally, "!My-Header" style expressions indicate that the
240+
* specified header is <i>not</i> supposed to be present in the request.
241+
* <p>Also supports media type wildcards (*), for headers such as Accept
242+
* and Content-Type. For instance,
243+
* <pre>
244+
* &#064;RequestMapping(value = "/something", headers = "content-type=text/*")
245+
* </pre>
246+
* will match requests with a Content-Type of "text/html", "text/plain", etc.
247+
* <p><b>Supported at the type level as well as at the method level!</b>
248+
* When used at the type level, all method-level mappings inherit
249+
* this header restriction (i.e. the type-level restriction
250+
* gets checked before the handler method is even resolved).
251+
* @see org.springframework.http.MediaType
252+
*/
253+
String[] headers() default {};
233254
}

org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java

+25-12
Original file line numberDiff line numberDiff line change
@@ -396,12 +396,15 @@ public Method resolveHandlerMethod(HttpServletRequest request) throws ServletExc
396396
if (!hasTypeLevelMapping() || !Arrays.equals(mapping.params(), getTypeLevelMapping().params())) {
397397
mappingInfo.params = mapping.params();
398398
}
399+
if (!hasTypeLevelMapping() || !Arrays.equals(mapping.headers(), getTypeLevelMapping().headers())) {
400+
mappingInfo.headers = mapping.headers();
401+
}
399402
boolean match = false;
400403
if (mappingInfo.paths.length > 0) {
401404
List<String> matchedPaths = new ArrayList<String>(mappingInfo.paths.length);
402405
for (String methodLevelPattern : mappingInfo.paths) {
403406
if (isPathMatch(methodLevelPattern, lookupPath)) {
404-
if (checkParameters(mappingInfo, request)) {
407+
if (mappingInfo.matches(request)) {
405408
match = true;
406409
matchedPaths.add(methodLevelPattern);
407410
}
@@ -418,7 +421,7 @@ public Method resolveHandlerMethod(HttpServletRequest request) throws ServletExc
418421
}
419422
else {
420423
// No paths specified: parameter match sufficient.
421-
match = checkParameters(mappingInfo, request);
424+
match = mappingInfo.matches(request);
422425
if (match && mappingInfo.methods.length == 0 && mappingInfo.params.length == 0 &&
423426
resolvedMethodName != null && !resolvedMethodName.equals(handlerMethod.getName())) {
424427
match = false;
@@ -514,11 +517,6 @@ private boolean isPathMatchInternal(String pattern, String lookupPath) {
514517
return false;
515518
}
516519

517-
private boolean checkParameters(RequestMappingInfo mapping, HttpServletRequest request) {
518-
return ServletAnnotationMappingUtils.checkRequestMethod(mapping.methods, request) &&
519-
ServletAnnotationMappingUtils.checkParameters(mapping.params, request);
520-
}
521-
522520
@SuppressWarnings("unchecked")
523521
private void extractHandlerMethodUriTemplates(String mappedPath,
524522
String lookupPath,
@@ -726,21 +724,29 @@ static class RequestMappingInfo {
726724

727725
String[] params = new String[0];
728726

727+
String[] headers = new String[0];
728+
729729
String bestMatchedPath() {
730730
return matchedPaths.length > 0 ? matchedPaths[0] : null;
731731
}
732732

733+
public boolean matches(HttpServletRequest request) {
734+
return ServletAnnotationMappingUtils.checkRequestMethod(this.methods, request) &&
735+
ServletAnnotationMappingUtils.checkParameters(this.params, request) &&
736+
ServletAnnotationMappingUtils.checkHeaders(this.headers, request);
737+
}
738+
733739
@Override
734740
public boolean equals(Object obj) {
735741
RequestMappingInfo other = (RequestMappingInfo) obj;
736742
return (Arrays.equals(this.paths, other.paths) && Arrays.equals(this.methods, other.methods) &&
737-
Arrays.equals(this.params, other.params));
743+
Arrays.equals(this.params, other.params) && Arrays.equals(this.headers, other.headers));
738744
}
739745

740746
@Override
741747
public int hashCode() {
742-
return (Arrays.hashCode(this.paths) * 29 + Arrays.hashCode(this.methods) * 31 +
743-
Arrays.hashCode(this.params));
748+
return (Arrays.hashCode(this.paths) * 23 + Arrays.hashCode(this.methods) * 29 +
749+
Arrays.hashCode(this.params) * 31 + Arrays.hashCode(this.headers));
744750
}
745751
}
746752

@@ -777,14 +783,21 @@ else if (info2MethodCount == 0 && info1MethodCount > 0) {
777783
}
778784
else if (info1MethodCount == 1 & info2MethodCount > 1) {
779785
return -1;
780-
781786
}
782787
else if (info2MethodCount == 1 & info1MethodCount > 1) {
783788
return 1;
784789
}
785790
int info1ParamCount = info1.params.length;
786791
int info2ParamCount = info2.params.length;
787-
return (info1ParamCount < info2ParamCount ? 1 : (info1ParamCount == info2ParamCount ? 0 : -1));
792+
if (info1ParamCount != info2ParamCount) {
793+
return info2ParamCount - info1ParamCount;
794+
}
795+
int info1HeaderCount = info1.headers.length;
796+
int info2HeaderCount = info2.headers.length;
797+
if (info1HeaderCount != info2HeaderCount) {
798+
return info2HeaderCount - info1HeaderCount;
799+
}
800+
return 0;
788801
}
789802
}
790803

org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtils.java

+64-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616

1717
package org.springframework.web.servlet.mvc.annotation;
1818

19+
import java.util.Iterator;
20+
import java.util.List;
1921
import javax.servlet.http.HttpServletRequest;
2022

23+
import org.springframework.http.MediaType;
2124
import org.springframework.util.ObjectUtils;
2225
import org.springframework.web.bind.annotation.RequestMethod;
2326
import org.springframework.web.util.WebUtils;
@@ -32,6 +35,7 @@ abstract class ServletAnnotationMappingUtils {
3235

3336
/**
3437
* Check whether the given request matches the specified request methods.
38+
*
3539
* @param methods the HTTP request methods to check against
3640
* @param request the current HTTP request to check
3741
*/
@@ -49,8 +53,8 @@ public static boolean checkRequestMethod(RequestMethod[] methods, HttpServletReq
4953

5054
/**
5155
* Check whether the given request matches the specified parameter conditions.
52-
* @param params the parameter conditions, following
53-
* {@link org.springframework.web.bind.annotation.RequestMapping#params()}
56+
*
57+
* @param params the parameter conditions, following {@link org.springframework.web.bind.annotation.RequestMapping#params()}
5458
* @param request the current HTTP request to check
5559
*/
5660
public static boolean checkParameters(String[] params, HttpServletRequest request) {
@@ -79,4 +83,62 @@ else if (!WebUtils.hasSubmitParameter(request, param)) {
7983
return true;
8084
}
8185

86+
/**
87+
* Check whether the given request matches the specified header conditions.
88+
*
89+
* @param headers the header conditions, following {@link org.springframework.web.bind.annotation.RequestMapping#headers()}
90+
* @param request the current HTTP request to check
91+
*/
92+
public static boolean checkHeaders(String[] headers, HttpServletRequest request) {
93+
if (!ObjectUtils.isEmpty(headers)) {
94+
for (String header : headers) {
95+
int separator = header.indexOf('=');
96+
if (separator == -1) {
97+
if (header.startsWith("!")) {
98+
if (hasHeader(request, header.substring(1))) {
99+
return false;
100+
}
101+
}
102+
else if (!hasHeader(request, header)) {
103+
return false;
104+
}
105+
}
106+
else {
107+
String key = header.substring(0, separator);
108+
String value = header.substring(separator + 1);
109+
if (isMediaTypeHeader(key)) {
110+
List<MediaType> requestMediaTypes = MediaType.parseMediaTypes(request.getHeader(key));
111+
List<MediaType> valueMediaTypes = MediaType.parseMediaTypes(value);
112+
boolean found = false;
113+
for (Iterator<MediaType> valIter = valueMediaTypes.iterator(); valIter.hasNext() && !found;) {
114+
MediaType valueMediaType = valIter.next();
115+
for (Iterator<MediaType> reqIter = requestMediaTypes.iterator(); reqIter.hasNext() && !found;) {
116+
MediaType requestMediaType = reqIter.next();
117+
if (valueMediaType.includes(requestMediaType)) {
118+
found = true;
119+
}
120+
}
121+
122+
}
123+
if (!found) {
124+
return false;
125+
}
126+
}
127+
else if (!value.equals(request.getHeader(key))) {
128+
return false;
129+
}
130+
}
131+
}
132+
}
133+
return true;
134+
}
135+
136+
private static boolean hasHeader(HttpServletRequest request, String headerName) {
137+
return request.getHeader(headerName) != null;
138+
}
139+
140+
private static boolean isMediaTypeHeader(String headerName) {
141+
return "Accept".equalsIgnoreCase(headerName) || "Content-Type".equalsIgnoreCase(headerName);
142+
}
143+
82144
}

org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java

+31
Original file line numberDiff line numberDiff line change
@@ -894,6 +894,23 @@ protected WebApplicationContext createWebApplicationContext(WebApplicationContex
894894
assertEquals("Invalid response status code", HttpServletResponse.SC_BAD_REQUEST, response.getStatus());
895895
}
896896

897+
@Test
898+
public void headers() throws ServletException, IOException {
899+
initServlet(HeadersController.class);
900+
901+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/something");
902+
request.addHeader("Content-Type", "application/pdf");
903+
MockHttpServletResponse response = new MockHttpServletResponse();
904+
servlet.service(request, response);
905+
assertEquals("pdf", response.getContentAsString());
906+
907+
request = new MockHttpServletRequest("GET", "/something");
908+
request.addHeader("Content-Type", "text/html");
909+
response = new MockHttpServletResponse();
910+
servlet.service(request, response);
911+
assertEquals("text", response.getContentAsString());
912+
}
913+
897914
/*
898915
* Controllers
899916
*/
@@ -1472,6 +1489,20 @@ public void handle(@RequestBody String body, Writer writer) throws IOException {
14721489
}
14731490
}
14741491

1492+
@Controller
1493+
public static class HeadersController {
1494+
1495+
@RequestMapping(value = "/something", headers = "content-type=application/pdf")
1496+
public void handlePdf(Writer writer) throws IOException {
1497+
writer.write("pdf");
1498+
}
1499+
1500+
@RequestMapping(value = "/something", headers = "content-type=text/*")
1501+
public void handleHtml(Writer writer) throws IOException {
1502+
writer.write("text");
1503+
}
1504+
}
1505+
14751506

14761507
public static class MyMessageConverter implements HttpMessageConverter {
14771508

org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtilsTests.java

+109-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
1+
/*
2+
* Copyright 2002-2009 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
package org.springframework.web.servlet.mvc.annotation;
218

3-
import static org.junit.Assert.assertFalse;
4-
import static org.junit.Assert.assertTrue;
19+
import static org.junit.Assert.*;
520
import org.junit.Test;
621

722
import org.springframework.mock.web.MockHttpServletRequest;
@@ -26,4 +41,96 @@ public void checkRequestMethodNoMatch() {
2641
assertFalse("Invalid request method result", result);
2742
}
2843

44+
@Test
45+
public void checkParametersSimpleMatch() {
46+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
47+
request.addParameter("param1", "value1");
48+
String[] params = new String[]{"param1", "!param2"};
49+
boolean result = ServletAnnotationMappingUtils.checkParameters(params, request);
50+
assertTrue("Invalid request method result", result);
51+
}
52+
53+
@Test
54+
public void checkParametersSimpleNoMatch() {
55+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
56+
request.addParameter("param1", "value1");
57+
request.addParameter("param2", "value2");
58+
String[] params = new String[]{"param1", "!param2"};
59+
boolean result = ServletAnnotationMappingUtils.checkParameters(params, request);
60+
assertFalse("Invalid request method result", result);
61+
}
62+
63+
@Test
64+
public void checkParametersKeyValueMatch() {
65+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
66+
request.addParameter("param1", "value1");
67+
String[] params = new String[]{"param1=value1"};
68+
boolean result = ServletAnnotationMappingUtils.checkParameters(params, request);
69+
assertTrue("Invalid request method result", result);
70+
}
71+
72+
@Test
73+
public void checkParametersKeyValueNoMatch() {
74+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
75+
request.addParameter("param1", "value1");
76+
String[] params = new String[]{"param1=foo"};
77+
boolean result = ServletAnnotationMappingUtils.checkParameters(params, request);
78+
assertFalse("Invalid request method result", result);
79+
}
80+
81+
@Test
82+
public void checkHeadersSimpleMatch() {
83+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
84+
request.addHeader("header1", "value1");
85+
String[] headers = new String[]{"header1", "!header2"};
86+
boolean result = ServletAnnotationMappingUtils.checkHeaders(headers, request);
87+
assertTrue("Invalid request method result", result);
88+
}
89+
90+
@Test
91+
public void checkHeadersSimpleNoMatch() {
92+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
93+
request.addHeader("header1", "value1");
94+
request.addHeader("header2", "value2");
95+
String[] headers = new String[]{"header1", "!header2"};
96+
boolean result = ServletAnnotationMappingUtils.checkHeaders(headers, request);
97+
assertFalse("Invalid request method result", result);
98+
}
99+
100+
@Test
101+
public void checkHeadersKeyValueMatch() {
102+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
103+
request.addHeader("header1", "value1");
104+
String[] headers = new String[]{"header1=value1"};
105+
boolean result = ServletAnnotationMappingUtils.checkHeaders(headers, request);
106+
assertTrue("Invalid request method result", result);
107+
}
108+
109+
@Test
110+
public void checkHeadersKeyValueNoMatch() {
111+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
112+
request.addHeader("header1", "value1");
113+
String[] headers = new String[]{"header1=foo"};
114+
boolean result = ServletAnnotationMappingUtils.checkHeaders(headers, request);
115+
assertFalse("Invalid request method result", result);
116+
}
117+
118+
@Test
119+
public void checkHeadersAcceptMatch() {
120+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
121+
request.addHeader("Accept", "application/pdf, text/html");
122+
String[] headers = new String[]{"accept=text/html, application/*"};
123+
boolean result = ServletAnnotationMappingUtils.checkHeaders(headers, request);
124+
assertTrue("Invalid request method result", result);
125+
}
126+
127+
@Test
128+
public void checkHeadersAcceptNoMatch() {
129+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
130+
request.addHeader("Accept", "application/pdf, text/html");
131+
String[] headers = new String[]{"accept=audio/basic, application/xml"};
132+
boolean result = ServletAnnotationMappingUtils.checkHeaders(headers, request);
133+
assertFalse("Invalid request method result", result);
134+
}
135+
29136
}

0 commit comments

Comments
 (0)