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 * @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 * <bean id="myFilter" class="com.class.that.implements.javax.servlet.Filter">
274 * ...
275 * </bean></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 }