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 * <context-param>
59 * <param-name>shiroEnvironmentClass</param-name>
60 * <param-value>com.foo.bar.shiro.MyWebEnvironment</param-value>
61 * </context-param>
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 * <context-param>
70 * <param-name>shiroConfigLocations</param-name>
71 * <param-value>/WEB-INF/someLocation/shiro.ini</param-value>
72 * </context-param>
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 }