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