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