SEC-2230: Added Cache Control support

This commit is contained in:
Rob Winch 2013-07-29 13:20:32 -05:00
parent 7b164bb5e1
commit 8013cd54d6
11 changed files with 323 additions and 39 deletions

View File

@ -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<BeanDefinition> headers = new ManagedList<BeanDefinition>();
BeanDefinitionBuilder pragmaHeader = BeanDefinitionBuilder.genericBeanDefinition(Header.class);
pragmaHeader.addConstructorArgValue("Pragma");
ManagedList<String> pragmaValues = new ManagedList<String>();
pragmaValues.add("no-cache");
pragmaHeader.addConstructorArgValue(pragmaValues);
headers.add(pragmaHeader.getBeanDefinition());
BeanDefinitionBuilder cacheControlHeader = BeanDefinitionBuilder.genericBeanDefinition(Header.class);
cacheControlHeader.addConstructorArgValue("Cache-Control");
ManagedList<String> cacheControlValues = new ManagedList<String>();
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<Element> headerElts = DomUtils.getChildElementsByTagName(element, GENERIC_HEADER_ELEMENT);
for (Element headerElt : headerElts) {

View File

@ -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?

View File

@ -2240,6 +2240,7 @@
</xs:annotation>
<xs:complexType>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element ref="security:cache-control"/>
<xs:element ref="security:xss-protection"/>
<xs:element ref="security:frame-options"/>
<xs:element ref="security:content-type-options"/>
@ -2247,6 +2248,13 @@
</xs:choice>
</xs:complexType>
</xs:element>
<xs:element name="cache-control">
<xs:annotation>
<xs:documentation>Adds Cache-Control no-cache, no-store, must-revalidate and Pragma no-cache every URL
</xs:documentation>
</xs:annotation>
<xs:complexType/>
</xs:element>
<xs:element name="frame-options">
<xs:annotation>
<xs:documentation>Enable basic clickjacking support for newer browsers (IE8+), will set the X-Frame-Options

View File

@ -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 '<xss-protection enabled="false"/> 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<String,String> expected) {
assert response.headerNames == expected.keySet()
expected.each { headerName, value ->
assert response.getHeaderValues(headerName) == [value]
assert response.getHeaderValues(headerName) == value.split(',')
}
}
}

View File

@ -264,6 +264,9 @@
It enables easy configuration for several headers and also allows for setting custom headers through
the <link xlink:href="#nsa-header">header</link> element.
<itemizedlist>
<listitem><literal>Cache-Control</literal> and <literal>Pragma</literal> - Can be set using the
<link xlink:href="#nsa-cache-control">cache-control</link> element. This ensures that the
browser does not cache your secured pages.</listitem>
<listitem><literal>X-Frame-Options</literal> - Can be set using the
<link xlink:href="#nsa-frame-options">frame-options</link> element. The
<link xlink:href="http://en.wikipedia.org/wiki/Clickjacking#X-Frame-Options">X-Frame-Options
@ -288,6 +291,7 @@
<section xml:id="nsa-headers-children">
<title>Child Elements of <literal>&lt;headers&gt;</literal></title>
<itemizedlist>
<listitem><link xlink:href="#nsa-cache-control">cache-control</link></listitem>
<listitem><link xlink:href="#nsa-content-type-options">content-type-options</link></listitem>
<listitem><link xlink:href="#nsa-frame-options">frame-options</link></listitem>
<listitem><link xlink:href="#nsa-header">header</link></listitem>
@ -295,6 +299,17 @@
</itemizedlist>
</section>
</section>
<section xml:id="nsa-cache-control">
<title><literal>&lt;cache-control&gt;</literal></title>
<para>Adds <literal>Cache-Control</literal> and <literal>Pragma</literal> headers to ensure that the
browser does not cache your secured pages.</para>
<section xml:id="nsa-cache-control-parents">
<title>Parent Elements of <literal>&lt;cache-control&gt;</literal></title>
<itemizedlist>
<listitem><link xlink:href="#nsa-headers">headers</link></listitem>
</itemizedlist>
</section>
</section>
<section xml:id="nsa-frame-options">
<title><literal>&lt;frame-options&gt;</literal></title>
<para>When enabled adds the <link xlink:href="http://tools.ietf.org/html/draft-ietf-websec-x-frame-options-01">X-Frame-Options header</link> to the response, this allows newer browsers to do some security

View File

@ -617,6 +617,8 @@ List&lt;OpenIDAttribute> attributes = token.getAttributes();</programlisting>The
<progamlisting language="xml">
<![CDATA[
<headers>
<!-- Add Cache-Control and Pragma headers -->
<cache-control/>
<!-- Adds X-XSS-Protection with value of 1 -->
<xss-protection/>
<!-- Add X-Frame-Options with a value of DENY -->

View File

@ -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 + "]";
}
}

View File

@ -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<String> headerValues;

View File

@ -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<Header> headers;
/**
* Creates a new instance
* @param headers the {@link Header} instances to use
*/
public StaticHeadersWriter(List<Header> 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 + "]";
}
}

View File

@ -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);
}
}

View File

@ -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.<Header>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());
}
}