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     */
019    package org.apache.shiro.web.env;
020    
021    import org.apache.shiro.config.ConfigurationException;
022    import org.apache.shiro.config.ResourceConfigurable;
023    import org.apache.shiro.util.ClassUtils;
024    import org.apache.shiro.util.LifecycleUtils;
025    import org.apache.shiro.util.StringUtils;
026    import org.apache.shiro.util.UnknownClassException;
027    import org.slf4j.Logger;
028    import org.slf4j.LoggerFactory;
029    
030    import 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     */
092    public 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 [" + WebEnvironment.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        protected void customizeEnvironment(WebEnvironment environment) {
227        }
228    
229        /**
230         * Destroys the {@link WebEnvironment} for the given servlet context.
231         *
232         * @param servletContext the ServletContext attributed to the WebSecurityManager
233         */
234        public void destroyEnvironment(ServletContext servletContext) {
235            servletContext.log("Cleaning up Shiro Environment");
236            try {
237                Object environment = servletContext.getAttribute(ENVIRONMENT_ATTRIBUTE_KEY);
238                LifecycleUtils.destroy(environment);
239            } finally {
240                servletContext.removeAttribute(ENVIRONMENT_ATTRIBUTE_KEY);
241            }
242        }
243    }