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 */
019package org.apache.shiro.spring.web;
020
021import org.apache.shiro.config.Ini;
022import org.apache.shiro.mgt.SecurityManager;
023import org.apache.shiro.util.CollectionUtils;
024import org.apache.shiro.lang.util.Nameable;
025import org.apache.shiro.lang.util.StringUtils;
026import org.apache.shiro.web.config.IniFilterChainResolverFactory;
027import org.apache.shiro.web.config.ShiroFilterConfiguration;
028import org.apache.shiro.web.filter.AccessControlFilter;
029import org.apache.shiro.web.filter.InvalidRequestFilter;
030import org.apache.shiro.web.filter.authc.AuthenticationFilter;
031import org.apache.shiro.web.filter.authz.AuthorizationFilter;
032import org.apache.shiro.web.filter.mgt.DefaultFilter;
033import org.apache.shiro.web.filter.mgt.DefaultFilterChainManager;
034import org.apache.shiro.web.filter.mgt.FilterChainManager;
035import org.apache.shiro.web.filter.mgt.FilterChainResolver;
036import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;
037import org.apache.shiro.web.mgt.WebSecurityManager;
038import org.apache.shiro.web.servlet.AbstractShiroFilter;
039import org.apache.shiro.web.servlet.OncePerRequestFilter;
040import org.slf4j.Logger;
041import org.slf4j.LoggerFactory;
042import org.springframework.beans.BeansException;
043import org.springframework.beans.factory.BeanInitializationException;
044import org.springframework.beans.factory.FactoryBean;
045import org.springframework.beans.factory.config.BeanPostProcessor;
046
047import javax.servlet.Filter;
048import java.util.ArrayList;
049import java.util.LinkedHashMap;
050import java.util.List;
051import java.util.Map;
052
053/**
054 * {@link org.springframework.beans.factory.FactoryBean FactoryBean} to be used in Spring-based web applications for
055 * defining the master Shiro Filter.
056 * <h4>Usage</h4>
057 * Declare a DelegatingFilterProxy in {@code web.xml}, matching the filter name to the bean id:
058 * <pre>
059 * &lt;filter&gt;
060 *   &lt;filter-name&gt;<b>shiroFilter</b>&lt;/filter-name&gt;
061 *   &lt;filter-class&gt;org.springframework.web.filter.DelegatingFilterProxy&lt;filter-class&gt;
062 *   &lt;init-param&gt;
063 *    &lt;param-name&gt;targetFilterLifecycle&lt;/param-name&gt;
064 *     &lt;param-value&gt;true&lt;/param-value&gt;
065 *   &lt;/init-param&gt;
066 * &lt;/filter&gt;
067 * </pre>
068 * Then, in your spring XML file that defines your web ApplicationContext:
069 * <pre>
070 * &lt;bean id="<b>shiroFilter</b>" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"&gt;
071 *    &lt;property name="securityManager" ref="securityManager"/&gt;
072 *    &lt;!-- other properties as necessary ... --&gt;
073 * &lt;/bean&gt;
074 * </pre>
075 * <h4>Filter Auto-Discovery</h4>
076 * While there is a {@link #setFilters(java.util.Map) filters} property that allows you to assign a filter beans
077 * to the 'pool' of filters available when defining {@link #setFilterChainDefinitions(String) filter chains}, it is
078 * optional.
079 * <p/>
080 * This implementation is also a {@link BeanPostProcessor} and will acquire
081 * any {@link javax.servlet.Filter Filter} beans defined independently in your Spring application context.  Upon
082 * discovery, they will be automatically added to the {@link #setFilters(java.util.Map) map} keyed by the bean ID.
083 * That ID can then be used in the filter chain definitions, for example:
084 *
085 * <pre>
086 * &lt;bean id="<b>myCustomFilter</b>" class="com.class.that.implements.javax.servlet.Filter"/&gt;
087 * ...
088 * &lt;bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"&gt;
089 *    ...
090 *    &lt;property name="filterChainDefinitions"&gt;
091 *        &lt;value&gt;
092 *            /some/path/** = authc, <b>myCustomFilter</b>
093 *        &lt;/value&gt;
094 *    &lt;/property&gt;
095 * &lt;/bean&gt;
096 * </pre>
097 * <h4>Global Property Values</h4>
098 * Most Shiro servlet Filter implementations exist for defining custom Filter
099 * {@link #setFilterChainDefinitions(String) chain definitions}.  Most implementations subclass one of the
100 * {@link AccessControlFilter}, {@link AuthenticationFilter}, {@link AuthorizationFilter} classes to simplify things,
101 * and each of these 3 classes has configurable properties that are application-specific.
102 * <p/>
103 * A dilemma arises where, if you want to for example set the application's 'loginUrl' for any Filter, you don't want
104 * to have to manually specify that value for <em>each</em> filter instance defined.
105 * <p/>
106 * To prevent configuration duplication, this implementation provides the following properties to allow you
107 * to set relevant values in only one place:
108 * <ul>
109 * <li>{@link #setLoginUrl(String)}</li>
110 * <li>{@link #setSuccessUrl(String)}</li>
111 * <li>{@link #setUnauthorizedUrl(String)}</li>
112 * </ul>
113 * <p>
114 * Then at startup, any values specified via these 3 properties will be applied to all configured
115 * Filter instances so you don't have to specify them individually on each filter instance.  To ensure your own custom
116 * filters benefit from this convenience, your filter implementation should subclass one of the 3 mentioned
117 * earlier.
118 *
119 * @see org.springframework.web.filter.DelegatingFilterProxy DelegatingFilterProxy
120 * @since 1.0
121 */
122public class ShiroFilterFactoryBean implements FactoryBean, BeanPostProcessor {
123
124    private static final Logger LOGGER = LoggerFactory.getLogger(ShiroFilterFactoryBean.class);
125
126    private SecurityManager securityManager;
127
128    private Map<String, Filter> filters;
129
130    private List<String> globalFilters;
131
132    //urlPathExpression_to_comma-delimited-filter-chain-definition
133    private Map<String, String> filterChainDefinitionMap;
134
135    private String loginUrl;
136    private String successUrl;
137    private String unauthorizedUrl;
138    private boolean caseInsensitive;
139
140    private AbstractShiroFilter instance;
141
142    private ShiroFilterConfiguration filterConfiguration;
143
144    public ShiroFilterFactoryBean() {
145        this.filters = new LinkedHashMap<String, Filter>();
146        this.globalFilters = new ArrayList<>();
147        this.globalFilters.add(DefaultFilter.invalidRequest.name());
148        //order matters!
149        this.filterChainDefinitionMap = new LinkedHashMap<String, String>();
150        this.filterConfiguration = new ShiroFilterConfiguration();
151    }
152
153    /**
154     * Gets the application {@code SecurityManager} instance to be used by the constructed Shiro Filter.  This is a
155     * required property - failure to set it will throw an initialization exception.
156     *
157     * @return the application {@code SecurityManager} instance to be used by the constructed Shiro Filter.
158     */
159    public SecurityManager getSecurityManager() {
160        return securityManager;
161    }
162
163    /**
164     * Sets the application {@code SecurityManager} instance to be used by the constructed Shiro Filter.  This is a
165     * required property - failure to set it will throw an initialization exception.
166     *
167     * @param securityManager the application {@code SecurityManager} instance to be used by the constructed Shiro Filter.
168     */
169    public void setSecurityManager(SecurityManager securityManager) {
170        this.securityManager = securityManager;
171    }
172
173    /**
174     * Gets the application {@code ShiroFilterConfiguration} instance to be used by the constructed Shiro Filter.
175     *
176     * @return the application {@code ShiroFilterConfiguration} instance to be used by the constructed Shiro Filter.
177     */
178    public ShiroFilterConfiguration getShiroFilterConfiguration() {
179        return filterConfiguration;
180    }
181
182    /**
183     * Sets the application {@code ShiroFilterConfiguration} instance to be used by the constructed Shiro Filter.
184     *
185     * @param filterConfiguration the application {@code SecurityManager} instance to be used by the constructed Shiro Filter.
186     */
187    public void setShiroFilterConfiguration(ShiroFilterConfiguration filterConfiguration) {
188        this.filterConfiguration = filterConfiguration;
189    }
190
191    /**
192     * Returns the application's login URL to be assigned to all acquired Filters that subclass
193     * {@link AccessControlFilter} or {@code null} if no value should be assigned globally. The default value
194     * is {@code null}.
195     *
196     * @return the application's login URL to be assigned to all acquired Filters that subclass
197     * {@link AccessControlFilter} or {@code null} if no value should be assigned globally.
198     * @see #setLoginUrl
199     */
200    public String getLoginUrl() {
201        return loginUrl;
202    }
203
204    /**
205     * Sets the application's login URL to be assigned to all acquired Filters that subclass
206     * {@link AccessControlFilter}.  This is a convenience mechanism: for all configured {@link #setFilters filters},
207     * as well for any default ones ({@code authc}, {@code user}, etc.), this value will be passed on to each Filter
208     * via the {@link AccessControlFilter#setLoginUrl(String)} method<b>*</b>.  This eliminates the need to
209     * configure the 'loginUrl' property manually on each filter instance, and instead that can be configured once
210     * via this attribute.
211     * <p/>
212     * <b>*</b>If a filter already has already been explicitly configured with a value, it will
213     * <em>not</em> receive this value. Individual filter configuration overrides this global convenience property.
214     *
215     * @param loginUrl the application's login URL to apply to as a convenience to all discovered
216     *                 {@link AccessControlFilter} instances.
217     * @see AccessControlFilter#setLoginUrl(String)
218     */
219    public void setLoginUrl(String loginUrl) {
220        this.loginUrl = loginUrl;
221    }
222
223    /**
224     * Returns the application's after-login success URL to be assigned to all acquired Filters that subclass
225     * {@link AuthenticationFilter} or {@code null} if no value should be assigned globally. The default value
226     * is {@code null}.
227     *
228     * @return the application's after-login success URL to be assigned to all acquired Filters that subclass
229     * {@link AuthenticationFilter} or {@code null} if no value should be assigned globally.
230     * @see #setSuccessUrl
231     */
232    public String getSuccessUrl() {
233        return successUrl;
234    }
235
236    /**
237     * Sets the application's after-login success URL to be assigned to all acquired Filters that subclass
238     * {@link AuthenticationFilter}.  This is a convenience mechanism: for all configured {@link #setFilters filters},
239     * as well for any default ones ({@code authc}, {@code user}, etc.), this value will be passed on to each Filter
240     * via the {@link AuthenticationFilter#setSuccessUrl(String)} method<b>*</b>.  This eliminates the need to
241     * configure the 'successUrl' property manually on each filter instance, and instead that can be configured once
242     * via this attribute.
243     * <p/>
244     * <b>*</b>If a filter already has already been explicitly configured with a value, it will
245     * <em>not</em> receive this value. Individual filter configuration overrides this global convenience property.
246     *
247     * @param successUrl the application's after-login success URL to apply to as a convenience to all discovered
248     *                   {@link AccessControlFilter} instances.
249     * @see AuthenticationFilter#setSuccessUrl(String)
250     */
251    public void setSuccessUrl(String successUrl) {
252        this.successUrl = successUrl;
253    }
254
255    /**
256     * Returns the application's after-login success URL to be assigned to all acquired Filters that subclass
257     * {@link AuthenticationFilter} or {@code null} if no value should be assigned globally. The default value
258     * is {@code null}.
259     *
260     * @return the application's after-login success URL to be assigned to all acquired Filters that subclass
261     * {@link AuthenticationFilter} or {@code null} if no value should be assigned globally.
262     * @see #setSuccessUrl
263     */
264    public String getUnauthorizedUrl() {
265        return unauthorizedUrl;
266    }
267
268    /**
269     * Sets the application's 'unauthorized' URL to be assigned to all acquired Filters that subclass
270     * {@link AuthorizationFilter}.  This is a convenience mechanism: for all configured {@link #setFilters filters},
271     * as well for any default ones ({@code roles}, {@code perms}, etc.), this value will be passed on to each Filter
272     * via the {@link AuthorizationFilter#setUnauthorizedUrl(String)} method<b>*</b>.  This eliminates the need to
273     * configure the 'unauthorizedUrl' property manually on each filter instance, and instead that can be configured once
274     * via this attribute.
275     * <p/>
276     * <b>*</b>If a filter already has already been explicitly configured with a value, it will
277     * <em>not</em> receive this value. Individual filter configuration overrides this global convenience property.
278     *
279     * @param unauthorizedUrl the application's 'unauthorized' URL to apply to as a convenience to all discovered
280     *                        {@link AuthorizationFilter} instances.
281     * @see AuthorizationFilter#setUnauthorizedUrl(String)
282     */
283    public void setUnauthorizedUrl(String unauthorizedUrl) {
284        this.unauthorizedUrl = unauthorizedUrl;
285    }
286
287    /**
288     * @return true if filter chain matching should be case insensitive.
289     */
290    public boolean isCaseInsensitive() {
291        return caseInsensitive;
292    }
293
294    /**
295     * Sets whether filter chain matching should be case insensitive.
296     * @param caseInsensitive true if filter chain matching should be case insensitive.
297     */
298    public void setCaseInsensitive(boolean caseInsensitive) {
299        this.caseInsensitive = caseInsensitive;
300    }
301
302    /**
303     * Returns the filterName-to-Filter map of filters available for reference when defining filter chain definitions.
304     * All filter chain definitions will reference filters by the names in this map (i.e. the keys).
305     *
306     * @return the filterName-to-Filter map of filters available for reference when defining filter chain definitions.
307     */
308    public Map<String, Filter> getFilters() {
309        return filters;
310    }
311
312    /**
313     * Sets the filterName-to-Filter map of filters available for reference when creating
314     * {@link #setFilterChainDefinitionMap(java.util.Map) filter chain definitions}.
315     * <p/>
316     * <b>Note:</b> This property is optional:  this {@code FactoryBean} implementation will discover all beans in the
317     * web application context that implement the {@link Filter} interface and automatically add them to this filter
318     * map under their bean name.
319     * <p/>
320     * For example, just defining this bean in a web Spring XML application context:
321     * <pre>
322     * &lt;bean id=&quot;myFilter&quot; class=&quot;com.class.that.implements.javax.servlet.Filter&quot;&gt;
323     * ...
324     * &lt;/bean&gt;</pre>
325     * Will automatically place that bean into this Filters map under the key '<b>myFilter</b>'.
326     *
327     * @param filters the optional filterName-to-Filter map of filters available for reference when creating
328     *                {@link #setFilterChainDefinitionMap (java.util.Map) filter chain definitions}.
329     */
330    public void setFilters(Map<String, Filter> filters) {
331        this.filters = filters;
332    }
333
334    /**
335     * Returns the chainName-to-chainDefinition map of chain definitions to use for creating filter chains intercepted
336     * by the Shiro Filter.  Each map entry should conform to the format defined by the
337     * {@link FilterChainManager#createChain(String, String)} JavaDoc, where the map key is the chain name (e.g. URL
338     * path expression) and the map value is the comma-delimited string chain definition.
339     *
340     * @return he chainName-to-chainDefinition map of chain definitions to use for creating filter chains intercepted
341     * by the Shiro Filter.
342     */
343    public Map<String, String> getFilterChainDefinitionMap() {
344        return filterChainDefinitionMap;
345    }
346
347    /**
348     * Sets the chainName-to-chainDefinition map of chain definitions to use for creating filter chains intercepted
349     * by the Shiro Filter.  Each map entry should conform to the format defined by the
350     * {@link FilterChainManager#createChain(String, String)} JavaDoc, where the map key is the chain name (e.g. URL
351     * path expression) and the map value is the comma-delimited string chain definition.
352     *
353     * @param filterChainDefinitionMap the chainName-to-chainDefinition map of chain definitions to use for creating
354     *                                 filter chains intercepted by the Shiro Filter.
355     */
356    public void setFilterChainDefinitionMap(Map<String, String> filterChainDefinitionMap) {
357        this.filterChainDefinitionMap = filterChainDefinitionMap;
358    }
359
360    /**
361     * A convenience method that sets the {@link #setFilterChainDefinitionMap(java.util.Map) filterChainDefinitionMap}
362     * property by accepting a {@link java.util.Properties Properties}-compatible string (multi-line key/value pairs).
363     * Each key/value pair must conform to the format defined by the
364     * {@link FilterChainManager#createChain(String, String)} JavaDoc - each property key is an ant URL
365     * path expression and the value is the comma-delimited chain definition.
366     *
367     * @param definitions a {@link java.util.Properties Properties}-compatible string (multi-line key/value pairs)
368     *                    where each key/value pair represents a single urlPathExpression-commaDelimitedChainDefinition.
369     */
370    public void setFilterChainDefinitions(String definitions) {
371        Ini ini = new Ini();
372        ini.load(definitions);
373        //did they explicitly state a 'urls' section?  Not necessary, but just in case:
374        Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS);
375        if (CollectionUtils.isEmpty(section)) {
376            //no urls section.  Since this _is_ a urls chain definition property, just assume the
377            //default section contains only the definitions:
378            section = ini.getSection(Ini.DEFAULT_SECTION_NAME);
379        }
380        setFilterChainDefinitionMap(section);
381    }
382
383    /**
384     * Sets the list of filters that will be executed against every request.
385     * Defaults to the {@link InvalidRequestFilter} which will block known invalid request attacks.
386     *
387     * @param globalFilters the list of filters to execute before specific path filters.
388     */
389    public void setGlobalFilters(List<String> globalFilters) {
390        this.globalFilters = globalFilters;
391    }
392
393    /**
394     * Lazily creates and returns a {@link AbstractShiroFilter} concrete instance via the
395     * {@link #createInstance} method.
396     *
397     * @return the application's Shiro Filter instance used to filter incoming web requests.
398     * @throws Exception if there is a problem creating the {@code Filter} instance.
399     */
400    public Object getObject() throws Exception {
401        if (instance == null) {
402            instance = createInstance();
403        }
404        return instance;
405    }
406
407    /**
408     * Returns <code>{@link org.apache.shiro.web.servlet.AbstractShiroFilter}.class</code>
409     *
410     * @return <code>{@link org.apache.shiro.web.servlet.AbstractShiroFilter}.class</code>
411     */
412    public Class getObjectType() {
413        return SpringShiroFilter.class;
414    }
415
416    /**
417     * Returns {@code true} always.  There is almost always only ever 1 Shiro {@code Filter} per web application.
418     *
419     * @return {@code true} always.  There is almost always only ever 1 Shiro {@code Filter} per web application.
420     */
421    public boolean isSingleton() {
422        return true;
423    }
424
425    protected FilterChainManager createFilterChainManager() {
426
427        DefaultFilterChainManager manager = new DefaultFilterChainManager();
428        manager.setCaseInsensitive(caseInsensitive);
429        Map<String, Filter> defaultFilters = manager.getFilters();
430        //apply global settings if necessary:
431        for (Filter filter : defaultFilters.values()) {
432            applyGlobalPropertiesIfNecessary(filter);
433        }
434
435        //Apply the acquired and/or configured filters:
436        Map<String, Filter> filters = getFilters();
437        if (!CollectionUtils.isEmpty(filters)) {
438            for (Map.Entry<String, Filter> entry : filters.entrySet()) {
439                String name = entry.getKey();
440                Filter filter = entry.getValue();
441                applyGlobalPropertiesIfNecessary(filter);
442                if (filter instanceof Nameable) {
443                    ((Nameable) filter).setName(name);
444                }
445                //'init' argument is false, since Spring-configured filters should be initialized
446                //in Spring (i.e. 'init-method=blah') or implement InitializingBean:
447                manager.addFilter(name, filter, false);
448            }
449        }
450
451        // set the global filters
452        manager.setGlobalFilters(this.globalFilters);
453
454        //build up the chains:
455        Map<String, String> chains = getFilterChainDefinitionMap();
456        if (!CollectionUtils.isEmpty(chains)) {
457            for (Map.Entry<String, String> entry : chains.entrySet()) {
458                String url = entry.getKey();
459                String chainDefinition = entry.getValue();
460                manager.createChain(url, chainDefinition);
461            }
462        }
463
464        // create the default chain, to match anything the path matching would have missed
465        // TODO this assumes ANT path matching, which might be OK here
466        manager.createDefaultChain("/**");
467
468        return manager;
469    }
470
471    /**
472     * This implementation:
473     * <ol>
474     * <li>Ensures the required {@link #setSecurityManager(org.apache.shiro.mgt.SecurityManager) securityManager}
475     * property has been set</li>
476     * <li>{@link #createFilterChainManager() Creates} a {@link FilterChainManager} instance that reflects the
477     * configured {@link #setFilters(java.util.Map) filters} and
478     * {@link #setFilterChainDefinitionMap(java.util.Map) filter chain definitions}</li>
479     * <li>Wraps the FilterChainManager with a suitable
480     * {@link org.apache.shiro.web.filter.mgt.FilterChainResolver FilterChainResolver} since the Shiro Filter
481     * implementations do not know of {@code FilterChainManager}s</li>
482     * <li>Sets both the {@code SecurityManager} and {@code FilterChainResolver} instances on a new Shiro Filter
483     * instance and returns that filter instance.</li>
484     * </ol>
485     *
486     * @return a new Shiro Filter reflecting any configured filters and filter chain definitions.
487     * @throws Exception if there is a problem creating the AbstractShiroFilter instance.
488     */
489    protected AbstractShiroFilter createInstance() throws Exception {
490
491        LOGGER.debug("Creating Shiro Filter instance.");
492
493        SecurityManager securityManager = getSecurityManager();
494        if (securityManager == null) {
495            String msg = "SecurityManager property must be set.";
496            throw new BeanInitializationException(msg);
497        }
498
499        if (!(securityManager instanceof WebSecurityManager)) {
500            String msg = "The security manager does not implement the WebSecurityManager interface.";
501            throw new BeanInitializationException(msg);
502        }
503
504        FilterChainManager manager = createFilterChainManager();
505
506        //Expose the constructed FilterChainManager by first wrapping it in a
507        // FilterChainResolver implementation. The AbstractShiroFilter implementations
508        // do not know about FilterChainManagers - only resolvers:
509        PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver().caseInsensitive(caseInsensitive);
510        chainResolver.setFilterChainManager(manager);
511
512        //Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built
513        //FilterChainResolver.  It doesn't matter that the instance is an anonymous inner class
514        //here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts
515        //injection of the SecurityManager and FilterChainResolver:
516        return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver, getShiroFilterConfiguration());
517    }
518
519    private void applyLoginUrlIfNecessary(Filter filter) {
520        String loginUrl = getLoginUrl();
521        if (StringUtils.hasText(loginUrl) && (filter instanceof AccessControlFilter)) {
522            AccessControlFilter acFilter = (AccessControlFilter) filter;
523            //only apply the login url if they haven't explicitly configured one already:
524            String existingLoginUrl = acFilter.getLoginUrl();
525            if (AccessControlFilter.DEFAULT_LOGIN_URL.equals(existingLoginUrl)) {
526                acFilter.setLoginUrl(loginUrl);
527            }
528        }
529    }
530
531    private void applySuccessUrlIfNecessary(Filter filter) {
532        String successUrl = getSuccessUrl();
533        if (StringUtils.hasText(successUrl) && (filter instanceof AuthenticationFilter)) {
534            AuthenticationFilter authcFilter = (AuthenticationFilter) filter;
535            //only apply the successUrl if they haven't explicitly configured one already:
536            String existingSuccessUrl = authcFilter.getSuccessUrl();
537            if (AuthenticationFilter.DEFAULT_SUCCESS_URL.equals(existingSuccessUrl)) {
538                authcFilter.setSuccessUrl(successUrl);
539            }
540        }
541    }
542
543    private void applyUnauthorizedUrlIfNecessary(Filter filter) {
544        String unauthorizedUrl = getUnauthorizedUrl();
545        if (StringUtils.hasText(unauthorizedUrl) && (filter instanceof AuthorizationFilter)) {
546            AuthorizationFilter authzFilter = (AuthorizationFilter) filter;
547            //only apply the unauthorizedUrl if they haven't explicitly configured one already:
548            String existingUnauthorizedUrl = authzFilter.getUnauthorizedUrl();
549            if (existingUnauthorizedUrl == null) {
550                authzFilter.setUnauthorizedUrl(unauthorizedUrl);
551            }
552        }
553    }
554
555    private void applyGlobalPropertiesIfNecessary(Filter filter) {
556        applyLoginUrlIfNecessary(filter);
557        applySuccessUrlIfNecessary(filter);
558        applyUnauthorizedUrlIfNecessary(filter);
559
560        if (filter instanceof OncePerRequestFilter) {
561            ((OncePerRequestFilter) filter).setFilterOncePerRequest(filterConfiguration.isFilterOncePerRequest());
562        }
563    }
564
565    /**
566     * Inspects a bean, and if it implements the {@link Filter} interface, automatically adds that filter
567     * instance to the internal {@link #setFilters(java.util.Map) filters map} that will be referenced
568     * later during filter chain construction.
569     */
570    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
571        if (bean instanceof Filter) {
572            LOGGER.debug("Found filter chain candidate filter '{}'", beanName);
573            Filter filter = (Filter) bean;
574            applyGlobalPropertiesIfNecessary(filter);
575            getFilters().put(beanName, filter);
576        } else {
577            LOGGER.trace("Ignoring non-Filter bean '{}'", beanName);
578        }
579        return bean;
580    }
581
582    /**
583     * Does nothing - only exists to satisfy the BeanPostProcessor interface and immediately returns the
584     * {@code bean} argument.
585     */
586    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
587        return bean;
588    }
589
590    /**
591     * Ordinarily the {@code AbstractShiroFilter} must be subclassed to additionally perform configuration
592     * and initialization behavior.  Because this {@code FactoryBean} implementation manually builds the
593     * {@link AbstractShiroFilter}'s
594     * {@link AbstractShiroFilter#setSecurityManager(org.apache.shiro.web.mgt.WebSecurityManager) securityManager} and
595     * {@link AbstractShiroFilter#setFilterChainResolver(org.apache.shiro.web.filter.mgt.FilterChainResolver) filterChainResolver}
596     * properties, the only thing left to do is set those properties explicitly.  We do that in a simple
597     * concrete subclass in the constructor.
598     */
599    private static final class SpringShiroFilter extends AbstractShiroFilter {
600
601        protected SpringShiroFilter(WebSecurityManager webSecurityManager,
602                                    FilterChainResolver resolver,
603                                    ShiroFilterConfiguration filterConfiguration) {
604            super();
605            if (webSecurityManager == null) {
606                throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
607            }
608            setSecurityManager(webSecurityManager);
609            setShiroFilterConfiguration(filterConfiguration);
610
611            if (resolver != null) {
612                setFilterChainResolver(resolver);
613            }
614        }
615    }
616}