Parcourir la source

SEC-2230: Added Cache Control support

Rob Winch il y a 12 ans
Parent
commit
8013cd54d6

+ 43 - 6
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<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) {

+ 31 - 27
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?

+ 8 - 0
config/src/main/resources/org/springframework/security/config/spring-security-3.2.xsd

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

+ 18 - 1
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 '<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(',')
         }
     }
 }

+ 15 - 0
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 <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

+ 2 - 0
docs/manual/src/docbook/namespace-config.xml

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

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

+ 1 - 1
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<String> headerValues;

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

+ 84 - 0
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);
+    }
+}

+ 24 - 0
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.<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());
+    }
 }