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>&lt;filter&gt;
041     * &lt;filter-name&gt;ShiroFilter&lt;/filter-name&gt;
042     * &lt;filter-class&gt;org.apache.shiro.web.servlet.IniShiroFilter&lt;/filter-class&gt;
043     * &lt;init-param&gt;&lt;param-name&gt;config&lt;/param-name&gt;&lt;param-value&gt;
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     * # &quot;apply the above 'authcBasic' filter to any request matching the '/account/**' pattern&quot;.  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     * # &quot;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.&quot;
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     * &lt;/param-value&gt;&lt;/init-param&gt;
205     * &lt;/filter&gt;
206     * <p/>
207     * &lt;filter-mapping&gt;
208     *     &lt;filter-name&gt;ShiroFilter&lt;/filter-name&gt;
209     *     &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
210     * &lt;/filter-mapping&gt;</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    }