/* * 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.header.writers; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.security.web.header.HeaderWriter; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; /** * Provides support for <a href="http://tools.ietf.org/html/rfc6797">HTTP Strict Transport * Security (HSTS)</a>. * * <p> * By default the expiration is one year and subdomains will be included. This can be * customized using {@link #setMaxAgeInSeconds(long)} and * {@link #setIncludeSubDomains(boolean)} respectively. * </p> * * <p> * Since <a href="http://tools.ietf.org/html/rfc6797#section-7.2">section 7.2</a> states * that HSTS Host MUST NOT include the STS header in HTTP responses, the default behavior * is that the "Strict-Transport-Security" will only be added when * {@link HttpServletRequest#isSecure()} returns {@code true} . At times this may need to * be customized. For example, in some situations where SSL termination is used, something * else may be used to determine if SSL was used. For these circumstances, * {@link #setRequestMatcher(RequestMatcher)} can be invoked with a custom * {@link RequestMatcher}. * </p> * * @author Rob Winch * @since 3.2 */ public final class HstsHeaderWriter implements HeaderWriter { private static final long DEFAULT_MAX_AGE_SECONDS = 31536000; private static final String HSTS_HEADER_NAME = "Strict-Transport-Security"; private final Log logger = LogFactory.getLog(getClass()); private RequestMatcher requestMatcher; private long maxAgeInSeconds; private boolean includeSubDomains; private String hstsHeaderValue; /** * Creates a new instance * * @param requestMatcher maps to {@link #setRequestMatcher(RequestMatcher)} * @param maxAgeInSeconds maps to {@link #setMaxAgeInSeconds(long)} * @param includeSubDomains maps to {@link #setIncludeSubDomains(boolean)} */ public HstsHeaderWriter(RequestMatcher requestMatcher, long maxAgeInSeconds, boolean includeSubDomains) { super(); this.requestMatcher = requestMatcher; this.maxAgeInSeconds = maxAgeInSeconds; this.includeSubDomains = includeSubDomains; updateHstsHeaderValue(); } /** * Creates a new instance * * @param maxAgeInSeconds maps to {@link #setMaxAgeInSeconds(long)} * @param includeSubDomains maps to {@link #setIncludeSubDomains(boolean)} */ public HstsHeaderWriter(long maxAgeInSeconds, boolean includeSubDomains) { this(new SecureRequestMatcher(), maxAgeInSeconds, includeSubDomains); } /** * Creates a new instance * * @param maxAgeInSeconds maps to {@link #setMaxAgeInSeconds(long)} */ public HstsHeaderWriter(long maxAgeInSeconds) { this(new SecureRequestMatcher(), maxAgeInSeconds, true); } /** * Creates a new instance * * @param includeSubDomains maps to {@link #setIncludeSubDomains(boolean)} */ public HstsHeaderWriter(boolean includeSubDomains) { this(new SecureRequestMatcher(), DEFAULT_MAX_AGE_SECONDS, includeSubDomains); } /** * Creates a new instance */ public HstsHeaderWriter() { this(DEFAULT_MAX_AGE_SECONDS); } /* * (non-Javadoc) * * @see org.springframework.security.web.headers.HeaderWriter#writeHeaders(javax * .servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) */ public void writeHeaders(HttpServletRequest request, HttpServletResponse response) { if (this.requestMatcher.matches(request)) { response.setHeader(HSTS_HEADER_NAME, this.hstsHeaderValue); } else if (this.logger.isDebugEnabled()) { this.logger .debug("Not injecting HSTS header since it did not match the requestMatcher " + this.requestMatcher); } } /** * Sets the {@link RequestMatcher} used to determine if the * "Strict-Transport-Security" should be added. If true the header is added, else the * header is not added. By default the header is added when * {@link HttpServletRequest#isSecure()} returns true. * * @param requestMatcher the {@link RequestMatcher} to use. * @throws IllegalArgumentException if {@link RequestMatcher} is null */ public void setRequestMatcher(RequestMatcher requestMatcher) { Assert.notNull(requestMatcher, "requestMatcher cannot be null"); this.requestMatcher = requestMatcher; } /** * <p> * Sets the value (in seconds) for the max-age directive of the * Strict-Transport-Security header. The default is one year. * </p> * * <p> * This instructs browsers how long to remember to keep this domain as a known HSTS * Host. See <a href="http://tools.ietf.org/html/rfc6797#section-6.1.1">Section * 6.1.1</a> for additional details. * </p> * * @param maxAgeInSeconds the maximum amount of time (in seconds) to consider this * domain as a known HSTS Host. * @throws IllegalArgumentException if maxAgeInSeconds is negative */ public void setMaxAgeInSeconds(long maxAgeInSeconds) { if (maxAgeInSeconds < 0) { throw new IllegalArgumentException( "maxAgeInSeconds must be non-negative. Got " + maxAgeInSeconds); } this.maxAgeInSeconds = maxAgeInSeconds; updateHstsHeaderValue(); } /** * <p> * If true, subdomains should be considered HSTS Hosts too. The default is true. * </p> * * <p> * See <a href="http://tools.ietf.org/html/rfc6797#section-6.1.2">Section 6.1.2</a> * for additional details. * </p> * * @param includeSubDomains true to include subdomains, else false */ public void setIncludeSubDomains(boolean includeSubDomains) { this.includeSubDomains = includeSubDomains; updateHstsHeaderValue(); } private void updateHstsHeaderValue() { String headerValue = "max-age=" + this.maxAgeInSeconds; if (this.includeSubDomains) { headerValue += " ; includeSubDomains"; } this.hstsHeaderValue = headerValue; } private static final class SecureRequestMatcher implements RequestMatcher { public boolean matches(HttpServletRequest request) { return request.isSecure(); } } }