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