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.web.env; 020 021import org.apache.shiro.config.ConfigurationException; 022import org.apache.shiro.config.Ini; 023import org.apache.shiro.ini.IniFactorySupport; 024import org.apache.shiro.lang.io.ResourceUtils; 025import org.apache.shiro.lang.util.Destroyable; 026import org.apache.shiro.lang.util.Factory; 027import org.apache.shiro.lang.util.Initializable; 028import org.apache.shiro.lang.util.StringUtils; 029import org.apache.shiro.util.CollectionUtils; 030import org.apache.shiro.web.config.IniFilterChainResolverFactory; 031import org.apache.shiro.web.config.ShiroFilterConfiguration; 032import org.apache.shiro.web.config.WebIniSecurityManagerFactory; 033import org.apache.shiro.web.filter.mgt.FilterChainResolver; 034import org.apache.shiro.web.mgt.WebSecurityManager; 035import org.apache.shiro.web.util.WebUtils; 036import org.slf4j.Logger; 037import org.slf4j.LoggerFactory; 038 039import javax.servlet.ServletContext; 040import java.io.IOException; 041import java.io.InputStream; 042import java.util.HashMap; 043import java.util.Map; 044 045/** 046 * {@link WebEnvironment} implementation configured by an {@link Ini} instance or {@code Ini} resource locations. 047 * 048 * @since 1.2 049 */ 050public class IniWebEnvironment extends ResourceBasedWebEnvironment implements Initializable, Destroyable { 051 052 /** 053 * web ini resource path. 054 */ 055 public static final String DEFAULT_WEB_INI_RESOURCE_PATH = "/WEB-INF/shiro.ini"; 056 /** 057 * filter chain resolver name. 058 */ 059 public static final String FILTER_CHAIN_RESOLVER_NAME = "filterChainResolver"; 060 061 /** 062 * shiro filter config name. 063 */ 064 public static final String SHIRO_FILTER_CONFIG_NAME = "shiroFilter"; 065 066 private static final Logger LOGGER = LoggerFactory.getLogger(IniWebEnvironment.class); 067 068 /** 069 * The Ini that configures this WebEnvironment instance. 070 */ 071 private Ini ini; 072 073 @SuppressWarnings("deprecation") 074 private WebIniSecurityManagerFactory factory; 075 076 @SuppressWarnings("deprecation") 077 public IniWebEnvironment() { 078 factory = new WebIniSecurityManagerFactory(); 079 } 080 081 /** 082 * Initializes this instance by resolving any potential (explicit or resource-configured) {@link Ini} 083 * configuration and calling {@link #configure() configure} for actual instance configuration. 084 */ 085 public void init() { 086 087 setIni(parseConfig()); 088 089 configure(); 090 } 091 092 /** 093 * Loads configuration {@link Ini} from {@link #getConfigLocations()} if set, otherwise falling back 094 * to the {@link #getDefaultConfigLocations()}. Finally any Ini objects will be merged with the value returned 095 * from {@link #getFrameworkIni()} 096 * 097 * @return Ini configuration to be used by this Environment. 098 * @since 1.4 099 */ 100 protected Ini parseConfig() { 101 Ini ini = getIni(); 102 103 String[] configLocations = getConfigLocations(); 104 105 if (LOGGER.isWarnEnabled() && !CollectionUtils.isEmpty(ini) 106 && configLocations != null && configLocations.length > 0) { 107 LOGGER.warn("Explicit INI instance has been provided, but configuration locations have also been " 108 + "specified. The {} implementation does not currently support multiple Ini config, but this may " 109 + "be supported in the future. Only the INI instance will be used for configuration.", 110 IniWebEnvironment.class.getName()); 111 } 112 113 if (CollectionUtils.isEmpty(ini)) { 114 LOGGER.debug("Checking any specified config locations."); 115 ini = getSpecifiedIni(configLocations); 116 } 117 118 if (CollectionUtils.isEmpty(ini)) { 119 LOGGER.debug("No INI instance or config locations specified. Trying default config locations."); 120 ini = getDefaultIni(); 121 } 122 123 // Allow for integrations to provide default that will be merged other configuration. 124 // to retain backwards compatibility this must be a different method then 'getDefaultIni()' 125 ini = mergeIni(getFrameworkIni(), ini); 126 127 if (CollectionUtils.isEmpty(ini)) { 128 String msg = "Shiro INI configuration was either not found or discovered to be empty/unconfigured."; 129 throw new ConfigurationException(msg); 130 } 131 return ini; 132 } 133 134 protected void configure() { 135 136 this.objects.clear(); 137 138 WebSecurityManager securityManager = createWebSecurityManager(); 139 setWebSecurityManager(securityManager); 140 141 ShiroFilterConfiguration filterConfiguration = createFilterConfiguration(); 142 setShiroFilterConfiguration(filterConfiguration); 143 144 FilterChainResolver resolver = createFilterChainResolver(); 145 if (resolver != null) { 146 setFilterChainResolver(resolver); 147 } 148 } 149 150 /** 151 * Extension point to allow subclasses to provide an {@link Ini} configuration that will be merged into the 152 * users configuration. The users configuration will override anything set here. 153 * <p> 154 * <strong>NOTE:</strong> Framework developers should use with caution. It is possible a user could provide 155 * configuration that would conflict with the frameworks configuration. For example: if this method returns an 156 * Ini object with the following configuration: 157 * <pre><code> 158 * [main] 159 * realm = com.myco.FoobarRealm 160 * realm.foobarSpecificField = A string 161 * </code></pre> 162 * And the user provides a similar configuration: 163 * <pre><code> 164 * [main] 165 * realm = net.differentco.MyCustomRealm 166 * </code></pre> 167 * <p> 168 * This would merge into: 169 * <pre><code> 170 * [main] 171 * realm = net.differentco.MyCustomRealm 172 * realm.foobarSpecificField = A string 173 * </code></pre> 174 * <p> 175 * This may cause a configuration error if <code>MyCustomRealm</code 176 * does not contain the field <code>foobarSpecificField</code>. 177 * This can be avoided if the Framework Ini uses more unique names, such as <code>foobarRealm</code>. which would result 178 * in a merged configuration that looks like: 179 * <pre><code> 180 * [main] 181 * foobarRealm = com.myco.FoobarRealm 182 * foobarRealm.foobarSpecificField = A string 183 * realm = net.differentco.MyCustomRealm 184 * </code></pre> 185 * 186 * </p> 187 * 188 * @return Ini configuration used by the framework integrations. 189 * @since 1.4 190 */ 191 protected Ini getFrameworkIni() { 192 return null; 193 } 194 195 protected Ini getSpecifiedIni(String[] configLocations) throws ConfigurationException { 196 197 Ini ini = null; 198 199 if (configLocations != null && configLocations.length > 0) { 200 201 if (configLocations.length > 1) { 202 LOGGER.warn("More than one Shiro .ini config location has been specified. Only the first will be " 203 + "used for configuration as the {} implementation does not currently support multiple " 204 + "files. This may be supported in the future however.", IniWebEnvironment.class.getName()); 205 } 206 207 //required, as it is user specified: 208 ini = createIni(configLocations[0], true); 209 } 210 211 return ini; 212 } 213 214 protected Ini mergeIni(Ini ini1, Ini ini2) { 215 216 if (ini1 == null) { 217 return ini2; 218 } 219 220 if (ini2 == null) { 221 return ini1; 222 } 223 224 // at this point we have two valid ini objects, create a new one and merge the contents of 2 into 1 225 Ini iniResult = new Ini(ini1); 226 iniResult.merge(ini2); 227 228 return iniResult; 229 } 230 231 protected Ini getDefaultIni() { 232 233 Ini ini = null; 234 235 String[] configLocations = getDefaultConfigLocations(); 236 if (configLocations != null) { 237 for (String location : configLocations) { 238 ini = createIni(location, false); 239 if (!CollectionUtils.isEmpty(ini)) { 240 LOGGER.debug("Discovered non-empty INI configuration at location '{}'. Using for configuration.", 241 location); 242 break; 243 } 244 } 245 } 246 247 return ini; 248 } 249 250 /** 251 * Creates an {@link Ini} instance reflecting the specified path, or {@code null} if the path does not exist and 252 * is not required. 253 * <p/> 254 * If the path is required and does not exist or is empty, a {@link ConfigurationException} will be thrown. 255 * 256 * @param configLocation the resource path to load into an {@code Ini} instance. 257 * @param required if the path must exist and be converted to a non-empty {@link Ini} instance. 258 * @return an {@link Ini} instance reflecting the specified path, or {@code null} if the path does not exist and 259 * is not required. 260 * @throws ConfigurationException if the path is required but results in a null or empty Ini instance. 261 */ 262 protected Ini createIni(String configLocation, boolean required) throws ConfigurationException { 263 264 Ini ini = null; 265 266 if (configLocation != null) { 267 ini = convertPathToIni(configLocation, required); 268 } 269 if (required && CollectionUtils.isEmpty(ini)) { 270 String msg = "Required configuration location '" + configLocation + "' does not exist or did not " 271 + "contain any INI configuration."; 272 throw new ConfigurationException(msg); 273 } 274 275 return ini; 276 } 277 278 279 protected ShiroFilterConfiguration createFilterConfiguration() { 280 return (ShiroFilterConfiguration) this.objects.get(SHIRO_FILTER_CONFIG_NAME); 281 } 282 283 @SuppressWarnings("deprecation") 284 protected FilterChainResolver createFilterChainResolver() { 285 286 FilterChainResolver resolver = null; 287 288 Ini ini = getIni(); 289 290 if (!CollectionUtils.isEmpty(ini)) { 291 @SuppressWarnings("unchecked") 292 Factory<FilterChainResolver> factory = (Factory<FilterChainResolver>) this.objects.get(FILTER_CHAIN_RESOLVER_NAME); 293 if (factory instanceof IniFactorySupport) { 294 var iniFactory = (IniFactorySupport<?>) factory; 295 iniFactory.setIni(ini); 296 iniFactory.setDefaults(this.objects); 297 } 298 resolver = factory.getInstance(); 299 } 300 301 return resolver; 302 } 303 304 protected WebSecurityManager createWebSecurityManager() { 305 306 Ini ini = getIni(); 307 if (!CollectionUtils.isEmpty(ini)) { 308 factory.setIni(ini); 309 } 310 311 Map<String, Object> defaults = getDefaults(); 312 if (!CollectionUtils.isEmpty(defaults)) { 313 factory.setDefaults(defaults); 314 } 315 316 WebSecurityManager wsm = (WebSecurityManager) factory.getInstance(); 317 318 //SHIRO-306 - get beans after they've been created (the call was before the factory.getInstance() call, 319 //which always returned null. 320 Map<String, ?> beans = factory.getBeans(); 321 if (!CollectionUtils.isEmpty(beans)) { 322 this.objects.putAll(beans); 323 } 324 325 return wsm; 326 } 327 328 /** 329 * Returns an array with two elements, {@code /WEB-INF/shiro.ini} and {@code classpath:shiro.ini}. 330 * 331 * @return an array with two elements, {@code /WEB-INF/shiro.ini} and {@code classpath:shiro.ini}. 332 */ 333 @SuppressWarnings("deprecation") 334 protected String[] getDefaultConfigLocations() { 335 return new String[] { 336 DEFAULT_WEB_INI_RESOURCE_PATH, 337 IniFactorySupport.DEFAULT_INI_RESOURCE_PATH 338 }; 339 } 340 341 /** 342 * Converts the specified file path to an {@link Ini} instance. 343 * <p/> 344 * If the path does not have a resource prefix as defined by {@link ResourceUtils#hasResourcePrefix(String)}, 345 * the path is expected to be resolvable by the {@code ServletContext} via 346 * {@link javax.servlet.ServletContext#getResourceAsStream(String)}. 347 * 348 * @param path the path of the INI resource to load into an INI instance. 349 * @param required if the specified path must exist 350 * @return an INI instance populated based on the given INI resource path. 351 */ 352 private Ini convertPathToIni(String path, boolean required) { 353 354 //TODO - this logic is ugly - it'd be ideal if we had a Resource API to polymorphically encapsulate this behavior 355 356 Ini ini = null; 357 358 if (StringUtils.hasText(path)) { 359 InputStream is = null; 360 361 //SHIRO-178: Check for servlet context resource and not only resource paths: 362 if (!ResourceUtils.hasResourcePrefix(path)) { 363 is = getServletContextResourceStream(path); 364 } else { 365 try { 366 is = ResourceUtils.getInputStreamForPath(path); 367 } catch (IOException e) { 368 if (required) { 369 throw new ConfigurationException(e); 370 } else { 371 if (LOGGER.isDebugEnabled()) { 372 LOGGER.debug("Unable to load optional path '" + path + "'.", e); 373 } 374 } 375 } 376 } 377 if (is != null) { 378 ini = new Ini(); 379 ini.load(is); 380 } else { 381 if (required) { 382 throw new ConfigurationException("Unable to load resource path '" + path + "'"); 383 } 384 } 385 } 386 387 return ini; 388 } 389 390 //TODO - this logic is ugly - it'd be ideal if we had a Resource API to polymorphically encapsulate this behavior 391 private InputStream getServletContextResourceStream(String path) { 392 InputStream is = null; 393 394 path = WebUtils.normalize(path); 395 ServletContext sc = getServletContext(); 396 if (sc != null) { 397 is = sc.getResourceAsStream(path); 398 } 399 400 return is; 401 } 402 403 /** 404 * Returns the {@code Ini} instance reflecting this WebEnvironment's configuration. 405 * 406 * @return the {@code Ini} instance reflecting this WebEnvironment's configuration. 407 */ 408 public Ini getIni() { 409 return this.ini; 410 } 411 412 /** 413 * Allows for configuration via a direct {@link Ini} instance instead of via 414 * {@link #getConfigLocations() config locations}. 415 * <p/> 416 * If the specified instance is null or empty, the fallback/default resource-based configuration will be used. 417 * 418 * @param ini the ini instance to use for creation. 419 */ 420 public void setIni(Ini ini) { 421 this.ini = ini; 422 } 423 424 protected Map<String, Object> getDefaults() { 425 Map<String, Object> defaults = new HashMap<String, Object>(); 426 defaults.put(FILTER_CHAIN_RESOLVER_NAME, new IniFilterChainResolverFactory()); 427 defaults.put(SHIRO_FILTER_CONFIG_NAME, new ShiroFilterConfiguration()); 428 return defaults; 429 } 430 431 /** 432 * Returns the SecurityManager factory used by this WebEnvironment. 433 * 434 * @return the SecurityManager factory used by this WebEnvironment. 435 * @since 1.4 436 */ 437 @SuppressWarnings({"unused", "deprecation"}) 438 protected WebIniSecurityManagerFactory getSecurityManagerFactory() { 439 return factory; 440 } 441 442 /** 443 * Allows for setting the SecurityManager factory which will be used to create the SecurityManager. 444 * 445 * @param factory the SecurityManager factory to used. 446 * @since 1.4 447 */ 448 @SuppressWarnings("deprecation") 449 protected void setSecurityManagerFactory(WebIniSecurityManagerFactory factory) { 450 this.factory = factory; 451 } 452}