View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.web.env;
20  
21  import org.apache.shiro.config.ConfigurationException;
22  import org.apache.shiro.config.ResourceConfigurable;
23  import org.apache.shiro.lang.util.ClassUtils;
24  import org.apache.shiro.lang.util.LifecycleUtils;
25  import org.apache.shiro.lang.util.StringUtils;
26  import org.apache.shiro.lang.util.UnknownClassException;
27  import org.slf4j.Logger;
28  import org.slf4j.LoggerFactory;
29  
30  import javax.servlet.ServletContext;
31  import java.util.ArrayList;
32  import java.util.Iterator;
33  import java.util.List;
34  import java.util.ServiceLoader;
35  
36  
37  /**
38   * An {@code EnvironmentLoader} is responsible for loading a web application's Shiro {@link WebEnvironment}
39   * (which includes the web app's {@link org.apache.shiro.web.mgt.WebSecurityManager WebSecurityManager}) into the
40   * {@code ServletContext} at application startup.
41   * <p/>
42   * In Shiro 1.1 and earlier, the Shiro ServletFilter was responsible for creating the {@code WebSecurityManager} and
43   * any additional objects (security filters, etc.).  However, any component not filtered by the Shiro Filter (such
44   * as other context listeners) was not able to easily acquire the these objects to perform security operations.
45   * <p/>
46   * Due to this, in Shiro 1.2 and later, this {@code EnvironmentLoader} (or more likely, the
47   * {@link EnvironmentLoaderListener} subclass) is the preferred mechanism to initialize
48   * a Shiro environment.  The Shiro Filter, while still required for request filtering, will not perform this
49   * initialization at startup if the {@code EnvironmentLoader} (or listener) runs first.
50   * <h2>Usage</h2>
51   * This implementation will look for two servlet context {@code context-param}s in {@code web.xml}:
52   * {@code shiroEnvironmentClass} and {@code shiroConfigLocations} that customize how the {@code WebEnvironment} instance
53   * will be initialized.
54   * <h3>shiroEnvironmentClass</h3>
55   * The {@code shiroEnvironmentClass} {@code context-param}, if it exists, allows you to specify the
56   * fully-qualified implementation class name of the {@link WebEnvironment} to instantiate.  For example:
57   * <pre>
58   * &lt;context-param&gt;
59   *     &lt;param-name&gt;shiroEnvironmentClass&lt;/param-name&gt;
60   *     &lt;param-value&gt;com.foo.bar.shiro.MyWebEnvironment&lt;/param-value&gt;
61   * &lt;/context-param&gt;
62   * </pre>
63   * If not specified, the default value is the {@link IniWebEnvironment} class, which assumes Shiro's default
64   * <a href="http://shiro.apache.org/configuration.html">INI configuration format</a>
65   * <h3>shiroConfigLocations</h3>
66   * The {@code shiroConfigLocations} {@code context-param}, if it exists, allows you to specify the config location(s)
67   * (resource path(s)) that will be relayed to the instantiated {@link WebEnvironment}.  For example:
68   * <pre>
69   * &lt;context-param&gt;
70   *     &lt;param-name&gt;shiroConfigLocations&lt;/param-name&gt;
71   *     &lt;param-value&gt;/WEB-INF/someLocation/shiro.ini&lt;/param-value&gt;
72   * &lt;/context-param&gt;
73   * </pre>
74   * The {@code WebEnvironment} implementation must implement the {@link ResourceConfigurable} interface if it is to
75   * acquire the {@code shiroConfigLocations} value.
76   * <p/>
77   * If this {@code context-param} is not specified, the {@code WebEnvironment} instance determines default resource
78   * lookup behavior.  For example, the {@link IniWebEnvironment} will check the following two locations for INI config
79   * by default (in order):
80   * <ol>
81   * <li>/WEB-INF/shiro.ini</li>
82   * <li>classpath:shiro.ini</li>
83   * </ol>
84   * <h2>Web Security Enforcement</h2>
85   * Using this loader will only initialize Shiro's environment in a web application - it will not filter web requests or
86   * perform web-specific security operations.  To do this, you must ensure that you have also configured the
87   * {@link org.apache.shiro.web.servlet.ShiroFilter ShiroFilter} in {@code web.xml}.
88   * <p/>
89   * Finally, it should be noted that this implementation was based on ideas in Spring 3's
90   * {@code org.springframework.web.context.ContextLoader} implementation - no need to reinvent the wheel for this common
91   * behavior.
92   *
93   * @see EnvironmentLoaderListener
94   * @see org.apache.shiro.web.servlet.ShiroFilter ShiroFilter
95   * @since 1.2
96   */
97  public class EnvironmentLoader {
98  
99      /**
100      * Servlet Context config param for specifying the {@link WebEnvironment} implementation class to use:
101      * {@code shiroEnvironmentClass}
102      */
103     public static final String ENVIRONMENT_CLASS_PARAM = "shiroEnvironmentClass";
104 
105     /**
106      * Servlet Context config param for the resource path to use for configuring the {@link WebEnvironment} instance:
107      * {@code shiroConfigLocations}
108      */
109     public static final String CONFIG_LOCATIONS_PARAM = "shiroConfigLocations";
110 
111     /**
112      * environment attribute key.
113      */
114     public static final String ENVIRONMENT_ATTRIBUTE_KEY = EnvironmentLoader.class.getName() + ".ENVIRONMENT_ATTRIBUTE_KEY";
115 
116     private static final Logger LOGGER = LoggerFactory.getLogger(EnvironmentLoader.class);
117 
118     /**
119      * Initializes Shiro's {@link WebEnvironment} instance for the specified {@code ServletContext} based on the
120      * {@link #CONFIG_LOCATIONS_PARAM} value.
121      *
122      * @param servletContext current servlet context
123      * @return the new Shiro {@code WebEnvironment} instance.
124      * @throws IllegalStateException if an existing WebEnvironment has already been initialized and associated with
125      *                               the specified {@code ServletContext}.
126      */
127     public WebEnvironment initEnvironment(ServletContext servletContext) throws IllegalStateException {
128 
129         if (servletContext.getAttribute(ENVIRONMENT_ATTRIBUTE_KEY) != null) {
130             String msg = "There is already a Shiro environment associated with the current ServletContext.  "
131                     + "Check if you have multiple EnvironmentLoader* definitions in your web.xml!";
132             throw new IllegalStateException(msg);
133         }
134 
135         servletContext.log("Initializing Shiro environment");
136         LOGGER.info("Starting Shiro environment initialization.");
137 
138         long startTime = System.currentTimeMillis();
139 
140         try {
141 
142             WebEnvironment environment = createEnvironment(servletContext);
143             servletContext.setAttribute(ENVIRONMENT_ATTRIBUTE_KEY, environment);
144 
145             LOGGER.debug("Published WebEnvironment as ServletContext attribute with name [{}]",
146                     ENVIRONMENT_ATTRIBUTE_KEY);
147 
148             if (LOGGER.isInfoEnabled()) {
149                 long elapsed = System.currentTimeMillis() - startTime;
150                 LOGGER.info("Shiro environment initialized in {} ms.", elapsed);
151             }
152 
153             return environment;
154         } catch (RuntimeException | Error ex) {
155             LOGGER.error("Shiro environment initialization failed", ex);
156             servletContext.setAttribute(ENVIRONMENT_ATTRIBUTE_KEY, ex);
157             throw ex;
158         }
159     }
160 
161     /**
162      * Return the WebEnvironment implementation class to use, either the default
163      * {@link IniWebEnvironment} or a custom class if specified.
164      *
165      * @param servletContext current servlet context
166      * @return the WebEnvironment implementation class to use
167      * @see #ENVIRONMENT_CLASS_PARAM
168      * @see IniWebEnvironment
169      * @see #determineWebEnvironment(ServletContext)
170      * @see #getDefaultWebEnvironmentClass(ServletContext)
171      * @deprecated This method is not longer used by Shiro, and will be removed in future versions,
172      * use {@link #determineWebEnvironment(ServletContext)} or {@link #determineWebEnvironment(ServletContext)}
173      */
174     @Deprecated
175     protected Class<?> determineWebEnvironmentClass(ServletContext servletContext) {
176         Class<? extends WebEnvironment> webEnvironmentClass = webEnvironmentClassFromServletContext(servletContext);
177         if (webEnvironmentClass != null) {
178             return webEnvironmentClass;
179         } else {
180 
181             return getDefaultWebEnvironmentClass(servletContext);
182         }
183     }
184 
185     private Class<? extends WebEnvironment> webEnvironmentClassFromServletContext(ServletContext servletContext) {
186 
187         Class<? extends WebEnvironment> webEnvironmentClass = null;
188         String className = servletContext.getInitParameter(ENVIRONMENT_CLASS_PARAM);
189         if (className != null) {
190             try {
191                 webEnvironmentClass = ClassUtils.forName(className);
192             } catch (UnknownClassException ex) {
193                 throw new ConfigurationException(
194                         "Failed to load custom WebEnvironment class [" + className + "]", ex);
195             }
196         }
197         return webEnvironmentClass;
198     }
199 
200     private WebEnvironment webEnvironmentFromServiceLoader() {
201 
202         WebEnvironment webEnvironment = null;
203         // try to load WebEnvironment as a service
204         Iterator<WebEnvironment> iterator = doLoadWebEnvironmentsFromServiceLoader();
205 
206         // Use the first one
207         if (iterator.hasNext()) {
208             webEnvironment = iterator.next();
209         }
210         // if there are others, throw an error
211         if (iterator.hasNext()) {
212             List<String> allWebEnvironments = new ArrayList<String>();
213             allWebEnvironments.add(webEnvironment.getClass().getName());
214             while (iterator.hasNext()) {
215                 allWebEnvironments.add(iterator.next().getClass().getName());
216             }
217             throw new ConfigurationException("ServiceLoader for class [" + WebEnvironment.class + "] returned more then one "
218                     + "result.  ServiceLoader must return zero or exactly one result for this class. Select one using the "
219                     + "servlet init parameter '" + ENVIRONMENT_CLASS_PARAM + "'. Found: " + allWebEnvironments);
220         }
221         return webEnvironment;
222     }
223 
224     protected Iterator<WebEnvironment> doLoadWebEnvironmentsFromServiceLoader() {
225         ServiceLoader<WebEnvironment> serviceLoader = ServiceLoader.load(WebEnvironment.class);
226 
227         return serviceLoader.iterator();
228     }
229 
230     /**
231      * Returns the default WebEnvironment class, which is unless overridden: {@link IniWebEnvironment}.
232      *
233      * @param ctx servlet context
234      * @return the default WebEnvironment class.
235      */
236     protected Class<? extends WebEnvironment> getDefaultWebEnvironmentClass(ServletContext ctx) {
237         return IniWebEnvironment.class;
238     }
239 
240     /**
241      * Return the WebEnvironment implementation class to use, based on the order of:
242      * <ul>
243      *     <li>A custom WebEnvironment class
244      *              - specified in the {@code servletContext} {@link #ENVIRONMENT_ATTRIBUTE_KEY} property</li>
245      *     <li>{@code ServiceLoader.load(WebEnvironment.class)} -
246      *     (if more then one instance is found a {@link ConfigurationException} will be thrown</li>
247      *     <li>A call to {@link #getDefaultWebEnvironmentClass(ServletContext)} (default: {@link IniWebEnvironment})</li>
248      * </ul>
249      *
250      * @param servletContext current servlet context
251      * @param servletContext the {@code servletContext} to query the {@code ENVIRONMENT_ATTRIBUTE_KEY} property from
252      * @return the WebEnvironment implementation class to use
253      * @return the {@code WebEnvironment} to be used
254      * @see #ENVIRONMENT_CLASS_PARAM
255      */
256     protected WebEnvironment determineWebEnvironment(ServletContext servletContext) {
257 
258         Class<? extends WebEnvironment> webEnvironmentClass = webEnvironmentClassFromServletContext(servletContext);
259         WebEnvironment webEnvironment = null;
260 
261         // try service loader next
262         if (webEnvironmentClass == null) {
263             webEnvironment = webEnvironmentFromServiceLoader();
264         }
265 
266         // if webEnvironment is not set, and ENVIRONMENT_CLASS_PARAM prop was not set, use the default
267         if (webEnvironmentClass == null && webEnvironment == null) {
268             webEnvironmentClass = getDefaultWebEnvironmentClass(servletContext);
269         }
270 
271         // at this point, we anything is set for the webEnvironmentClass, load it.
272         if (webEnvironmentClass != null) {
273             webEnvironment = (WebEnvironment) ClassUtils.newInstance(webEnvironmentClass);
274         }
275 
276         return webEnvironment;
277     }
278 
279     /**
280      * Instantiates a {@link WebEnvironment} based on the specified ServletContext.
281      * <p/>
282      * This implementation {@link #determineWebEnvironmentClass(javax.servlet.ServletContext) determines} a
283      * {@link WebEnvironment} implementation class to use.  That class is instantiated, configured, and returned.
284      * <p/>
285      * This allows custom {@code WebEnvironment} implementations to be specified via a ServletContext init-param if
286      * desired.  If not specified, the default {@link IniWebEnvironment} implementation will be used.
287      *
288      * @param sc current servlet context
289      * @return the constructed Shiro WebEnvironment instance
290      * @see MutableWebEnvironment
291      * @see ResourceConfigurable
292      */
293     protected WebEnvironment createEnvironment(ServletContext sc) {
294 
295         WebEnvironment webEnvironment = determineWebEnvironment(sc);
296         if (!MutableWebEnvironment.class.isInstance(webEnvironment)) {
297             throw new ConfigurationException("Custom WebEnvironment class [" + webEnvironment.getClass().getName()
298                     + "] is not of required type [" + MutableWebEnvironment.class.getName() + "]");
299         }
300 
301         String configLocations = sc.getInitParameter(CONFIG_LOCATIONS_PARAM);
302         boolean configSpecified = StringUtils.hasText(configLocations);
303 
304         if (configSpecified && !(ResourceConfigurable.class.isInstance(webEnvironment))) {
305             String msg = "WebEnvironment class [" + webEnvironment.getClass().getName() + "] does not implement the "
306                     + ResourceConfigurable.class.getName() + "interface.  This is required to accept any "
307                     + "configured " + CONFIG_LOCATIONS_PARAM + "value(s).";
308             throw new ConfigurationException(msg);
309         }
310 
311         MutableWebEnvironment environment = (MutableWebEnvironment) webEnvironment;
312 
313         environment.setServletContext(sc);
314 
315         if (configSpecified && (environment instanceof ResourceConfigurable)) {
316             ((ResourceConfigurable) environment).setConfigLocations(configLocations);
317         }
318 
319         customizeEnvironment(environment);
320 
321         LifecycleUtils.init(environment);
322 
323         return environment;
324     }
325 
326     /**
327      * Any additional customization of the Environment can be by overriding this method. For example setup shared
328      * resources, etc. By default this method does nothing.
329      *
330      * @param environment
331      */
332     protected void customizeEnvironment(WebEnvironment environment) {
333     }
334 
335     /**
336      * Destroys the {@link WebEnvironment} for the given servlet context.
337      *
338      * @param servletContext the ServletContext attributed to the WebSecurityManager
339      */
340     public void destroyEnvironment(ServletContext servletContext) {
341         servletContext.log("Cleaning up Shiro Environment");
342         try {
343             Object environment = servletContext.getAttribute(ENVIRONMENT_ATTRIBUTE_KEY);
344             if (environment instanceof WebEnvironment) {
345                 finalizeEnvironment((WebEnvironment) environment);
346             }
347             LifecycleUtils.destroy(environment);
348         } finally {
349             servletContext.removeAttribute(ENVIRONMENT_ATTRIBUTE_KEY);
350         }
351     }
352 
353     /**
354      * Any additional cleanup of the Environment can be done by overriding this method.  For example clean up shared
355      * resources, etc. By default this method does nothing.
356      *
357      * @param environment
358      * @since 1.3
359      */
360     protected void finalizeEnvironment(WebEnvironment environment) {
361     }
362 }