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.util.ClassUtils;
024import org.apache.shiro.util.LifecycleUtils;
025import org.apache.shiro.util.StringUtils;
026import org.apache.shiro.util.UnknownClassException;
027import org.slf4j.Logger;
028import org.slf4j.LoggerFactory;
029
030import javax.servlet.ServletContext;
031
032/**
033 * An {@code EnvironmentLoader} is responsible for loading a web application's Shiro {@link WebEnvironment}
034 * (which includes the web app's {@link org.apache.shiro.web.mgt.WebSecurityManager WebSecurityManager}) into the
035 * {@code ServletContext} at application startup.
036 * <p/>
037 * In Shiro 1.1 and earlier, the Shiro ServletFilter was responsible for creating the {@code WebSecurityManager} and
038 * any additional objects (security filters, etc).  However, any component not filtered by the Shiro Filter (such
039 * as other context listeners) was not able to easily acquire the these objects to perform security operations.
040 * <p/>
041 * Due to this, in Shiro 1.2 and later, this {@code EnvironmentLoader} (or more likely, the
042 * {@link EnvironmentLoaderListener} subclass) is the preferred mechanism to initialize
043 * a Shiro environment.  The Shiro Filter, while still required for request filtering, will not perform this
044 * initialization at startup if the {@code EnvironmentLoader} (or listener) runs first.
045 * <h2>Usage</h2>
046 * This implementation will look for two servlet context {@code context-param}s in {@code web.xml}:
047 * {@code shiroEnvironmentClass} and {@code shiroConfigLocations} that customize how the {@code WebEnvironment} instance
048 * will be initialized.
049 * <h3>shiroEnvironmentClass</h3>
050 * The {@code shiroEnvironmentClass} {@code context-param}, if it exists, allows you to specify the
051 * fully-qualified implementation class name of the {@link WebEnvironment} to instantiate.  For example:
052 * <pre>
053 * &lt;context-param&gt;
054 *     &lt;param-name&gt;shiroEnvironmentClass&lt;/param-name&gt;
055 *     &lt;param-value&gt;com.foo.bar.shiro.MyWebEnvironment&lt;/param-value&gt;
056 * &lt;/context-param&gt;
057 * </pre>
058 * If not specified, the default value is the {@link IniWebEnvironment} class, which assumes Shiro's default
059 * <a href="http://shiro.apache.org/configuration.html">INI configuration format</a>
060 * <h3>shiroConfigLocations</h3>
061 * The {@code shiroConfigLocations} {@code context-param}, if it exists, allows you to specify the config location(s)
062 * (resource path(s)) that will be relayed to the instantiated {@link WebEnvironment}.  For example:
063 * <pre>
064 * &lt;context-param&gt;
065 *     &lt;param-name&gt;shiroConfigLocations&lt;/param-name&gt;
066 *     &lt;param-value&gt;/WEB-INF/someLocation/shiro.ini&lt;/param-value&gt;
067 * &lt;/context-param&gt;
068 * </pre>
069 * The {@code WebEnvironment} implementation must implement the {@link ResourceConfigurable} interface if it is to
070 * acquire the {@code shiroConfigLocations} value.
071 * <p/>
072 * If this {@code context-param} is not specified, the {@code WebEnvironment} instance determines default resource
073 * lookup behavior.  For example, the {@link IniWebEnvironment} will check the following two locations for INI config
074 * by default (in order):
075 * <ol>
076 * <li>/WEB-INF/shiro.ini</li>
077 * <li>classpath:shiro.ini</li>
078 * </ol>
079 * <h2>Web Security Enforcement</h2>
080 * Using this loader will only initialize Shiro's environment in a web application - it will not filter web requests or
081 * perform web-specific security operations.  To do this, you must ensure that you have also configured the
082 * {@link org.apache.shiro.web.servlet.ShiroFilter ShiroFilter} in {@code web.xml}.
083 * <p/>
084 * Finally, it should be noted that this implementation was based on ideas in Spring 3's
085 * {@code org.springframework.web.context.ContextLoader} implementation - no need to reinvent the wheel for this common
086 * behavior.
087 *
088 * @see EnvironmentLoaderListener
089 * @see org.apache.shiro.web.servlet.ShiroFilter ShiroFilter
090 * @since 1.2
091 */
092public class EnvironmentLoader {
093
094    /**
095     * Servlet Context config param for specifying the {@link WebEnvironment} implementation class to use:
096     * {@code shiroEnvironmentClass}
097     */
098    public static final String ENVIRONMENT_CLASS_PARAM = "shiroEnvironmentClass";
099
100    /**
101     * Servlet Context config param for the resource path to use for configuring the {@link WebEnvironment} instance:
102     * {@code shiroConfigLocations}
103     */
104    public static final String CONFIG_LOCATIONS_PARAM = "shiroConfigLocations";
105
106    public static final String ENVIRONMENT_ATTRIBUTE_KEY = EnvironmentLoader.class.getName() + ".ENVIRONMENT_ATTRIBUTE_KEY";
107
108    private static final Logger log = LoggerFactory.getLogger(EnvironmentLoader.class);
109
110    /**
111     * Initializes Shiro's {@link WebEnvironment} instance for the specified {@code ServletContext} based on the
112     * {@link #CONFIG_LOCATIONS_PARAM} value.
113     *
114     * @param servletContext current servlet context
115     * @return the new Shiro {@code WebEnvironment} instance.
116     * @throws IllegalStateException if an existing WebEnvironment has already been initialized and associated with
117     *                               the specified {@code ServletContext}.
118     */
119    public WebEnvironment initEnvironment(ServletContext servletContext) throws IllegalStateException {
120
121        if (servletContext.getAttribute(ENVIRONMENT_ATTRIBUTE_KEY) != null) {
122            String msg = "There is already a Shiro environment associated with the current ServletContext.  " +
123                    "Check if you have multiple EnvironmentLoader* definitions in your web.xml!";
124            throw new IllegalStateException(msg);
125        }
126
127        servletContext.log("Initializing Shiro environment");
128        log.info("Starting Shiro environment initialization.");
129
130        long startTime = System.currentTimeMillis();
131
132        try {
133            WebEnvironment environment = createEnvironment(servletContext);
134            servletContext.setAttribute(ENVIRONMENT_ATTRIBUTE_KEY, environment);
135
136            log.debug("Published WebEnvironment as ServletContext attribute with name [{}]",
137                    ENVIRONMENT_ATTRIBUTE_KEY);
138
139            if (log.isInfoEnabled()) {
140                long elapsed = System.currentTimeMillis() - startTime;
141                log.info("Shiro environment initialized in {} ms.", elapsed);
142            }
143
144            return environment;
145        } catch (RuntimeException ex) {
146            log.error("Shiro environment initialization failed", ex);
147            servletContext.setAttribute(ENVIRONMENT_ATTRIBUTE_KEY, ex);
148            throw ex;
149        } catch (Error err) {
150            log.error("Shiro environment initialization failed", err);
151            servletContext.setAttribute(ENVIRONMENT_ATTRIBUTE_KEY, err);
152            throw err;
153        }
154    }
155
156    /**
157     * Return the WebEnvironment implementation class to use, either the default
158     * {@link IniWebEnvironment} or a custom class if specified.
159     *
160     * @param servletContext current servlet context
161     * @return the WebEnvironment implementation class to use
162     * @see #ENVIRONMENT_CLASS_PARAM
163     * @see IniWebEnvironment
164     */
165    protected Class<?> determineWebEnvironmentClass(ServletContext servletContext) {
166        String className = servletContext.getInitParameter(ENVIRONMENT_CLASS_PARAM);
167        if (className != null) {
168            try {
169                return ClassUtils.forName(className);
170            } catch (UnknownClassException ex) {
171                throw new ConfigurationException(
172                        "Failed to load custom WebEnvironment class [" + className + "]", ex);
173            }
174        } else {
175            return IniWebEnvironment.class;
176        }
177    }
178
179    /**
180     * Instantiates a {@link WebEnvironment} based on the specified ServletContext.
181     * <p/>
182     * This implementation {@link #determineWebEnvironmentClass(javax.servlet.ServletContext) determines} a
183     * {@link WebEnvironment} implementation class to use.  That class is instantiated, configured, and returned.
184     * <p/>
185     * This allows custom {@code WebEnvironment} implementations to be specified via a ServletContext init-param if
186     * desired.  If not specified, the default {@link IniWebEnvironment} implementation will be used.
187     *
188     * @param sc current servlet context
189     * @return the constructed Shiro WebEnvironment instance
190     * @see MutableWebEnvironment
191     * @see ResourceConfigurable
192     */
193    protected WebEnvironment createEnvironment(ServletContext sc) {
194
195        Class<?> clazz = determineWebEnvironmentClass(sc);
196        if (!MutableWebEnvironment.class.isAssignableFrom(clazz)) {
197            throw new ConfigurationException("Custom WebEnvironment class [" + clazz.getName() +
198                    "] is not of required type [" + MutableWebEnvironment.class.getName() + "]");
199        }
200
201        String configLocations = sc.getInitParameter(CONFIG_LOCATIONS_PARAM);
202        boolean configSpecified = StringUtils.hasText(configLocations);
203
204        if (configSpecified && !(ResourceConfigurable.class.isAssignableFrom(clazz))) {
205            String msg = "WebEnvironment class [" + clazz.getName() + "] does not implement the " +
206                    ResourceConfigurable.class.getName() + "interface.  This is required to accept any " +
207                    "configured " + CONFIG_LOCATIONS_PARAM + "value(s).";
208            throw new ConfigurationException(msg);
209        }
210
211        MutableWebEnvironment environment = (MutableWebEnvironment) ClassUtils.newInstance(clazz);
212
213        environment.setServletContext(sc);
214
215        if (configSpecified && (environment instanceof ResourceConfigurable)) {
216            ((ResourceConfigurable) environment).setConfigLocations(configLocations);
217        }
218
219        customizeEnvironment(environment);
220
221        LifecycleUtils.init(environment);
222
223        return environment;
224    }
225
226    /**
227     * Any additional customization of the Environment can be by overriding this method. For example setup shared
228     * resources, etc. By default this method does nothing.
229     * @param environment
230     */
231    protected void customizeEnvironment(WebEnvironment environment) {
232    }
233
234    /**
235     * Destroys the {@link WebEnvironment} for the given servlet context.
236     *
237     * @param servletContext the ServletContext attributed to the WebSecurityManager
238     */
239    public void destroyEnvironment(ServletContext servletContext) {
240        servletContext.log("Cleaning up Shiro Environment");
241        try {
242            Object environment = servletContext.getAttribute(ENVIRONMENT_ATTRIBUTE_KEY);
243            if (environment instanceof WebEnvironment) {
244                finalizeEnvironment((WebEnvironment) environment);
245            }
246            LifecycleUtils.destroy(environment);
247        } finally {
248            servletContext.removeAttribute(ENVIRONMENT_ATTRIBUTE_KEY);
249        }
250    }
251
252    /**
253     * Any additional cleanup of the Environment can be done by overriding this method.  For example clean up shared
254     * resources, etc. By default this method does nothing.
255     * @param environment
256     * @since 1.3
257     */
258    protected void finalizeEnvironment(WebEnvironment environment) {
259    }
260}