diff --git a/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java index dba6fc71cc..881d9b4fee 100644 --- a/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java @@ -15,24 +15,29 @@ */ package org.springframework.security.config.http; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.regex.PatternSyntaxException; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.ManagedList; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.security.web.headers.Header; import org.springframework.security.web.headers.HeadersFilter; import org.springframework.security.web.headers.StaticHeadersWriter; -import org.springframework.security.web.headers.frameoptions.*; +import org.springframework.security.web.headers.frameoptions.AbstractRequestParameterAllowFromStrategy; +import org.springframework.security.web.headers.frameoptions.RegExpAllowFromStrategy; +import org.springframework.security.web.headers.frameoptions.StaticAllowFromStrategy; +import org.springframework.security.web.headers.frameoptions.WhiteListedAllowFromStrategy; +import org.springframework.security.web.headers.frameoptions.XFrameOptionsHeaderWriter; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.List; -import java.util.regex.PatternSyntaxException; - /** * Parser for the {@code HeadersFilter}. * @@ -52,6 +57,8 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser { private static final String ATT_VALUE = "value"; private static final String ATT_REF = "ref"; + private static final String CACHE_CONTROL_ELEMENT = "cache-control"; + private static final String XSS_ELEMENT = "xss-protection"; private static final String CONTENT_TYPE_ELEMENT = "content-type-options"; private static final String FRAME_OPTIONS_ELEMENT = "frame-options"; @@ -68,6 +75,7 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser { headerWriters = new ManagedList(); BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(HeadersFilter.class); + parseCacheControlElement(element); parseXssElement(element, parserContext); parseFrameOptionsElement(element, parserContext); parseContentTypeOptionsElement(element); @@ -82,6 +90,35 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser { return builder.getBeanDefinition(); } + private void parseCacheControlElement(Element element) { + Element cacheControlElement = DomUtils.getChildElementByTagName(element, CACHE_CONTROL_ELEMENT); + if (cacheControlElement != null) { + ManagedList headers = new ManagedList(); + + BeanDefinitionBuilder pragmaHeader = BeanDefinitionBuilder.genericBeanDefinition(Header.class); + pragmaHeader.addConstructorArgValue("Pragma"); + ManagedList pragmaValues = new ManagedList(); + pragmaValues.add("no-cache"); + pragmaHeader.addConstructorArgValue(pragmaValues); + headers.add(pragmaHeader.getBeanDefinition()); + + BeanDefinitionBuilder cacheControlHeader = BeanDefinitionBuilder.genericBeanDefinition(Header.class); + cacheControlHeader.addConstructorArgValue("Cache-Control"); + ManagedList cacheControlValues = new ManagedList(); + cacheControlValues.add("no-cache"); + cacheControlValues.add("no-store"); + cacheControlValues.add("max-age=0"); + cacheControlValues.add("must-revalidate"); + cacheControlHeader.addConstructorArgValue(cacheControlValues); + headers.add(cacheControlHeader.getBeanDefinition()); + + BeanDefinitionBuilder headersWriter = BeanDefinitionBuilder.genericBeanDefinition(StaticHeadersWriter.class); + headersWriter.addConstructorArgValue(headers); + + headerWriters.add(headersWriter.getBeanDefinition()); + } + } + private void parseHeaderElements(Element element) { List headerElts = DomUtils.getChildElementsByTagName(element, GENERIC_HEADER_ELEMENT); for (Element headerElt : headerElts) { diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-3.2.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-3.2.rnc index 6a6276077e..80d56371a7 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-3.2.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-3.2.rnc @@ -720,51 +720,55 @@ jdbc-user-service.attlist &= headers = ## Element for configuration of the AddHeadersFilter. Enables easy setting for the X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. - element headers {xss-protection? & frame-options? & content-type-options? & header*} + element headers {cache-control? & xss-protection? & frame-options? & content-type-options? & header*} + +cache-control = + ## Adds Cache-Control no-cache, no-store, must-revalidate and Pragma no-cache every URL + element cache-control {empty} frame-options = - ## Enable basic clickjacking support for newer browsers (IE8+), will set the X-Frame-Options header. - element frame-options {frame-options.attlist,empty} + ## Enable basic clickjacking support for newer browsers (IE8+), will set the X-Frame-Options header. + element frame-options {frame-options.attlist,empty} frame-options.attlist &= - ## Specify the policy to use for the X-Frame-Options-Header. - attribute policy {"DENY","SAMEORIGIN","ALLOW-FROM"}? + ## Specify the policy to use for the X-Frame-Options-Header. + attribute policy {"DENY","SAMEORIGIN","ALLOW-FROM"}? frame-options.attlist &= - ## Specify the strategy to use when ALLOW-FROM is chosen. - attribute strategy {"static","whitelist","regexp"}? + ## Specify the strategy to use when ALLOW-FROM is chosen. + attribute strategy {"static","whitelist","regexp"}? frame-options.attlist &= - ## Specify the a reference to the custom AllowFromStrategy to use when ALLOW-FROM is chosen. - ref? + ## Specify the a reference to the custom AllowFromStrategy to use when ALLOW-FROM is chosen. + ref? frame-options.attlist &= - ## Specify the a value to use for the chosen strategy. - attribute value {xsd:string}? + ## Specify the a value to use for the chosen strategy. + attribute value {xsd:string}? frame-options.attlist &= - ## Specify the request parameter to use for the origin when using a 'whitelist' or 'regexp' based strategy. Default is 'from'. - attribute from-parameter {xsd:string}? + ## Specify the request parameter to use for the origin when using a 'whitelist' or 'regexp' based strategy. Default is 'from'. + attribute from-parameter {xsd:string}? xss-protection = - ## Enable basic XSS browser protection, supported by newer browsers (IE8+), will set the X-XSS-Protection header. - element xss-protection {xss-protection.attlist,empty} + ## Enable basic XSS browser protection, supported by newer browsers (IE8+), will set the X-XSS-Protection header. + element xss-protection {xss-protection.attlist,empty} xss-protection.attlist &= - ## enable or disable the X-XSS-Protection header. Default is 'true' meaning it is enabled. - attribute enabled {xsd:boolean}? + ## enable or disable the X-XSS-Protection header. Default is 'true' meaning it is enabled. + attribute enabled {xsd:boolean}? xss-protection.attlist &= - ## Add mode=block to the header or not, default is on. - attribute block {xsd:boolean}? + ## Add mode=block to the header or not, default is on. + attribute block {xsd:boolean}? content-type-options = - ## Add a X-Content-Type-Options header to the resopnse. Value is always 'nosniff'. - element content-type-options {empty} + ## Add a X-Content-Type-Options header to the resopnse. Value is always 'nosniff'. + element content-type-options {empty} header= - ## Add additional headers to the response. - element header {header.attlist} + ## Add additional headers to the response. + element header {header.attlist} header.attlist &= - ## The name of the header to add. - attribute name {xsd:token}? + ## The name of the header to add. + attribute name {xsd:token}? header.attlist &= - ## The value for the header. - attribute value {xsd:token}? + ## The value for the header. + attribute value {xsd:token}? header.attlist &= ## Reference to a custom HeaderFactory implementation. ref? diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-3.2.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-3.2.xsd index f9b6007a6d..ba73ae02d0 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-3.2.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-3.2.xsd @@ -2240,6 +2240,7 @@ + @@ -2247,6 +2248,13 @@ + + + Adds Cache-Control no-cache, no-store, must-revalidate and Pragma no-cache every URL + + + + Enable basic clickjacking support for newer browsers (IE8+), will set the X-Frame-Options diff --git a/config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy index 90eecc156a..aeeca2d855 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy @@ -28,6 +28,7 @@ import org.springframework.security.openid.OpenIDAuthenticationFilter import org.springframework.security.openid.OpenIDAuthenticationToken import org.springframework.security.openid.OpenIDConsumer import org.springframework.security.openid.OpenIDConsumerException +import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.access.ExceptionTranslationFilter import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter @@ -324,10 +325,26 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests { e.message.contains ' does not allow block="true".' } + def 'http headers cache-control'() { + setup: + httpAutoConfig { + 'headers'() { + 'cache-control'() + } + } + createAppContext() + def springSecurityFilterChain = appContext.getBean(FilterChainProxy) + MockHttpServletResponse response = new MockHttpServletResponse() + when: + springSecurityFilterChain.doFilter(new MockHttpServletRequest(), response, new MockFilterChain()) + then: + assertHeaders(response, ['Cache-Control': 'no-cache,no-store,max-age=0,must-revalidate','Pragma':'no-cache']) + } + def assertHeaders(MockHttpServletResponse response, Map expected) { assert response.headerNames == expected.keySet() expected.each { headerName, value -> - assert response.getHeaderValues(headerName) == [value] + assert response.getHeaderValues(headerName) == value.split(',') } } } diff --git a/docs/manual/src/docbook/appendix-namespace.xml b/docs/manual/src/docbook/appendix-namespace.xml index 781a2ba978..f6ec43716a 100644 --- a/docs/manual/src/docbook/appendix-namespace.xml +++ b/docs/manual/src/docbook/appendix-namespace.xml @@ -264,6 +264,9 @@ It enables easy configuration for several headers and also allows for setting custom headers through the header element. + Cache-Control and Pragma - Can be set using the + cache-control element. This ensures that the + browser does not cache your secured pages. X-Frame-Options - Can be set using the frame-options element. The X-Frame-Options @@ -288,6 +291,7 @@
Child Elements of <literal><headers></literal> + cache-control content-type-options frame-options header @@ -295,6 +299,17 @@
+
+ <literal><cache-control></literal> + Adds Cache-Control and Pragma headers to ensure that the + browser does not cache your secured pages. +
+ Parent Elements of <literal><cache-control></literal> + + headers + +
+
<literal><frame-options></literal> When enabled adds the X-Frame-Options header to the response, this allows newer browsers to do some security diff --git a/docs/manual/src/docbook/namespace-config.xml b/docs/manual/src/docbook/namespace-config.xml index 323e0893cd..8153b1fc54 100644 --- a/docs/manual/src/docbook/namespace-config.xml +++ b/docs/manual/src/docbook/namespace-config.xml @@ -617,6 +617,8 @@ List<OpenIDAttribute> attributes = token.getAttributes();The + + diff --git a/web/src/main/java/org/springframework/security/web/headers/DelegatingRequestMatcherHeaderWriter.java b/web/src/main/java/org/springframework/security/web/headers/DelegatingRequestMatcherHeaderWriter.java new file mode 100644 index 0000000000..b96383534b --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/headers/DelegatingRequestMatcherHeaderWriter.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2013 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.web.headers; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.web.util.RequestMatcher; +import org.springframework.util.Assert; + +/** + * Delegates to the provided {@link HeaderWriter} when + * {@link RequestMatcher#matches(HttpServletRequest)} returns true. + * + * @author Rob Winch + * @since 3.2 + */ +public class DelegatingRequestMatcherHeaderWriter implements HeaderWriter { + private final RequestMatcher requestMatcher; + + private final HeaderWriter delegateHeaderWriter; + + /** + * Creates a new instance + * + * @param requestMatcher + * the {@link RequestMatcher} to use. If returns true, the + * delegateHeaderWriter will be invoked. + * @param delegateHeaderWriter + * the {@link HeaderWriter} to invoke if the + * {@link RequestMatcher} returns true. + */ + public DelegatingRequestMatcherHeaderWriter(RequestMatcher requestMatcher, + HeaderWriter delegateHeaderWriter) { + Assert.notNull(requestMatcher, "requestMatcher cannot be null"); + Assert.notNull(delegateHeaderWriter, "delegateHeaderWriter cannot be null"); + this.requestMatcher = requestMatcher; + this.delegateHeaderWriter = delegateHeaderWriter; + } + + /* (non-Javadoc) + * @see org.springframework.security.web.headers.HeaderWriter#writeHeaders(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + */ + @Override + public void writeHeaders(HttpServletRequest request, + HttpServletResponse response) { + if(requestMatcher.matches(request)) { + delegateHeaderWriter.writeHeaders(request, response); + } + } + + @Override + public String toString() { + return getClass().getName()+ " [requestMatcher=" + + requestMatcher + ", delegateHeaderWriter=" + + delegateHeaderWriter + "]"; + } +} diff --git a/web/src/main/java/org/springframework/security/web/headers/Header.java b/web/src/main/java/org/springframework/security/web/headers/Header.java index 503fd01c61..d007643446 100644 --- a/web/src/main/java/org/springframework/security/web/headers/Header.java +++ b/web/src/main/java/org/springframework/security/web/headers/Header.java @@ -10,7 +10,7 @@ import org.springframework.util.Assert; /** * Represents a Header to be added to the {@link HttpServletResponse} */ -final class Header { +public final class Header { private final String headerName; private final List headerValues; diff --git a/web/src/main/java/org/springframework/security/web/headers/StaticHeadersWriter.java b/web/src/main/java/org/springframework/security/web/headers/StaticHeadersWriter.java index 51bb3711aa..4201e44cca 100644 --- a/web/src/main/java/org/springframework/security/web/headers/StaticHeadersWriter.java +++ b/web/src/main/java/org/springframework/security/web/headers/StaticHeadersWriter.java @@ -1,30 +1,52 @@ package org.springframework.security.web.headers; +import java.util.Collections; +import java.util.List; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.springframework.util.Assert; + /** * {@code HeaderWriter} implementation which writes the same {@code Header} instance. * * @author Marten Deinum + * @author Rob Winch * @since 3.2 */ public class StaticHeadersWriter implements HeaderWriter { - private final Header header; + private final List
headers; /** * Creates a new instance + * @param headers the {@link Header} instances to use + */ + public StaticHeadersWriter(List
headers) { + Assert.notEmpty(headers,"headers cannot be null or empty"); + this.headers = headers; + } + + /** + * Creates a new instance with a single header * @param headerName the name of the header * @param headerValues the values for the header */ public StaticHeadersWriter(String headerName, String... headerValues) { - header = new Header(headerName, headerValues); + this(Collections.singletonList(new Header(headerName, headerValues))); } public void writeHeaders(HttpServletRequest request, HttpServletResponse response) { - for(String value : header.getValues()) { - response.addHeader(header.getName(), value); + for(Header header : headers) { + for(String value : header.getValues()) { + response.addHeader(header.getName(), value); + } } } + + @Override + public String toString() { + return getClass().getName() + " [headers=" + headers + "]"; + } } \ No newline at end of file diff --git a/web/src/test/java/org/springframework/security/web/headers/DelegatingRequestMatcherHeaderWriterTests.java b/web/src/test/java/org/springframework/security/web/headers/DelegatingRequestMatcherHeaderWriterTests.java new file mode 100644 index 0000000000..77f4a6e93a --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/headers/DelegatingRequestMatcherHeaderWriterTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2013 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.web.headers; + +import static org.junit.Assert.fail; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.web.util.RequestMatcher; + +/** + * @author Rob Winch + * + */ +@RunWith(MockitoJUnitRunner.class) +public class DelegatingRequestMatcherHeaderWriterTests { + @Mock + private RequestMatcher matcher; + + @Mock + private HeaderWriter delegate; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + private DelegatingRequestMatcherHeaderWriter headerWriter; + + @Before + public void setup() { + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + headerWriter = new DelegatingRequestMatcherHeaderWriter(matcher, delegate); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorNullRequestMatcher() { + new DelegatingRequestMatcherHeaderWriter(null, delegate); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorNullDelegate() { + new DelegatingRequestMatcherHeaderWriter(matcher, null); + } + + @Test + public void writeHeadersOnMatch() { + when(matcher.matches(request)).thenReturn(true); + + headerWriter.writeHeaders(request, response); + + verify(delegate).writeHeaders(request, response); + } + + @Test + public void writeHeadersOnNoMatch() { + when(matcher.matches(request)).thenReturn(false); + + headerWriter.writeHeaders(request, response); + + verify(delegate, times(0)).writeHeaders(request, response); + } +} diff --git a/web/src/test/java/org/springframework/security/web/headers/StaticHeaderWriterTests.java b/web/src/test/java/org/springframework/security/web/headers/StaticHeaderWriterTests.java index eaa5b5b25f..67a7a105b3 100644 --- a/web/src/test/java/org/springframework/security/web/headers/StaticHeaderWriterTests.java +++ b/web/src/test/java/org/springframework/security/web/headers/StaticHeaderWriterTests.java @@ -18,6 +18,7 @@ package org.springframework.security.web.headers; import static org.fest.assertions.Assertions.assertThat; import java.util.Arrays; +import java.util.Collections; import org.junit.Before; import org.junit.Test; @@ -41,6 +42,16 @@ public class StaticHeaderWriterTests { response = new MockHttpServletResponse(); } + @Test(expected = IllegalArgumentException.class) + public void constructorNullHeaders() { + new StaticHeadersWriter(null); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorEmptyHeaders() { + new StaticHeadersWriter(Collections.
emptyList()); + } + @Test(expected = IllegalArgumentException.class) public void constructorNullHeaderName() { new StaticHeadersWriter(null, "value1"); @@ -65,4 +76,17 @@ public class StaticHeaderWriterTests { factory.writeHeaders(request, response); assertThat(response.getHeaderValues(headerName)).isEqualTo(Arrays.asList(headerValue)); } + + @Test + public void writeHeadersMulti() { + Header pragma = new Header("Pragma","no-cache"); + Header cacheControl= new Header("Cache-Control","no-cache","no-store","must-revalidate"); + StaticHeadersWriter factory = new StaticHeadersWriter(Arrays.asList(pragma, cacheControl)); + + factory.writeHeaders(request, response); + + assertThat(response.getHeaderNames().size()).isEqualTo(2); + assertThat(response.getHeaderValues(pragma.getName())).isEqualTo(pragma.getValues()); + assertThat(response.getHeaderValues(cacheControl.getName())).isEqualTo(cacheControl.getValues()); + } }