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.servlet;
020
021 import org.apache.shiro.config.ConfigurationException;
022 import org.apache.shiro.config.Ini;
023 import org.apache.shiro.config.IniFactorySupport;
024 import org.apache.shiro.mgt.SecurityManager;
025 import org.apache.shiro.util.CollectionUtils;
026 import org.apache.shiro.web.config.IniFilterChainResolverFactory;
027 import org.apache.shiro.web.config.WebIniSecurityManagerFactory;
028 import org.apache.shiro.web.filter.mgt.FilterChainResolver;
029 import org.apache.shiro.web.mgt.WebSecurityManager;
030 import org.slf4j.Logger;
031 import org.slf4j.LoggerFactory;
032
033 import java.util.Map;
034
035 /**
036 * Main Servlet Filter that configures and enables all Shiro functions within a web application by using the
037 * <a href="http://en.wikipedia.org/wiki/INI_file">INI</a> configuration format.
038 * <p/>
039 * The following is a fully commented example that documents how to configure it:
040 * <pre><filter>
041 * <filter-name>ShiroFilter</filter-name>
042 * <filter-class>org.apache.shiro.web.servlet.IniShiroFilter</filter-class>
043 * <init-param><param-name>config</param-name><param-value>
044 * #
045 * #NOTE: This config looks pretty long - but its not - its only a few lines of actual config.
046 * # Everything else is just heavily commented to explain things in-depth. Feel free to delete any
047 * # comments that you don't want to read from your own configuration ;)
048 * #
049 * # Any commented values below that _don't_ start with 'example.pkg' are Shiro's defaults. If you want to change any
050 * # values on those lines, you only need to uncomment the lines you want to change.
051 * #
052 * [main]
053 * # The 'main' section defines Shiro-wide configuration.
054 * #
055 * # Each section's configuration is essentially an object graph definition in a .properties style (name/value pair)
056 * # format. The beans defined would be those that are used to construct the application's SecurityManager. It is
057 * # essentially 'poor man's' dependency injection via a .properties format.
058 * #
059 * # --- Defining Realms ---
060 * #
061 * # Any Realm defined here will automatically be injected into Shiro's default SecurityManager created at start up.
062 * # For example:
063 * #
064 * # myRealm = example.pkg.security.MyRealm
065 * #
066 * # This would instantiate the example.pkg.security.MyRealm class with a default no-arg constructor and inject it into
067 * # the SecurityManager. More than one realm can be defined if needed. You can create graphs and reference
068 * # other beans ('$' bean reference notation) while defining Realms and other objects:
069 * #
070 * # <b>connectionFactory</b> = example.pkg.ConnectionFactory
071 * # connectionFactory.driverClassName = a.jdbc.Driver
072 * # connectionFactory.username = aUsername
073 * # connectionFactory.password = aPassword
074 * # connectionFactory.minConnections = 3
075 * # connectionFactory.maxConnections = 10
076 * # ... etc...
077 * #
078 * # myJdbcRealm = example.pkg.jdbc.MyJdbcRealm
079 * # myJdbcRealm.connectionFactory = <b>$connectionFactory</b>
080 * # ... etc ...
081 * #
082 * # --- Realm Factories ---
083 * #
084 * # If the INI style isn't robust enough for your needs, you also have the option of implementing the
085 * # {@link org.apache.shiro.realm.RealmFactory org.apache.shiro.realm.RealmFactory} interface with more complex construction
086 * # logic. Then you can declare the implementation here instead. The realms it returns will be injected in to the
087 * # SecurityManager just as the individual Realms are. For example:
088 * #
089 * # aRealmFactory = example.pkg.ClassThatImplementsRealmFactory
090 * #
091 * # --- SessionManager properties ---
092 * #
093 * # Except for Realms and RealmFactories, all other objects should be defined and set on the SecurityManager directly.
094 * # The default 'securityManager' bean is an instance of {@link org.apache.shiro.web.mgt.DefaultWebSecurityManager}, so you
095 * # can set any of its corresponding properties as necessary:
096 * #
097 * # someObject = some.fully.qualified.ClassName
098 * # someObject.propertyN = foo
099 * # ...
100 * # securityManager.someObject = $someObject
101 * #
102 * # For example, if you wanted to change Shiro's default session mechanism, you can change the 'sessionMode' property.
103 * # By default, Shiro's Session infrastructure in a web environment will use the
104 * # Servlet container's HttpSession. However, if you need to share session state across client types
105 * # (e.g. Web MVC plus Java Web Start or Flash), or are doing distributed/shared Sessions for
106 * # Single Sign On, HttpSessions aren't good enough. You'll need to use Shiro's more powerful
107 * # (and client-agnostic) session management. You can enable this by uncommenting the following line
108 * # and changing 'http' to 'native'
109 * #
110 * #securityManager.{@link org.apache.shiro.web.mgt.DefaultWebSecurityManager#setSessionMode(String) sessionMode} = http
111 * #
112 * [filters]
113 * # This section defines the 'pool' of all Filters available to the url path definitions in the [urls] section below.
114 * #
115 * # The following commented values are already provided by Shiro by default and are immediately usable
116 * # in the [urls] definitions below. If you like, you may override any values by uncommenting only the lines
117 * # you need to change.
118 * #
119 * # Each Filter is configured based on its functionality and/or protocol. You should read each
120 * # Filter's JavaDoc to fully understand what each does and how it works as well as how it would
121 * # affect the user experience.
122 * #
123 * # Form-based Authentication filter:
124 * #<a name="authc"></a>authc = {@link org.apache.shiro.web.filter.authc.FormAuthenticationFilter}
125 * #authc.{@link org.apache.shiro.web.filter.authc.FormAuthenticationFilter#setLoginUrl(String) loginUrl} = /login.jsp
126 * #authc.{@link org.apache.shiro.web.filter.authc.FormAuthenticationFilter#setUsernameParam(String) usernameParam} = username
127 * #authc.{@link org.apache.shiro.web.filter.authc.FormAuthenticationFilter#setPasswordParam(String) passwordParam} = password
128 * #authc.{@link org.apache.shiro.web.filter.authc.FormAuthenticationFilter#setRememberMeParam(String) rememberMeParam} = rememberMe
129 * #authc.{@link org.apache.shiro.web.filter.authc.FormAuthenticationFilter#setSuccessUrl(String) successUrl} = /login.jsp
130 * #authc.{@link org.apache.shiro.web.filter.authc.FormAuthenticationFilter#setFailureKeyAttribute(String) failureKeyAttribute} = {@link org.apache.shiro.web.filter.authc.FormAuthenticationFilter#DEFAULT_ERROR_KEY_ATTRIBUTE_NAME}
131 * #
132 * # Http BASIC Authentication filter:
133 * #<a name="authcBasic"></a>authcBasic = {@link org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter}
134 * #authcBasic.{@link org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter#setApplicationName(String) applicationName} = application
135 * #
136 * # Roles filter: requires the requesting user to have one or more roles for the request to continue.
137 * # If they do not have the specified roles, they are redirected to the specified URL.
138 * #<a name="roles"></a>roles = {@link org.apache.shiro.web.filter.authz.RolesAuthorizationFilter}
139 * #roles.{@link org.apache.shiro.web.filter.authz.RolesAuthorizationFilter#setUnauthorizedUrl(String) unauthorizedUrl} =
140 * # (note the above url is null by default, which will cause an HTTP 403 (Access Denied) response instead
141 * # of redirecting to a page. If you want to show a 'nice page' instead, you should specify that url.
142 * #
143 * # Permissions filter: requires the requesting user to have one or more permissions for the request to
144 * # continue, and if they do not, redirects them to the specified URL.
145 * #<a name="perms"></a>perms = {@link org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter}
146 * #perms.{@link org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter#setUnauthorizedUrl(String) unauthorizedUrl} =
147 * # (note the above url is null by default, which will cause an HTTP 403 (Access Denied) response instead
148 * # of redirecting to a page. If you want to show a 'nice page' instead, you should specify that url. Many
149 * # applications like to use the same url specified in roles.unauthorizedUrl above.
150 * #
151 * #
152 * # Define your own filters here as you would any other object as described in the '[main]' section above (properties,
153 * # $references, etc). To properly handle url path matching (see the [urls] section below), your
154 * # filter should extend the {@link org.apache.shiro.web.filter.PathMatchingFilter PathMatchingFilter} abstract class.
155 * #
156 * [urls]
157 * # This section defines url path mappings. Each mapping entry must be on a single line and conform to the
158 * # following representation:
159 * #
160 * # ant_path_expression = path_specific_filter_chain_definition
161 * #
162 * # For any request that matches a specified path, the corresponding value defines a comma-delimited chain of
163 * # filters to execute for that request.
164 * #
165 * # This is incredibly powerful in that you can define arbitrary filter chains for any given request pattern
166 * # to greatly customize the security experience.
167 * #
168 * # The path_specific_filter_chain_definition must match the following format:
169 * #
170 * # filter1[optional_config1], filter2[optional_config2], ..., filterN[optional_configN]
171 * #
172 * # where 'filterN' is the name of an filter defined above in the [filters] section and
173 * # '[optional_configN]' is an optional bracketed string that has meaning for that particular filter for
174 * # _that particular path_. If the filter does not need specific config for that url path, you may
175 * # discard the brackets so filterN[] just becomes filterN.
176 * #
177 * # And because filter tokens define chains, order matters! Define the tokens for each path pattern
178 * # in the order you want them to filter (comma-delimited).
179 * #
180 * # Finally, each filter is free to handle the response however it wants if its necessary
181 * # conditions are not met (redirect, HTTP error code, direct rendering, etc). Otherwise, it is expected to allow
182 * # the request to continue through the chain on to the final destination view.
183 * #
184 * # Examples:
185 * #
186 * # To illustrate chain configuration, look at the /account/** mapping below. This says
187 * # "apply the above 'authcBasic' filter to any request matching the '/account/**' pattern". Since the
188 * # 'authcBasic' filter does not need any path-specific config, it doesn't have any config brackets [].
189 * #
190 * # The /remoting/** definition on the other hand uses the 'roles' and 'perms' filters which do use
191 * # bracket notation. That definition says:
192 * #
193 * # "To access /remoting/** urls, ensure that the user is first authenticated ('authcBasic'), then ensure that user
194 * # has the 'b2bClient' role, and then finally ensure that they have the 'remote:invoke:lan,wan' permission."
195 * #
196 * # (Note that because elements within brackets [ ] are comma-delimited themselves, we needed to quote any config
197 * # value which may require a comma. If we didn't do that, the permission filter below would interpret
198 * # the text between the brackets as two permissions: 'remote:invoke:lan' and 'wan' instead of the
199 * # single desired 'remote:invoke:lan,wan' token. So, you can use quotes wherever you need to escape internal
200 * # commas.)
201 * #
202 * /account/** = <a href="#authcBasic">authcBasic</a>
203 * /remoting/** = <a href="#authcBasic">authcBasic</a>, <a href="#roles">roles</a>[b2bClient], <a href="#perms">perms</a>["remote:invoke:lan,wan"]
204 * </param-value></init-param>
205 * </filter>
206 * <p/>
207 * <filter-mapping>
208 * <filter-name>ShiroFilter</filter-name>
209 * <url-pattern>/*</url-pattern>
210 * </filter-mapping></pre>
211 *
212 * @since 1.0
213 */
214 public class IniShiroFilter extends AbstractShiroFilter {
215
216 public static final String CONFIG_INIT_PARAM_NAME = "config";
217 public static final String CONFIG_PATH_INIT_PARAM_NAME = "configPath";
218
219 private static final Logger log = LoggerFactory.getLogger(IniShiroFilter.class);
220
221 private String config;
222 private String configPath;
223
224 public IniShiroFilter() {
225 }
226
227 /**
228 * Returns the actual INI configuration text to use to build the {@link SecurityManager} and
229 * {@link FilterChainResolver} used by the web application or {@code null} if the
230 * {@link #getConfigPath() configPath} should be used to load a fallback INI source.
231 * <p/>
232 * This value is {@code null} by default, but it will be automatically set to the value of the
233 * '{@code config}' {@code init-param} if it exists in the {@code FilterConfig} provided by the servlet
234 * container at startup.
235 *
236 * @return the actual INI configuration text to use to build the {@link SecurityManager} and
237 * {@link FilterChainResolver} used by the web application or {@code null} if the
238 * {@link #getConfigPath() configPath} should be used to load a fallback INI source.
239 */
240 public String getConfig() {
241 return this.config;
242 }
243
244 /**
245 * Sets the actual INI configuration text to use to build the {@link SecurityManager} and
246 * {@link FilterChainResolver} used by the web application. If this value is {@code null}, the
247 * {@link #getConfigPath() configPath} will be checked to see if a .ini file should be loaded instead.
248 * <p/>
249 * This value is {@code null} by default, but it will be automatically set to the value of the
250 * '{@code config}' {@code init-param} if it exists in the {@code FilterConfig} provided by the servlet
251 * container at startup.
252 *
253 * @param config the actual INI configuration text to use to build the {@link SecurityManager} and
254 * {@link FilterChainResolver} used by the web application.
255 */
256 public void setConfig(String config) {
257 this.config = config;
258 }
259
260 /**
261 * Returns the config path to be used to load a .ini file for configuration if a configuration is
262 * not specified via the {@link #getConfig() config} attribute.
263 * <p/>
264 * This value is {@code null} by default, but it will be automatically set to the value of the
265 * '{@code configPath}' {@code init-param} if it exists in the {@code FilterConfig} provided by the servlet
266 * container at startup.
267 *
268 * @return the config path to be used to load a .ini file for configuration if a configuration is
269 * not specified via the {@link #getConfig() config} attribute.
270 */
271 public String getConfigPath() {
272 return configPath;
273 }
274
275 /**
276 * Sets the config path to be used to load a .ini file for configuration if a configuration is
277 * not specified via the {@link #getConfig() config} attribute.
278 * <p/>
279 * This value is {@code null} by default, but it will be automatically set to the value of the
280 * '{@code configPath}' {@code init-param} if it exists in the {@code FilterConfig} provided by the servlet
281 * container at startup.
282 *
283 * @param configPath the config path to be used to load a .ini file for configuration if a configuration is
284 * not specified via the {@link #getConfig() config} attribute.
285 */
286 public void setConfigPath(String configPath) {
287 this.configPath = configPath;
288 }
289
290 public void init() throws Exception {
291 applyInitParams();
292 configure();
293 }
294
295 protected void applyInitParams() throws Exception {
296 String config = getInitParam(CONFIG_INIT_PARAM_NAME);
297 if (config != null) {
298 setConfig(config);
299 }
300 String configPath = getInitParam(CONFIG_PATH_INIT_PARAM_NAME);
301 if (configPath != null) {
302 setConfigPath(configPath);
303 }
304 }
305
306 protected void configure() throws Exception {
307 Ini ini = loadIniFromConfig();
308
309 if (CollectionUtils.isEmpty(ini)) {
310 log.info("Null or empty configuration specified via 'config' init-param. " +
311 "Checking path-based configuration.");
312 ini = loadIniFromPath();
313 }
314 if (CollectionUtils.isEmpty(ini)) {
315 log.info("Null or empty configuration specified via '" + CONFIG_INIT_PARAM_NAME + "' or '" +
316 CONFIG_PATH_INIT_PARAM_NAME + "' filter parameters. Trying the default " +
317 IniFactorySupport.DEFAULT_INI_RESOURCE_PATH + " file.");
318 ini = IniFactorySupport.loadDefaultClassPathIni();
319 }
320
321 Map<String, ?> objects = applySecurityManager(ini);
322 applyFilterChainResolver(ini, objects);
323 }
324
325 protected Ini loadIniFromConfig() {
326 Ini ini = null;
327 String config = getConfig();
328 if (config != null) {
329 ini = convertConfigToIni(config);
330 }
331 return ini;
332 }
333
334 protected Ini loadIniFromPath() {
335 Ini ini = null;
336 String path = getConfigPath();
337 if (path != null) {
338 ini = convertPathToIni(path);
339 }
340 return ini;
341 }
342
343 protected Map<String, ?> applySecurityManager(Ini ini) {
344 WebIniSecurityManagerFactory factory;
345 if (CollectionUtils.isEmpty(ini)) {
346 factory = new WebIniSecurityManagerFactory();
347 } else {
348 factory = new WebIniSecurityManagerFactory(ini);
349 }
350
351 // Create the security manager and check that it implements WebSecurityManager.
352 // Otherwise, it can't be used with the filter.
353 SecurityManager securityManager = factory.getInstance();
354 if (!(securityManager instanceof WebSecurityManager)) {
355 String msg = "The configured security manager is not an instance of WebSecurityManager, so " +
356 "it can not be used with the Shiro servlet filter.";
357 throw new ConfigurationException(msg);
358 }
359
360 setSecurityManager((WebSecurityManager) securityManager);
361
362 return factory.getBeans();
363 }
364
365 protected void applyFilterChainResolver(Ini ini, Map<String, ?> defaults) {
366 if (ini == null || ini.isEmpty()) {
367 //nothing to use to create the resolver, just return
368 //(the AbstractShiroFilter allows a null resolver, in which case the original FilterChain is
369 // always used).
370 return;
371 }
372
373 //only create a resolver if the 'filters' or 'urls' sections are defined:
374 Ini.Section urls = ini.getSection(IniFilterChainResolverFactory.URLS);
375 Ini.Section filters = ini.getSection(IniFilterChainResolverFactory.FILTERS);
376 if ((urls != null && !urls.isEmpty()) || (filters != null && !filters.isEmpty())) {
377 //either the urls section or the filters section was defined. Go ahead and create the resolver
378 //and set it:
379 IniFilterChainResolverFactory filterChainResolverFactory = new IniFilterChainResolverFactory(ini, defaults);
380 filterChainResolverFactory.setFilterConfig(getFilterConfig());
381 FilterChainResolver resolver = filterChainResolverFactory.getInstance();
382 setFilterChainResolver(resolver);
383 }
384 }
385
386 protected Ini convertConfigToIni(String config) {
387 Ini ini = new Ini();
388 ini.load(config);
389 return ini;
390 }
391
392 protected Ini convertPathToIni(String path) {
393 Ini ini = new Ini();
394 ini.loadFromPath(path);
395 return ini;
396 }
397 }