001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    package org.apache.shiro.web.servlet;
020    
021    import org.slf4j.Logger;
022    import org.slf4j.LoggerFactory;
023    
024    import javax.servlet.FilterChain;
025    import javax.servlet.ServletException;
026    import javax.servlet.ServletRequest;
027    import javax.servlet.ServletResponse;
028    import java.io.IOException;
029    
030    /**
031     * Filter base class that guarantees to be just executed once per request,
032     * on any servlet container. It provides a {@link #doFilterInternal}
033     * method with HttpServletRequest and HttpServletResponse arguments.
034     * <p/>
035     * The {@link #getAlreadyFilteredAttributeName} method determines how
036     * to identify that a request is already filtered. The default implementation
037     * is based on the configured name of the concrete filter instance.
038     * <h3>Controlling filter execution</h3>
039     * 1.2 introduced the {@link #isEnabled(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} method and
040     * {@link #isEnabled()} property to allow explicit controll over whether the filter executes (or allows passthrough)
041     * for any given request.
042     * <p/>
043     * <b>NOTE</b> This class was initially borrowed from the Spring framework but has continued modifications.
044     *
045     * @since 0.1
046     */
047    public abstract class OncePerRequestFilter extends NameableFilter {
048    
049        /**
050         * Private internal log instance.
051         */
052        private static final Logger log = LoggerFactory.getLogger(OncePerRequestFilter.class);
053    
054        /**
055         * Suffix that gets appended to the filter name for the "already filtered" request attribute.
056         *
057         * @see #getAlreadyFilteredAttributeName
058         */
059        public static final String ALREADY_FILTERED_SUFFIX = ".FILTERED";
060    
061        /**
062         * Determines generally if this filter should execute or let requests fall through to the next chain element.
063         *
064         * @see #isEnabled()
065         */
066        private boolean enabled = true; //most filters wish to execute when configured, so default to true
067    
068        /**
069         * Returns {@code true} if this filter should <em>generally</em><b>*</b> execute for any request,
070         * {@code false} if it should let the request/response pass through immediately to the next
071         * element in the {@link FilterChain}.  The default value is {@code true}, as most filters would inherently need
072         * to execute when configured.
073         * <p/>
074         * <b>*</b> This configuration property is for general configuration for any request that comes through
075         * the filter.  The
076         * {@link #isEnabled(javax.servlet.ServletRequest, javax.servlet.ServletResponse) isEnabled(request,response)}
077         * method actually determines whether or not if the filter is enabled based on the current request.
078         *
079         * @return {@code true} if this filter should <em>generally</em> execute, {@code false} if it should let the
080         * request/response pass through immediately to the next element in the {@link FilterChain}.
081         * @since 1.2
082         */
083        public boolean isEnabled() {
084            return enabled;
085        }
086    
087        /**
088         * Sets whether or not this filter <em>generally</em> executes for any request.  See the
089         * {@link #isEnabled() isEnabled()} JavaDoc as to what <em>general</em> execution means.
090         *
091         * @param enabled whether or not this filter <em>generally</em> executes.
092         * @since 1.2
093         */
094        public void setEnabled(boolean enabled) {
095            this.enabled = enabled;
096        }
097    
098        /**
099         * This {@code doFilter} implementation stores a request attribute for
100         * "already filtered", proceeding without filtering again if the
101         * attribute is already there.
102         *
103         * @see #getAlreadyFilteredAttributeName
104         * @see #shouldNotFilter
105         * @see #doFilterInternal
106         */
107        public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
108                throws ServletException, IOException {
109            String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
110            if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
111                log.trace("Filter '{}' already executed.  Proceeding without invoking this filter.", getName());
112                filterChain.doFilter(request, response);
113            } else //noinspection deprecation
114                if (/* added in 1.2: */ !isEnabled(request, response) ||
115                    /* retain backwards compatibility: */ shouldNotFilter(request) ) {
116                log.debug("Filter '{}' is not enabled for the current request.  Proceeding without invoking this filter.",
117                        getName());
118                filterChain.doFilter(request, response);
119            } else {
120                // Do invoke this filter...
121                log.trace("Filter '{}' not yet executed.  Executing now.", getName());
122                request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
123    
124                try {
125                    doFilterInternal(request, response, filterChain);
126                } finally {
127                    // Once the request has finished, we're done and we don't
128                    // need to mark as 'already filtered' any more.
129                    request.removeAttribute(alreadyFilteredAttributeName);
130                }
131            }
132        }
133    
134        /**
135         * Returns {@code true} if this filter should filter the specified request, {@code false} if it should let the
136         * request/response pass through immediately to the next element in the {@code FilterChain}.
137         * <p/>
138         * This default implementation merely returns the value of {@link #isEnabled() isEnabled()}, which is
139         * {@code true} by default (to ensure the filter always executes by default), but it can be overridden by
140         * subclasses for request-specific behavior if necessary.  For example, a filter could be enabled or disabled
141         * based on the request path being accessed.
142         * <p/>
143         * <b>Helpful Hint:</b> if your subclass extends {@link org.apache.shiro.web.filter.PathMatchingFilter PathMatchingFilter},
144         * you may wish to instead override the
145         * {@link org.apache.shiro.web.filter.PathMatchingFilter#isEnabled(javax.servlet.ServletRequest, javax.servlet.ServletResponse, String, Object)
146         * PathMatchingFilter.isEnabled(request,response,path,pathSpecificConfig)}
147         * method if you want to make your enable/disable decision based on any path-specific configuration.
148         *
149         * @param request the incoming servlet request
150         * @param response the outbound servlet response
151         * @return {@code true} if this filter should filter the specified request, {@code false} if it should let the
152         * request/response pass through immediately to the next element in the {@code FilterChain}.
153         * @throws IOException in the case of any IO error
154         * @throws ServletException in the case of any error
155         * @see org.apache.shiro.web.filter.PathMatchingFilter#isEnabled(javax.servlet.ServletRequest, javax.servlet.ServletResponse, String, Object)
156         * @since 1.2
157         */
158        @SuppressWarnings({"UnusedParameters"})
159        protected boolean isEnabled(ServletRequest request, ServletResponse response) throws ServletException, IOException {
160            return isEnabled();
161        }
162    
163        /**
164         * Return name of the request attribute that identifies that a request has already been filtered.
165         * <p/>
166         * The default implementation takes the configured {@link #getName() name} and appends &quot;{@code .FILTERED}&quot;.
167         * If the filter is not fully initialized, it falls back to the implementation's class name.
168         *
169         * @return the name of the request attribute that identifies that a request has already been filtered.
170         * @see #getName
171         * @see #ALREADY_FILTERED_SUFFIX
172         */
173        protected String getAlreadyFilteredAttributeName() {
174            String name = getName();
175            if (name == null) {
176                name = getClass().getName();
177            }
178            return name + ALREADY_FILTERED_SUFFIX;
179        }
180    
181        /**
182         * Can be overridden in subclasses for custom filtering control,
183         * returning <code>true</code> to avoid filtering of the given request.
184         * <p>The default implementation always returns <code>false</code>.
185         *
186         * @param request current HTTP request
187         * @return whether the given request should <i>not</i> be filtered
188         * @throws ServletException in case of errors
189         * @deprecated in favor of overriding {@link #isEnabled(javax.servlet.ServletRequest, javax.servlet.ServletResponse)}
190         * for custom behavior.  This method will be removed in Shiro 2.0.
191         */
192        @Deprecated
193        @SuppressWarnings({"UnusedDeclaration"})
194        protected boolean shouldNotFilter(ServletRequest request) throws ServletException {
195            return false;
196        }
197    
198    
199        /**
200         * Same contract as for
201         * {@link #doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)},
202         * but guaranteed to be invoked only once per request.
203         *
204         * @param request  incoming {@code ServletRequest}
205         * @param response outgoing {@code ServletResponse}
206         * @param chain    the {@code FilterChain} to execute
207         * @throws ServletException if there is a problem processing the request
208         * @throws IOException      if there is an I/O problem processing the request
209         */
210        protected abstract void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
211                throws ServletException, IOException;
212    }