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