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.ResourceConfigurable;
023import org.apache.shiro.lang.util.ClassUtils;
024import org.apache.shiro.lang.util.LifecycleUtils;
025import org.apache.shiro.lang.util.StringUtils;
026import org.apache.shiro.lang.util.UnknownClassException;
027import org.slf4j.Logger;
028import org.slf4j.LoggerFactory;
029
030import javax.servlet.ServletContext;
031import java.util.ArrayList;
032import java.util.Iterator;
033import java.util.List;
034import java.util.ServiceLoader;
035
036
037/**
038 * An {@code EnvironmentLoader} is responsible for loading a web application's Shiro {@link WebEnvironment}
039 * (which includes the web app's {@link org.apache.shiro.web.mgt.WebSecurityManager WebSecurityManager}) into the
040 * {@code ServletContext} at application startup.
041 * <p/>
042 * In Shiro 1.1 and earlier, the Shiro ServletFilter was responsible for creating the {@code WebSecurityManager} and
043 * any additional objects (security filters, etc.).  However, any component not filtered by the Shiro Filter (such
044 * as other context listeners) was not able to easily acquire the these objects to perform security operations.
045 * <p/>
046 * Due to this, in Shiro 1.2 and later, this {@code EnvironmentLoader} (or more likely, the
047 * {@link EnvironmentLoaderListener} subclass) is the preferred mechanism to initialize
048 * a Shiro environment.  The Shiro Filter, while still required for request filtering, will not perform this
049 * initialization at startup if the {@code EnvironmentLoader} (or listener) runs first.
050 * <h2>Usage</h2>
051 * This implementation will look for two servlet context {@code context-param}s in {@code web.xml}:
052 * {@code shiroEnvironmentClass} and {@code shiroConfigLocations} that customize how the {@code WebEnvironment} instance
053 * will be initialized.
054 * <h3>shiroEnvironmentClass</h3>
055 * The {@code shiroEnvironmentClass} {@code context-param}, if it exists, allows you to specify the
056 * fully-qualified implementation class name of the {@link WebEnvironment} to instantiate.  For example:
057 * <pre>
058 * &lt;context-param&gt;
059 *     &lt;param-name&gt;shiroEnvironmentClass&lt;/param-name&gt;
060 *     &lt;param-value&gt;com.foo.bar.shiro.MyWebEnvironment&lt;/param-value&gt;
061 * &lt;/context-param&gt;
062 * </pre>
063 * If not specified, the default value is the {@link IniWebEnvironment} class, which assumes Shiro's default
064 * <a href="http://shiro.apache.org/configuration.html">INI configuration format</a>
065 * <h3>shiroConfigLocations</h3>
066 * The {@code shiroConfigLocations} {@code context-param}, if it exists, allows you to specify the config location(s)
067 * (resource path(s)) that will be relayed to the instantiated {@link WebEnvironment}.  For example:
068 * <pre>
069 * &lt;context-param&gt;
070 *     &lt;param-name&gt;shiroConfigLocations&lt;/param-name&gt;
071 *     &lt;param-value&gt;/WEB-INF/someLocation/shiro.ini&lt;/param-value&gt;
072 * &lt;/context-param&gt;
073 * </pre>
074 * The {@code WebEnvironment} implementation must implement the {@link ResourceConfigurable} interface if it is to
075 * acquire the {@code shiroConfigLocations} value.
076 * <p/>
077 * If this {@code context-param} is not specified, the {@code WebEnvironment} instance determines default resource
078 * lookup behavior.  For example, the {@link IniWebEnvironment} will check the following two locations for INI config
079 * by default (in order):
080 * <ol>
081 * <li>/WEB-INF/shiro.ini</li>
082 * <li>classpath:shiro.ini</li>
083 * </ol>
084 * <h2>Web Security Enforcement</h2>
085 * Using this loader will only initialize Shiro's environment in a web application - it will not filter web requests or
086 * perform web-specific security operations.  To do this, you must ensure that you have also configured the
087 * {@link org.apache.shiro.web.servlet.ShiroFilter ShiroFilter} in {@code web.xml}.
088 * <p/>
089 * Finally, it should be noted that this implementation was based on ideas in Spring 3's
090 * {@code org.springframework.web.context.ContextLoader} implementation - no need to reinvent the wheel for this common
091 * behavior.
092 *
093 * @see EnvironmentLoaderListener
094 * @see org.apache.shiro.web.servlet.ShiroFilter ShiroFilter
095 * @since 1.2
096 */
097public class EnvironmentLoader {
098
099    /**
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}