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.guice.web;
020
021import java.util.*;
022
023import javax.servlet.Filter;
024import javax.servlet.ServletContext;
025
026import org.apache.shiro.config.ConfigurationException;
027import org.apache.shiro.env.Environment;
028import org.apache.shiro.guice.ShiroModule;
029import org.apache.shiro.mgt.SecurityManager;
030import org.apache.shiro.session.mgt.SessionManager;
031import org.apache.shiro.util.StringUtils;
032import org.apache.shiro.web.env.WebEnvironment;
033import org.apache.shiro.web.filter.PathMatchingFilter;
034import org.apache.shiro.web.filter.authc.AnonymousFilter;
035import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
036import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
037import org.apache.shiro.web.filter.authc.LogoutFilter;
038import org.apache.shiro.web.filter.authc.UserFilter;
039import org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter;
040import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter;
041import org.apache.shiro.web.filter.authz.PortFilter;
042import org.apache.shiro.web.filter.authz.RolesAuthorizationFilter;
043import org.apache.shiro.web.filter.authz.SslFilter;
044import org.apache.shiro.web.filter.mgt.FilterChainResolver;
045import org.apache.shiro.web.filter.session.NoSessionCreationFilter;
046import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
047import org.apache.shiro.web.mgt.WebSecurityManager;
048import org.apache.shiro.web.session.mgt.ServletContainerSessionManager;
049
050import com.google.inject.Binder;
051import com.google.inject.Key;
052import com.google.inject.TypeLiteral;
053import com.google.inject.binder.AnnotatedBindingBuilder;
054import com.google.inject.name.Names;
055import com.google.inject.servlet.ServletModule;
056
057/**
058 * Sets up Shiro lifecycles within Guice, enables the injecting of Shiro objects, and binds a default
059 * {@link org.apache.shiro.web.mgt.WebSecurityManager}, {@link org.apache.shiro.mgt.SecurityManager} and {@link org.apache.shiro.session.mgt.SessionManager}.  At least one realm must be added by
060 * using {@link #bindRealm() bindRealm}.
061 * <p/>
062 * Also provides for the configuring of filter chains and binds a {@link org.apache.shiro.web.filter.mgt.FilterChainResolver} with that information.
063 */
064public abstract class ShiroWebModule extends ShiroModule {
065    @SuppressWarnings({"UnusedDeclaration"})
066    public static final Key<AnonymousFilter> ANON = Key.get(AnonymousFilter.class);
067    @SuppressWarnings({"UnusedDeclaration"})
068    public static final Key<FormAuthenticationFilter> AUTHC = Key.get(FormAuthenticationFilter.class);
069    @SuppressWarnings({"UnusedDeclaration"})
070    public static final Key<BasicHttpAuthenticationFilter> AUTHC_BASIC = Key.get(BasicHttpAuthenticationFilter.class);
071    @SuppressWarnings({"UnusedDeclaration"})
072    public static final Key<NoSessionCreationFilter> NO_SESSION_CREATION = Key.get(NoSessionCreationFilter.class);
073    @SuppressWarnings({"UnusedDeclaration"})
074    public static final Key<LogoutFilter> LOGOUT = Key.get(LogoutFilter.class);
075    @SuppressWarnings({"UnusedDeclaration"})
076    public static final Key<PermissionsAuthorizationFilter> PERMS = Key.get(PermissionsAuthorizationFilter.class);
077    @SuppressWarnings({"UnusedDeclaration"})
078    public static final Key<PortFilter> PORT = Key.get(PortFilter.class);
079    @SuppressWarnings({"UnusedDeclaration"})
080    public static final Key<HttpMethodPermissionFilter> REST = Key.get(HttpMethodPermissionFilter.class);
081    @SuppressWarnings({"UnusedDeclaration"})
082    public static final Key<RolesAuthorizationFilter> ROLES = Key.get(RolesAuthorizationFilter.class);
083    @SuppressWarnings({"UnusedDeclaration"})
084    public static final Key<SslFilter> SSL = Key.get(SslFilter.class);
085    @SuppressWarnings({"UnusedDeclaration"})
086    public static final Key<UserFilter> USER = Key.get(UserFilter.class);
087
088
089    static final String NAME = "SHIRO";
090
091    /**
092     * We use a LinkedHashMap here to ensure that iterator order is the same as add order.  This is important, as the
093     * FilterChainResolver uses iterator order when searching for a matching chain.
094     */
095    private final Map<String, FilterConfig<? extends Filter>[]> filterChains = new LinkedHashMap<String, FilterConfig<? extends Filter>[]>();
096    private final ServletContext servletContext;
097
098    public ShiroWebModule(ServletContext servletContext) {
099        this.servletContext = servletContext;
100    }
101
102    public static void bindGuiceFilter(Binder binder) {
103        binder.install(guiceFilterModule());
104    }
105
106    @SuppressWarnings({"UnusedDeclaration"})
107    public static void bindGuiceFilter(final String pattern, Binder binder) {
108        binder.install(guiceFilterModule(pattern));
109    }
110
111    public static ServletModule guiceFilterModule() {
112        return guiceFilterModule("/*");
113    }
114
115    public static ServletModule guiceFilterModule(final String pattern) {
116        return new ServletModule() {
117            @Override
118            protected void configureServlets() {
119                filter(pattern).through(GuiceShiroFilter.class);
120            }
121        };
122    }
123
124    @Override
125    protected final void configureShiro() {
126        bindBeanType(TypeLiteral.get(ServletContext.class), Key.get(ServletContext.class, Names.named(NAME)));
127        bind(Key.get(ServletContext.class, Names.named(NAME))).toInstance(this.servletContext);
128        bindWebSecurityManager(bind(WebSecurityManager.class));
129        bindWebEnvironment(bind(WebEnvironment.class));
130        bind(GuiceShiroFilter.class).asEagerSingleton();
131        expose(GuiceShiroFilter.class);
132
133        this.configureShiroWeb();
134
135        bind(FilterChainResolver.class).toProvider(new FilterChainResolverProvider(setupFilterChainConfigs()));
136    }
137
138    private Map<String, Key<? extends Filter>[]> setupFilterChainConfigs() {
139
140        // loop through and build a map of Filter Key -> Map<Path, Config>
141        Map<Key<? extends Filter>, Map<String, String>> filterToPathToConfig = new HashMap<Key<? extends Filter>, Map<String, String>>();
142
143        // At the same time build a map to return with Path -> Key[]
144        Map<String, Key<? extends Filter>[]> resultConfigMap = new LinkedHashMap<String, Key<? extends Filter>[]>();
145
146        for (Map.Entry<String, FilterConfig<? extends Filter>[]> filterChain : filterChains.entrySet()) {
147
148            String path = filterChain.getKey();
149
150            // collect the keys used for this path
151            List<Key<? extends Filter>> keysForPath = new ArrayList<Key<? extends Filter>>();
152
153            for (int i = 0; i < filterChain.getValue().length; i++) {
154                FilterConfig<? extends Filter> filterConfig = filterChain.getValue()[i];
155
156                Key<? extends Filter> key = filterConfig.getKey();
157                String config = filterConfig.getConfigValue();
158
159                // initialize key in filterToPathToConfig, if it doesn't exist
160                if (filterToPathToConfig.get(key) == null) {
161                        // Fix for SHIRO-621: REST filter bypassing matched path
162                    filterToPathToConfig.put((key), new LinkedHashMap<String, String>());
163                }
164                // now set the value
165                filterToPathToConfig.get(key).put(path, config);
166
167                // Config error if someone configured a non PathMatchingFilter with a config value
168                if (StringUtils.hasText(config) && !PathMatchingFilter.class.isAssignableFrom(key.getTypeLiteral().getRawType())) {
169                    throw new ConfigurationException("Config information requires a PathMatchingFilter - can't apply to " + key.getTypeLiteral().getRawType());
170                }
171
172                // store the key in keysForPath
173                keysForPath.add(key);
174            }
175
176            // map the current path to all of its Keys
177            resultConfigMap.put(path, keysForPath.toArray(new Key[keysForPath.size()]));
178        }
179
180        // now we find only the PathMatchingFilter and configure bindings
181        // non PathMatchingFilter, can be loaded with the default provider via the class name
182        for (Key<? extends Filter> key : filterToPathToConfig.keySet()) {
183            if (PathMatchingFilter.class.isAssignableFrom(key.getTypeLiteral().getRawType())) {
184                bindPathMatchingFilter(castToPathMatching(key), filterToPathToConfig.get(key));
185            }
186            else {
187                bind(key);
188            }
189        }
190
191        return resultConfigMap;
192    }
193
194
195    private <T extends PathMatchingFilter> void bindPathMatchingFilter(Key<T> filterKey, Map<String, String> configs) {
196        bind(filterKey).toProvider(new PathMatchingFilterProvider<T>(filterKey, configs)).asEagerSingleton();
197    }
198
199    @SuppressWarnings({"unchecked"})
200    private Key<? extends PathMatchingFilter> castToPathMatching(Key<? extends Filter> key) {
201        return (Key<? extends PathMatchingFilter>) key;
202    }
203
204    protected abstract void configureShiroWeb();
205
206    @SuppressWarnings({"unchecked"})
207    @Override
208    protected final void bindSecurityManager(AnnotatedBindingBuilder<? super SecurityManager> bind) {
209        bind.to(WebSecurityManager.class); // SHIRO-435
210    }
211
212    /**
213     * Binds the security manager.  Override this method in order to provide your own security manager binding.
214     * <p/>
215     * By default, a {@link org.apache.shiro.web.mgt.DefaultWebSecurityManager} is bound as an eager singleton.
216     *
217     * @param bind
218     */
219    protected void bindWebSecurityManager(AnnotatedBindingBuilder<? super WebSecurityManager> bind) {
220        try {
221            bind.toConstructor(DefaultWebSecurityManager.class.getConstructor(Collection.class)).asEagerSingleton();
222        } catch (NoSuchMethodException e) {
223            throw new ConfigurationException("This really shouldn't happen.  Either something has changed in Shiro, or there's a bug in ShiroModule.", e);
224        }
225    }
226
227    /**
228     * Binds the session manager.  Override this method in order to provide your own session manager binding.
229     * <p/>
230     * By default, a {@link org.apache.shiro.web.session.mgt.DefaultWebSessionManager} is bound as an eager singleton.
231     *
232     * @param bind
233     */
234    @Override
235    protected void bindSessionManager(AnnotatedBindingBuilder<SessionManager> bind) {
236        bind.to(ServletContainerSessionManager.class).asEagerSingleton();
237    }
238
239    @Override
240    protected final void bindEnvironment(AnnotatedBindingBuilder<Environment> bind) {
241        bind.to(WebEnvironment.class); // SHIRO-435
242    }
243
244    protected void bindWebEnvironment(AnnotatedBindingBuilder<? super WebEnvironment> bind) {
245        bind.to(WebGuiceEnvironment.class).asEagerSingleton();
246    }
247
248    protected final void addFilterChain(String pattern, Key<? extends Filter> key) {
249        // check for legacy API
250        if (key instanceof FilterConfigKey) {
251            addLegacyFilterChain(pattern, (FilterConfigKey) key);
252        }
253        else {
254            addFilterChain(pattern, new FilterConfig<Filter>((Key<Filter>) key, ""));
255        }
256    }
257
258    /**
259     * Maps 'n' number of <code>filterConfig</code>s to a specific path pattern.<BR/>
260     * For example, a path of '/my_private_resource/**' to 'filterConfig(AUTHC)' would require
261     * any resource under the path '/my_private_resource' would be processed through the {@link FormAuthenticationFilter}.
262     *
263     * @param pattern URL patter to be mapped to a FilterConfig, e.g. '/my_private-path/**'
264     * @param filterConfigs FilterConfiguration representing the Filter and config to be used when processing resources on <code>pattern</code>.
265     * @since 1.4
266     */
267    protected final void addFilterChain(String pattern, FilterConfig<? extends Filter>... filterConfigs) {
268        filterChains.put(pattern, filterConfigs);
269    }
270
271    /**
272     * Builds a FilterConfig from a Filer and configuration String
273     * @param baseKey The Key of the Filter class to be used.
274     * @param <T> A Servlet Filter class.
275     * @return A FilterConfig used to map a String path to this configuration.
276     * @since 1.4
277     */
278    protected static <T extends Filter> FilterConfig<T> filterConfig(Key<T> baseKey, String configValue) {
279        return new FilterConfig<T>(baseKey, configValue);
280    }
281
282    /**
283     * Builds a FilterConfig from a Filer and configuration String
284     * @param baseKey The Key of the Filter class to be used.
285     * @param <T> A Servlet Filter class.
286     * @return A FilterConfig used to map a String path to this configuration.
287     * @since 1.4
288     */
289    protected static <T extends Filter> FilterConfig<T> filterConfig(Key<T> baseKey) {
290        return filterConfig(baseKey, "");
291    }
292
293    /**
294     * Builds a FilterConfig from a Filer and configuration String
295     * @param typeLiteral The TyleLiteral of the filter key to be used.
296     * @param configValue the configuration used.
297     * @param <T> A Servlet Filter class.
298     * @return A FilterConfig used to map a String path to this configuration.
299     * @since 1.4
300     */
301    @SuppressWarnings({"UnusedDeclaration"})
302    protected static <T extends Filter> FilterConfig<T> filterConfig(TypeLiteral<T> typeLiteral, String configValue) {
303        return filterConfig(Key.get(typeLiteral), configValue);
304    }
305
306    /**
307     * Builds a FilterConfig from a Filer and configuration String
308     * @param type The filter to be used.
309     * @param configValue the configuration used.
310     * @param <T> A Servlet Filter class.
311     * @return A FilterConfig used to map a String path to this configuration.
312     * @since 1.4
313     */
314    @SuppressWarnings({"UnusedDeclaration"})
315    protected static <T extends Filter> FilterConfig<T> filterConfig(Class<T> type, String configValue) {
316        return filterConfig(Key.get(type), configValue);
317    }
318
319
320    /**
321     * Filter configuration which pairs a Filter class with its configuration used on a path.
322     * @param <T> The Servlet Filter class.
323     * @since 1.4
324     */
325    public static class FilterConfig<T extends Filter> {
326        private Key<T> key;
327        private String configValue;
328
329        private FilterConfig(Key<T> key, String configValue) {
330            super();
331            this.key = key;
332            this.configValue = configValue;
333        }
334
335        public Key<T> getKey() {
336            return key;
337        }
338
339        public String getConfigValue() {
340            return configValue;
341        }
342    }
343
344
345
346
347
348
349
350    // legacy methods
351
352
353    static boolean isGuiceVersion3() {
354        try {
355            Class.forName("com.google.inject.multibindings.MapKey");
356            return false;
357        } catch (ClassNotFoundException e) {
358            return true;
359        }
360    }
361
362    private void addLegacyFilterChain(String pattern, FilterConfigKey filterConfigKey) {
363
364        FilterConfig<Filter> filterConfig = new FilterConfig<Filter>(filterConfigKey.getKey(), filterConfigKey.getConfigValue());
365        addFilterChain(pattern, filterConfig);
366    }
367
368    /**
369     * Adds a filter chain to the shiro configuration.
370     * <p/>
371     * NOTE: If the provided key is for a subclass of {@link org.apache.shiro.web.filter.PathMatchingFilter}, it will be registered with a proper
372     * provider.
373     *
374     * @param pattern
375     * @param keys
376     */
377    @SuppressWarnings({"UnusedDeclaration"})
378    @Deprecated
379    protected final void addFilterChain(String pattern, Key<? extends Filter>... keys) {
380
381        // We need to extract the keys and FilterConfigKey and convert to the new format.
382
383        FilterConfig[] filterConfigs = new FilterConfig[keys.length];
384        for (int ii = 0; ii < keys.length; ii++) {
385            Key<? extends Filter> key = keys[ii];
386            // If this is a path matching filter, we need to remember the config
387            if (key instanceof FilterConfigKey) {
388                // legacy config
389                FilterConfigKey legacyKey = (FilterConfigKey) key;
390                filterConfigs[ii] = new FilterConfig(legacyKey.getKey(), legacyKey.getConfigValue());
391            }
392            else {
393                // Some other type of Filter key, no config
394                filterConfigs[ii] = new FilterConfig(key, "");
395            }
396        }
397
398        filterChains.put(pattern, filterConfigs);
399    }
400
401    @Deprecated
402    protected static <T extends PathMatchingFilter> Key<T> config(Key<T> baseKey, String configValue) {
403
404        if( !isGuiceVersion3()) {
405            throw new ConfigurationException("Method ShiroWebModule.config(Key<? extends PathMatchingFilter>, String configValue), is not supported when using Guice 4+");
406        }
407
408        return new FilterConfigKey<T>(baseKey, configValue);
409    }
410
411    @SuppressWarnings({"UnusedDeclaration"})
412    @Deprecated
413    protected static <T extends PathMatchingFilter> Key<T> config(TypeLiteral<T> typeLiteral, String configValue) {
414        return config(Key.get(typeLiteral), configValue);
415    }
416
417    @SuppressWarnings({"UnusedDeclaration"})
418    @Deprecated
419    protected static <T extends PathMatchingFilter> Key<T> config(Class<T> type, String configValue) {
420        return config(Key.get(type), configValue);
421    }
422
423    @Deprecated
424    private static class FilterConfigKey<T extends PathMatchingFilter> extends Key<T> {
425        private Key<T> key;
426        private String configValue;
427
428        private FilterConfigKey(Key<T> key, String configValue) {
429            super();
430            this.key = key;
431            this.configValue = configValue;
432        }
433
434        public Key<T> getKey() {
435            return key;
436        }
437
438        public String getConfigValue() {
439            return configValue;
440        }
441    }
442
443}