001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.filter; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.io.PrintWriter; 022 023import javax.servlet.FilterChain; 024import javax.servlet.ServletException; 025import javax.servlet.ServletOutputStream; 026import javax.servlet.ServletRequest; 027import javax.servlet.http.HttpServletRequest; 028import javax.servlet.http.HttpServletResponse; 029 030import org.springframework.http.HttpHeaders; 031import org.springframework.http.HttpMethod; 032import org.springframework.util.Assert; 033import org.springframework.util.DigestUtils; 034import org.springframework.util.StringUtils; 035import org.springframework.web.context.request.ServletWebRequest; 036import org.springframework.web.util.ContentCachingResponseWrapper; 037import org.springframework.web.util.WebUtils; 038 039/** 040 * {@link javax.servlet.Filter} that generates an {@code ETag} value based on the 041 * content on the response. This ETag is compared to the {@code If-None-Match} 042 * header of the request. If these headers are equal, the response content is 043 * not sent, but rather a {@code 304 "Not Modified"} status instead. 044 * 045 * <p>Since the ETag is based on the response content, the response 046 * (e.g. a {@link org.springframework.web.servlet.View}) is still rendered. 047 * As such, this filter only saves bandwidth, not server performance. 048 * 049 * <p><b>NOTE:</b> As of Spring Framework 5.0, this filter uses request/response 050 * decorators built on the Servlet 3.1 API. 051 * 052 * @author Arjen Poutsma 053 * @author Rossen Stoyanchev 054 * @author Brian Clozel 055 * @author Juergen Hoeller 056 * @since 3.0 057 */ 058public class ShallowEtagHeaderFilter extends OncePerRequestFilter { 059 060 private static final String DIRECTIVE_NO_STORE = "no-store"; 061 062 private static final String STREAMING_ATTRIBUTE = ShallowEtagHeaderFilter.class.getName() + ".STREAMING"; 063 064 065 private boolean writeWeakETag = false; 066 067 068 /** 069 * Set whether the ETag value written to the response should be weak, as per RFC 7232. 070 * <p>Should be configured using an {@code <init-param>} for parameter name 071 * "writeWeakETag" in the filter definition in {@code web.xml}. 072 * @since 4.3 073 * @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">RFC 7232 section 2.3</a> 074 */ 075 public void setWriteWeakETag(boolean writeWeakETag) { 076 this.writeWeakETag = writeWeakETag; 077 } 078 079 /** 080 * Return whether the ETag value written to the response should be weak, as per RFC 7232. 081 * @since 4.3 082 */ 083 public boolean isWriteWeakETag() { 084 return this.writeWeakETag; 085 } 086 087 088 /** 089 * The default value is {@code false} so that the filter may delay the generation 090 * of an ETag until the last asynchronously dispatched thread. 091 */ 092 @Override 093 protected boolean shouldNotFilterAsyncDispatch() { 094 return false; 095 } 096 097 @Override 098 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 099 throws ServletException, IOException { 100 101 HttpServletResponse responseToUse = response; 102 if (!isAsyncDispatch(request) && !(response instanceof ConditionalContentCachingResponseWrapper)) { 103 responseToUse = new ConditionalContentCachingResponseWrapper(response, request); 104 } 105 106 filterChain.doFilter(request, responseToUse); 107 108 if (!isAsyncStarted(request) && !isContentCachingDisabled(request)) { 109 updateResponse(request, responseToUse); 110 } 111 } 112 113 private void updateResponse(HttpServletRequest request, HttpServletResponse response) throws IOException { 114 ConditionalContentCachingResponseWrapper wrapper = 115 WebUtils.getNativeResponse(response, ConditionalContentCachingResponseWrapper.class); 116 Assert.notNull(wrapper, "ContentCachingResponseWrapper not found"); 117 HttpServletResponse rawResponse = (HttpServletResponse) wrapper.getResponse(); 118 119 if (isEligibleForEtag(request, wrapper, wrapper.getStatus(), wrapper.getContentInputStream())) { 120 String eTag = wrapper.getHeader(HttpHeaders.ETAG); 121 if (!StringUtils.hasText(eTag)) { 122 eTag = generateETagHeaderValue(wrapper.getContentInputStream(), this.writeWeakETag); 123 rawResponse.setHeader(HttpHeaders.ETAG, eTag); 124 } 125 if (new ServletWebRequest(request, rawResponse).checkNotModified(eTag)) { 126 return; 127 } 128 } 129 130 wrapper.copyBodyToResponse(); 131 } 132 133 /** 134 * Whether an ETag should be calculated for the given request and response 135 * exchange. By default this is {@code true} if all of the following match: 136 * <ul> 137 * <li>Response is not committed.</li> 138 * <li>Response status codes is in the {@code 2xx} series.</li> 139 * <li>Request method is a GET.</li> 140 * <li>Response Cache-Control header does not contain "no-store" (or is not present at all).</li> 141 * </ul> 142 * @param request the HTTP request 143 * @param response the HTTP response 144 * @param responseStatusCode the HTTP response status code 145 * @param inputStream the response body 146 * @return {@code true} if eligible for ETag generation, {@code false} otherwise 147 */ 148 protected boolean isEligibleForEtag(HttpServletRequest request, HttpServletResponse response, 149 int responseStatusCode, InputStream inputStream) { 150 151 if (!response.isCommitted() && 152 responseStatusCode >= 200 && responseStatusCode < 300 && 153 HttpMethod.GET.matches(request.getMethod())) { 154 155 String cacheControl = response.getHeader(HttpHeaders.CACHE_CONTROL); 156 return (cacheControl == null || !cacheControl.contains(DIRECTIVE_NO_STORE)); 157 } 158 159 return false; 160 } 161 162 /** 163 * Generate the ETag header value from the given response body byte array. 164 * <p>The default implementation generates an MD5 hash. 165 * @param inputStream the response body as an InputStream 166 * @param isWeak whether the generated ETag should be weak 167 * @return the ETag header value 168 * @see org.springframework.util.DigestUtils 169 */ 170 protected String generateETagHeaderValue(InputStream inputStream, boolean isWeak) throws IOException { 171 // length of W/ + " + 0 + 32bits md5 hash + " 172 StringBuilder builder = new StringBuilder(37); 173 if (isWeak) { 174 builder.append("W/"); 175 } 176 builder.append("\"0"); 177 DigestUtils.appendMd5DigestAsHex(inputStream, builder); 178 builder.append('"'); 179 return builder.toString(); 180 } 181 182 private boolean compareETagHeaderValue(String requestETag, String responseETag) { 183 if (requestETag.startsWith("W/")) { 184 requestETag = requestETag.substring(2); 185 } 186 if (responseETag.startsWith("W/")) { 187 responseETag = responseETag.substring(2); 188 } 189 return requestETag.equals(responseETag); 190 } 191 192 193 /** 194 * This method can be used to suppress the content caching response wrapper 195 * of the ShallowEtagHeaderFilter. The main reason for this is streaming 196 * scenarios which are not to be cached and do not need an eTag. 197 * <p><strong>Note:</strong> This method must be called before the response 198 * is written to in order for the entire response content to be written 199 * without caching. 200 * @since 4.2 201 */ 202 public static void disableContentCaching(ServletRequest request) { 203 Assert.notNull(request, "ServletRequest must not be null"); 204 request.setAttribute(STREAMING_ATTRIBUTE, true); 205 } 206 207 private static boolean isContentCachingDisabled(HttpServletRequest request) { 208 return (request.getAttribute(STREAMING_ATTRIBUTE) != null); 209 } 210 211 212 /** 213 * Returns the raw OutputStream, instead of the one that does caching, 214 * if {@link #isContentCachingDisabled}. 215 */ 216 private static class ConditionalContentCachingResponseWrapper extends ContentCachingResponseWrapper { 217 218 private final HttpServletRequest request; 219 220 ConditionalContentCachingResponseWrapper(HttpServletResponse response, HttpServletRequest request) { 221 super(response); 222 this.request = request; 223 } 224 225 @Override 226 public ServletOutputStream getOutputStream() throws IOException { 227 return (isContentCachingDisabled(this.request) || hasETag() ? 228 getResponse().getOutputStream() : super.getOutputStream()); 229 } 230 231 @Override 232 public PrintWriter getWriter() throws IOException { 233 return (isContentCachingDisabled(this.request) || hasETag()? 234 getResponse().getWriter() : super.getWriter()); 235 } 236 237 private boolean hasETag() { 238 return StringUtils.hasText(getHeader(HttpHeaders.ETAG)); 239 } 240 } 241 242}