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.util.Nameable; 025import org.apache.shiro.util.StringUtils; 026import org.apache.shiro.web.config.IniFilterChainResolverFactory; 027import org.apache.shiro.web.filter.AccessControlFilter; 028import org.apache.shiro.web.filter.authc.AuthenticationFilter; 029import org.apache.shiro.web.filter.authz.AuthorizationFilter; 030import org.apache.shiro.web.filter.mgt.DefaultFilterChainManager; 031import org.apache.shiro.web.filter.mgt.FilterChainManager; 032import org.apache.shiro.web.filter.mgt.FilterChainResolver; 033import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver; 034import org.apache.shiro.web.mgt.WebSecurityManager; 035import org.apache.shiro.web.servlet.AbstractShiroFilter; 036import org.slf4j.Logger; 037import org.slf4j.LoggerFactory; 038import org.springframework.beans.BeansException; 039import org.springframework.beans.factory.BeanInitializationException; 040import org.springframework.beans.factory.FactoryBean; 041import org.springframework.beans.factory.config.BeanPostProcessor; 042 043import javax.servlet.Filter; 044import java.util.LinkedHashMap; 045import 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 */ 116public 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}