001/*
002 * Copyright 2002-2019 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.UnsupportedEncodingException;
021import java.util.Enumeration;
022import java.util.function.Predicate;
023
024import javax.servlet.FilterChain;
025import javax.servlet.ServletException;
026import javax.servlet.http.HttpServletRequest;
027import javax.servlet.http.HttpServletResponse;
028import javax.servlet.http.HttpSession;
029
030import org.springframework.http.HttpHeaders;
031import org.springframework.http.server.ServletServerHttpRequest;
032import org.springframework.lang.Nullable;
033import org.springframework.util.Assert;
034import org.springframework.util.StringUtils;
035import org.springframework.web.util.ContentCachingRequestWrapper;
036import org.springframework.web.util.WebUtils;
037
038/**
039 * Base class for {@code Filter}s that perform logging operations before and after a request
040 * is processed.
041 *
042 * <p>Subclasses should override the {@code beforeRequest(HttpServletRequest, String)} and
043 * {@code afterRequest(HttpServletRequest, String)} methods to perform the actual logging
044 * around the request.
045 *
046 * <p>Subclasses are passed the message to write to the log in the {@code beforeRequest} and
047 * {@code afterRequest} methods. By default, only the URI of the request is logged. However,
048 * setting the {@code includeQueryString} property to {@code true} will cause the query string of
049 * the request to be included also; this can be further extended through {@code includeClientInfo}
050 * and {@code includeHeaders}. The payload (body content) of the request can be logged via the
051 * {@code includePayload} flag: Note that this will only log the part of the payload which has
052 * actually been read, not necessarily the entire body of the request.
053 *
054 * <p>Prefixes and suffixes for the before and after messages can be configured using the
055 * {@code beforeMessagePrefix}, {@code afterMessagePrefix}, {@code beforeMessageSuffix} and
056 * {@code afterMessageSuffix} properties.
057 *
058 * @author Rob Harrop
059 * @author Juergen Hoeller
060 * @author Rossen Stoyanchev
061 * @since 1.2.5
062 * @see #beforeRequest
063 * @see #afterRequest
064 */
065public abstract class AbstractRequestLoggingFilter extends OncePerRequestFilter {
066
067        /**
068         * The default value prepended to the log message written <i>before</i> a request is
069         * processed.
070         */
071        public static final String DEFAULT_BEFORE_MESSAGE_PREFIX = "Before request [";
072
073        /**
074         * The default value appended to the log message written <i>before</i> a request is
075         * processed.
076         */
077        public static final String DEFAULT_BEFORE_MESSAGE_SUFFIX = "]";
078
079        /**
080         * The default value prepended to the log message written <i>after</i> a request is
081         * processed.
082         */
083        public static final String DEFAULT_AFTER_MESSAGE_PREFIX = "After request [";
084
085        /**
086         * The default value appended to the log message written <i>after</i> a request is
087         * processed.
088         */
089        public static final String DEFAULT_AFTER_MESSAGE_SUFFIX = "]";
090
091        private static final int DEFAULT_MAX_PAYLOAD_LENGTH = 50;
092
093
094        private boolean includeQueryString = false;
095
096        private boolean includeClientInfo = false;
097
098        private boolean includeHeaders = false;
099
100        private boolean includePayload = false;
101
102        @Nullable
103        private Predicate<String> headerPredicate;
104
105        private int maxPayloadLength = DEFAULT_MAX_PAYLOAD_LENGTH;
106
107        private String beforeMessagePrefix = DEFAULT_BEFORE_MESSAGE_PREFIX;
108
109        private String beforeMessageSuffix = DEFAULT_BEFORE_MESSAGE_SUFFIX;
110
111        private String afterMessagePrefix = DEFAULT_AFTER_MESSAGE_PREFIX;
112
113        private String afterMessageSuffix = DEFAULT_AFTER_MESSAGE_SUFFIX;
114
115
116        /**
117         * Set whether the query string should be included in the log message.
118         * <p>Should be configured using an {@code <init-param>} for parameter name
119         * "includeQueryString" in the filter definition in {@code web.xml}.
120         */
121        public void setIncludeQueryString(boolean includeQueryString) {
122                this.includeQueryString = includeQueryString;
123        }
124
125        /**
126         * Return whether the query string should be included in the log message.
127         */
128        protected boolean isIncludeQueryString() {
129                return this.includeQueryString;
130        }
131
132        /**
133         * Set whether the client address and session id should be included in the
134         * log message.
135         * <p>Should be configured using an {@code <init-param>} for parameter name
136         * "includeClientInfo" in the filter definition in {@code web.xml}.
137         */
138        public void setIncludeClientInfo(boolean includeClientInfo) {
139                this.includeClientInfo = includeClientInfo;
140        }
141
142        /**
143         * Return whether the client address and session id should be included in the
144         * log message.
145         */
146        protected boolean isIncludeClientInfo() {
147                return this.includeClientInfo;
148        }
149
150        /**
151         * Set whether the request headers should be included in the log message.
152         * <p>Should be configured using an {@code <init-param>} for parameter name
153         * "includeHeaders" in the filter definition in {@code web.xml}.
154         * @since 4.3
155         */
156        public void setIncludeHeaders(boolean includeHeaders) {
157                this.includeHeaders = includeHeaders;
158        }
159
160        /**
161         * Return whether the request headers should be included in the log message.
162         * @since 4.3
163         */
164        protected boolean isIncludeHeaders() {
165                return this.includeHeaders;
166        }
167
168        /**
169         * Set whether the request payload (body) should be included in the log message.
170         * <p>Should be configured using an {@code <init-param>} for parameter name
171         * "includePayload" in the filter definition in {@code web.xml}.
172         * @since 3.0
173         */
174        public void setIncludePayload(boolean includePayload) {
175                this.includePayload = includePayload;
176        }
177
178        /**
179         * Return whether the request payload (body) should be included in the log message.
180         * @since 3.0
181         */
182        protected boolean isIncludePayload() {
183                return this.includePayload;
184        }
185
186        /**
187         * Configure a predicate for selecting which headers should be logged if
188         * {@link #setIncludeHeaders(boolean)} is set to {@code true}.
189         * <p>By default this is not set in which case all headers are logged.
190         * @param headerPredicate the predicate to use
191         * @since 5.2
192         */
193        public void setHeaderPredicate(@Nullable Predicate<String> headerPredicate) {
194                this.headerPredicate = headerPredicate;
195        }
196
197        /**
198         * The configured {@link #setHeaderPredicate(Predicate) headerPredicate}.
199         * @since 5.2
200         */
201        @Nullable
202        protected Predicate<String> getHeaderPredicate() {
203                return this.headerPredicate;
204        }
205
206        /**
207         * Set the maximum length of the payload body to be included in the log message.
208         * Default is 50 characters.
209         * @since 3.0
210         */
211        public void setMaxPayloadLength(int maxPayloadLength) {
212                Assert.isTrue(maxPayloadLength >= 0, "'maxPayloadLength' should be larger than or equal to 0");
213                this.maxPayloadLength = maxPayloadLength;
214        }
215
216        /**
217         * Return the maximum length of the payload body to be included in the log message.
218         * @since 3.0
219         */
220        protected int getMaxPayloadLength() {
221                return this.maxPayloadLength;
222        }
223
224        /**
225         * Set the value that should be prepended to the log message written
226         * <i>before</i> a request is processed.
227         */
228        public void setBeforeMessagePrefix(String beforeMessagePrefix) {
229                this.beforeMessagePrefix = beforeMessagePrefix;
230        }
231
232        /**
233         * Set the value that should be appended to the log message written
234         * <i>before</i> a request is processed.
235         */
236        public void setBeforeMessageSuffix(String beforeMessageSuffix) {
237                this.beforeMessageSuffix = beforeMessageSuffix;
238        }
239
240        /**
241         * Set the value that should be prepended to the log message written
242         * <i>after</i> a request is processed.
243         */
244        public void setAfterMessagePrefix(String afterMessagePrefix) {
245                this.afterMessagePrefix = afterMessagePrefix;
246        }
247
248        /**
249         * Set the value that should be appended to the log message written
250         * <i>after</i> a request is processed.
251         */
252        public void setAfterMessageSuffix(String afterMessageSuffix) {
253                this.afterMessageSuffix = afterMessageSuffix;
254        }
255
256
257        /**
258         * The default value is "false" so that the filter may log a "before" message
259         * at the start of request processing and an "after" message at the end from
260         * when the last asynchronously dispatched thread is exiting.
261         */
262        @Override
263        protected boolean shouldNotFilterAsyncDispatch() {
264                return false;
265        }
266
267        /**
268         * Forwards the request to the next filter in the chain and delegates down to the subclasses
269         * to perform the actual request logging both before and after the request is processed.
270         * @see #beforeRequest
271         * @see #afterRequest
272         */
273        @Override
274        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
275                        throws ServletException, IOException {
276
277                boolean isFirstRequest = !isAsyncDispatch(request);
278                HttpServletRequest requestToUse = request;
279
280                if (isIncludePayload() && isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
281                        requestToUse = new ContentCachingRequestWrapper(request, getMaxPayloadLength());
282                }
283
284                boolean shouldLog = shouldLog(requestToUse);
285                if (shouldLog && isFirstRequest) {
286                        beforeRequest(requestToUse, getBeforeMessage(requestToUse));
287                }
288                try {
289                        filterChain.doFilter(requestToUse, response);
290                }
291                finally {
292                        if (shouldLog && !isAsyncStarted(requestToUse)) {
293                                afterRequest(requestToUse, getAfterMessage(requestToUse));
294                        }
295                }
296        }
297
298        /**
299         * Get the message to write to the log before the request.
300         * @see #createMessage
301         */
302        private String getBeforeMessage(HttpServletRequest request) {
303                return createMessage(request, this.beforeMessagePrefix, this.beforeMessageSuffix);
304        }
305
306        /**
307         * Get the message to write to the log after the request.
308         * @see #createMessage
309         */
310        private String getAfterMessage(HttpServletRequest request) {
311                return createMessage(request, this.afterMessagePrefix, this.afterMessageSuffix);
312        }
313
314        /**
315         * Create a log message for the given request, prefix and suffix.
316         * <p>If {@code includeQueryString} is {@code true}, then the inner part
317         * of the log message will take the form {@code request_uri?query_string};
318         * otherwise the message will simply be of the form {@code request_uri}.
319         * <p>The final message is composed of the inner part as described and
320         * the supplied prefix and suffix.
321         */
322        protected String createMessage(HttpServletRequest request, String prefix, String suffix) {
323                StringBuilder msg = new StringBuilder();
324                msg.append(prefix);
325                msg.append(request.getMethod()).append(" ");
326                msg.append(request.getRequestURI());
327
328                if (isIncludeQueryString()) {
329                        String queryString = request.getQueryString();
330                        if (queryString != null) {
331                                msg.append('?').append(queryString);
332                        }
333                }
334
335                if (isIncludeClientInfo()) {
336                        String client = request.getRemoteAddr();
337                        if (StringUtils.hasLength(client)) {
338                                msg.append(", client=").append(client);
339                        }
340                        HttpSession session = request.getSession(false);
341                        if (session != null) {
342                                msg.append(", session=").append(session.getId());
343                        }
344                        String user = request.getRemoteUser();
345                        if (user != null) {
346                                msg.append(", user=").append(user);
347                        }
348                }
349
350                if (isIncludeHeaders()) {
351                        HttpHeaders headers = new ServletServerHttpRequest(request).getHeaders();
352                        if (getHeaderPredicate() != null) {
353                                Enumeration<String> names = request.getHeaderNames();
354                                while (names.hasMoreElements()) {
355                                        String header = names.nextElement();
356                                        if (!getHeaderPredicate().test(header)) {
357                                                headers.set(header, "masked");
358                                        }
359                                }
360                        }
361                        msg.append(", headers=").append(headers);
362                }
363
364                if (isIncludePayload()) {
365                        String payload = getMessagePayload(request);
366                        if (payload != null) {
367                                msg.append(", payload=").append(payload);
368                        }
369                }
370
371                msg.append(suffix);
372                return msg.toString();
373        }
374
375        /**
376         * Extracts the message payload portion of the message created by
377         * {@link #createMessage(HttpServletRequest, String, String)} when
378         * {@link #isIncludePayload()} returns true.
379         * @since 5.0.3
380         */
381        @Nullable
382        protected String getMessagePayload(HttpServletRequest request) {
383                ContentCachingRequestWrapper wrapper =
384                                WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
385                if (wrapper != null) {
386                        byte[] buf = wrapper.getContentAsByteArray();
387                        if (buf.length > 0) {
388                                int length = Math.min(buf.length, getMaxPayloadLength());
389                                try {
390                                        return new String(buf, 0, length, wrapper.getCharacterEncoding());
391                                }
392                                catch (UnsupportedEncodingException ex) {
393                                        return "[unknown]";
394                                }
395                        }
396                }
397                return null;
398        }
399
400
401        /**
402         * Determine whether to call the {@link #beforeRequest}/{@link #afterRequest}
403         * methods for the current request, i.e. whether logging is currently active
404         * (and the log message is worth building).
405         * <p>The default implementation always returns {@code true}. Subclasses may
406         * override this with a log level check.
407         * @param request current HTTP request
408         * @return {@code true} if the before/after method should get called;
409         * {@code false} otherwise
410         * @since 4.1.5
411         */
412        protected boolean shouldLog(HttpServletRequest request) {
413                return true;
414        }
415
416        /**
417         * Concrete subclasses should implement this method to write a log message
418         * <i>before</i> the request is processed.
419         * @param request current HTTP request
420         * @param message the message to log
421         */
422        protected abstract void beforeRequest(HttpServletRequest request, String message);
423
424        /**
425         * Concrete subclasses should implement this method to write a log message
426         * <i>after</i> the request is processed.
427         * @param request current HTTP request
428         * @param message the message to log
429         */
430        protected abstract void afterRequest(HttpServletRequest request, String message);
431
432}