/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.shiro.guice.web; import java.util.*; import javax.servlet.Filter; import javax.servlet.ServletContext; import org.apache.shiro.config.ConfigurationException; import org.apache.shiro.env.Environment; import org.apache.shiro.guice.ShiroModule; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.util.StringUtils; import org.apache.shiro.web.env.WebEnvironment; import org.apache.shiro.web.filter.PathMatchingFilter; import org.apache.shiro.web.filter.authc.AnonymousFilter; import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter; import org.apache.shiro.web.filter.authc.FormAuthenticationFilter; import org.apache.shiro.web.filter.authc.LogoutFilter; import org.apache.shiro.web.filter.authc.UserFilter; import org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter; import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter; import org.apache.shiro.web.filter.authz.PortFilter; import org.apache.shiro.web.filter.authz.RolesAuthorizationFilter; import org.apache.shiro.web.filter.authz.SslFilter; import org.apache.shiro.web.filter.mgt.FilterChainResolver; import org.apache.shiro.web.filter.session.NoSessionCreationFilter; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.mgt.WebSecurityManager; import org.apache.shiro.web.session.mgt.ServletContainerSessionManager; import com.google.inject.Binder; import com.google.inject.Key; import com.google.inject.TypeLiteral; import com.google.inject.binder.AnnotatedBindingBuilder; import com.google.inject.name.Names; import com.google.inject.servlet.ServletModule; /** * Sets up Shiro lifecycles within Guice, enables the injecting of Shiro objects, and binds a default * {@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 * using {@link #bindRealm() bindRealm}. * <p/> * Also provides for the configuring of filter chains and binds a {@link org.apache.shiro.web.filter.mgt.FilterChainResolver} with that information. */ public abstract class ShiroWebModule extends ShiroModule { @SuppressWarnings({"UnusedDeclaration"}) public static final Key<AnonymousFilter> ANON = Key.get(AnonymousFilter.class); @SuppressWarnings({"UnusedDeclaration"}) public static final Key<FormAuthenticationFilter> AUTHC = Key.get(FormAuthenticationFilter.class); @SuppressWarnings({"UnusedDeclaration"}) public static final Key<BasicHttpAuthenticationFilter> AUTHC_BASIC = Key.get(BasicHttpAuthenticationFilter.class); @SuppressWarnings({"UnusedDeclaration"}) public static final Key<NoSessionCreationFilter> NO_SESSION_CREATION = Key.get(NoSessionCreationFilter.class); @SuppressWarnings({"UnusedDeclaration"}) public static final Key<LogoutFilter> LOGOUT = Key.get(LogoutFilter.class); @SuppressWarnings({"UnusedDeclaration"}) public static final Key<PermissionsAuthorizationFilter> PERMS = Key.get(PermissionsAuthorizationFilter.class); @SuppressWarnings({"UnusedDeclaration"}) public static final Key<PortFilter> PORT = Key.get(PortFilter.class); @SuppressWarnings({"UnusedDeclaration"}) public static final Key<HttpMethodPermissionFilter> REST = Key.get(HttpMethodPermissionFilter.class); @SuppressWarnings({"UnusedDeclaration"}) public static final Key<RolesAuthorizationFilter> ROLES = Key.get(RolesAuthorizationFilter.class); @SuppressWarnings({"UnusedDeclaration"}) public static final Key<SslFilter> SSL = Key.get(SslFilter.class); @SuppressWarnings({"UnusedDeclaration"}) public static final Key<UserFilter> USER = Key.get(UserFilter.class); static final String NAME = "SHIRO"; /** * We use a LinkedHashMap here to ensure that iterator order is the same as add order. This is important, as the * FilterChainResolver uses iterator order when searching for a matching chain. */ private final Map<String, FilterConfig<? extends Filter>[]> filterChains = new LinkedHashMap<String, FilterConfig<? extends Filter>[]>(); private final ServletContext servletContext; public ShiroWebModule(ServletContext servletContext) { this.servletContext = servletContext; } public static void bindGuiceFilter(Binder binder) { binder.install(guiceFilterModule()); } @SuppressWarnings({"UnusedDeclaration"}) public static void bindGuiceFilter(final String pattern, Binder binder) { binder.install(guiceFilterModule(pattern)); } public static ServletModule guiceFilterModule() { return guiceFilterModule("/*"); } public static ServletModule guiceFilterModule(final String pattern) { return new ServletModule() { @Override protected void configureServlets() { filter(pattern).through(GuiceShiroFilter.class); } }; } @Override protected final void configureShiro() { bindBeanType(TypeLiteral.get(ServletContext.class), Key.get(ServletContext.class, Names.named(NAME))); bind(Key.get(ServletContext.class, Names.named(NAME))).toInstance(this.servletContext); bindWebSecurityManager(bind(WebSecurityManager.class)); bindWebEnvironment(bind(WebEnvironment.class)); bind(GuiceShiroFilter.class).asEagerSingleton(); expose(GuiceShiroFilter.class); this.configureShiroWeb(); bind(FilterChainResolver.class).toProvider(new FilterChainResolverProvider(setupFilterChainConfigs())); } private Map<String, Key<? extends Filter>[]> setupFilterChainConfigs() { // loop through and build a map of Filter Key -> Map<Path, Config> Map<Key<? extends Filter>, Map<String, String>> filterToPathToConfig = new HashMap<Key<? extends Filter>, Map<String, String>>(); // At the same time build a map to return with Path -> Key[] Map<String, Key<? extends Filter>[]> resultConfigMap = new LinkedHashMap<String, Key<? extends Filter>[]>(); for (Map.Entry<String, FilterConfig<? extends Filter>[]> filterChain : filterChains.entrySet()) { String path = filterChain.getKey(); // collect the keys used for this path List<Key<? extends Filter>> keysForPath = new ArrayList<Key<? extends Filter>>(); for (int i = 0; i < filterChain.getValue().length; i++) { FilterConfig<? extends Filter> filterConfig = filterChain.getValue()[i]; Key<? extends Filter> key = filterConfig.getKey(); String config = filterConfig.getConfigValue(); // initialize key in filterToPathToConfig, if it doesn't exist if (filterToPathToConfig.get(key) == null) { filterToPathToConfig.put((key), new HashMap<String, String>()); } // now set the value filterToPathToConfig.get(key).put(path, config); // Config error if someone configured a non PathMatchingFilter with a config value if (StringUtils.hasText(config) && !PathMatchingFilter.class.isAssignableFrom(key.getTypeLiteral().getRawType())) { throw new ConfigurationException("Config information requires a PathMatchingFilter - can't apply to " + key.getTypeLiteral().getRawType()); } // store the key in keysForPath keysForPath.add(key); } // map the current path to all of its Keys resultConfigMap.put(path, keysForPath.toArray(new Key[keysForPath.size()])); } // now we find only the PathMatchingFilter and configure bindings // non PathMatchingFilter, can be loaded with the default provider via the class name for (Key<? extends Filter> key : filterToPathToConfig.keySet()) { if (PathMatchingFilter.class.isAssignableFrom(key.getTypeLiteral().getRawType())) { bindPathMatchingFilter(castToPathMatching(key), filterToPathToConfig.get(key)); } else { bind(key); } } return resultConfigMap; } private <T extends PathMatchingFilter> void bindPathMatchingFilter(Key<T> filterKey, Map<String, String> configs) { bind(filterKey).toProvider(new PathMatchingFilterProvider<T>(filterKey, configs)).asEagerSingleton(); } @SuppressWarnings({"unchecked"}) private Key<? extends PathMatchingFilter> castToPathMatching(Key<? extends Filter> key) { return (Key<? extends PathMatchingFilter>) key; } protected abstract void configureShiroWeb(); @SuppressWarnings({"unchecked"}) @Override protected final void bindSecurityManager(AnnotatedBindingBuilder<? super SecurityManager> bind) { bind.to(WebSecurityManager.class); // SHIRO-435 } /** * Binds the security manager. Override this method in order to provide your own security manager binding. * <p/> * By default, a {@link org.apache.shiro.web.mgt.DefaultWebSecurityManager} is bound as an eager singleton. * * @param bind */ protected void bindWebSecurityManager(AnnotatedBindingBuilder<? super WebSecurityManager> bind) { try { bind.toConstructor(DefaultWebSecurityManager.class.getConstructor(Collection.class)).asEagerSingleton(); } catch (NoSuchMethodException e) { throw new ConfigurationException("This really shouldn't happen. Either something has changed in Shiro, or there's a bug in ShiroModule.", e); } } /** * Binds the session manager. Override this method in order to provide your own session manager binding. * <p/> * By default, a {@link org.apache.shiro.web.session.mgt.DefaultWebSessionManager} is bound as an eager singleton. * * @param bind */ @Override protected void bindSessionManager(AnnotatedBindingBuilder<SessionManager> bind) { bind.to(ServletContainerSessionManager.class).asEagerSingleton(); } @Override protected final void bindEnvironment(AnnotatedBindingBuilder<Environment> bind) { bind.to(WebEnvironment.class); // SHIRO-435 } protected void bindWebEnvironment(AnnotatedBindingBuilder<? super WebEnvironment> bind) { bind.to(WebGuiceEnvironment.class).asEagerSingleton(); } protected final void addFilterChain(String pattern, Key<? extends Filter> key) { // check for legacy API if (key instanceof FilterConfigKey) { addLegacyFilterChain(pattern, (FilterConfigKey) key); } else { addFilterChain(pattern, new FilterConfig<Filter>((Key<Filter>) key, "")); } } /** * Maps 'n' number of <code>filterConfig</code>s to a specific path pattern.<BR/> * For example, a path of '/my_private_resource/**' to 'filterConfig(AUTHC)' would require * any resource under the path '/my_private_resource' would be processed through the {@link FormAuthenticationFilter}. * * @param pattern URL patter to be mapped to a FilterConfig, e.g. '/my_private-path/**' * @param filterConfigs FilterConfiguration representing the Filter and config to be used when processing resources on <code>pattern</code>. * @since 1.4 */ protected final void addFilterChain(String pattern, FilterConfig<? extends Filter>... filterConfigs) { filterChains.put(pattern, filterConfigs); } /** * Builds a FilterConfig from a Filer and configuration String * @param baseKey The Key of the Filter class to be used. * @param <T> A Servlet Filter class. * @return A FilterConfig used to map a String path to this configuration. * @since 1.4 */ protected static <T extends Filter> FilterConfig<T> filterConfig(Key<T> baseKey, String configValue) { return new FilterConfig<T>(baseKey, configValue); } /** * Builds a FilterConfig from a Filer and configuration String * @param baseKey The Key of the Filter class to be used. * @param <T> A Servlet Filter class. * @return A FilterConfig used to map a String path to this configuration. * @since 1.4 */ protected static <T extends Filter> FilterConfig<T> filterConfig(Key<T> baseKey) { return filterConfig(baseKey, ""); } /** * Builds a FilterConfig from a Filer and configuration String * @param typeLiteral The TyleLiteral of the filter key to be used. * @param configValue the configuration used. * @param <T> A Servlet Filter class. * @return A FilterConfig used to map a String path to this configuration. * @since 1.4 */ @SuppressWarnings({"UnusedDeclaration"}) protected static <T extends Filter> FilterConfig<T> filterConfig(TypeLiteral<T> typeLiteral, String configValue) { return filterConfig(Key.get(typeLiteral), configValue); } /** * Builds a FilterConfig from a Filer and configuration String * @param type The filter to be used. * @param configValue the configuration used. * @param <T> A Servlet Filter class. * @return A FilterConfig used to map a String path to this configuration. * @since 1.4 */ @SuppressWarnings({"UnusedDeclaration"}) protected static <T extends Filter> FilterConfig<T> filterConfig(Class<T> type, String configValue) { return filterConfig(Key.get(type), configValue); } /** * Filter configuration which pairs a Filter class with its configuration used on a path. * @param <T> The Servlet Filter class. * @since 1.4 */ public static class FilterConfig<T extends Filter> { private Key<T> key; private String configValue; private FilterConfig(Key<T> key, String configValue) { super(); this.key = key; this.configValue = configValue; } public Key<T> getKey() { return key; } public String getConfigValue() { return configValue; } } // legacy methods static boolean isGuiceVersion3() { try { Class.forName("com.google.inject.multibindings.MapKey"); return false; } catch (ClassNotFoundException e) { return true; } } private void addLegacyFilterChain(String pattern, FilterConfigKey filterConfigKey) { FilterConfig<Filter> filterConfig = new FilterConfig<Filter>(filterConfigKey.getKey(), filterConfigKey.getConfigValue()); addFilterChain(pattern, filterConfig); } /** * Adds a filter chain to the shiro configuration. * <p/> * NOTE: If the provided key is for a subclass of {@link org.apache.shiro.web.filter.PathMatchingFilter}, it will be registered with a proper * provider. * * @param pattern * @param keys */ @SuppressWarnings({"UnusedDeclaration"}) @Deprecated protected final void addFilterChain(String pattern, Key<? extends Filter>... keys) { // We need to extract the keys and FilterConfigKey and convert to the new format. FilterConfig[] filterConfigs = new FilterConfig[keys.length]; for (int ii = 0; ii < keys.length; ii++) { Key<? extends Filter> key = keys[ii]; // If this is a path matching filter, we need to remember the config if (key instanceof FilterConfigKey) { // legacy config FilterConfigKey legacyKey = (FilterConfigKey) key; filterConfigs[ii] = new FilterConfig(legacyKey.getKey(), legacyKey.getConfigValue()); } else { // Some other type of Filter key, no config filterConfigs[ii] = new FilterConfig(key, ""); } } filterChains.put(pattern, filterConfigs); } @Deprecated protected static <T extends PathMatchingFilter> Key<T> config(Key<T> baseKey, String configValue) { if( !isGuiceVersion3()) { throw new ConfigurationException("Method ShiroWebModule.config(Key<? extends PathMatchingFilter>, String configValue), is not supported when using Guice 4+"); } return new FilterConfigKey<T>(baseKey, configValue); } @SuppressWarnings({"UnusedDeclaration"}) @Deprecated protected static <T extends PathMatchingFilter> Key<T> config(TypeLiteral<T> typeLiteral, String configValue) { return config(Key.get(typeLiteral), configValue); } @SuppressWarnings({"UnusedDeclaration"}) @Deprecated protected static <T extends PathMatchingFilter> Key<T> config(Class<T> type, String configValue) { return config(Key.get(type), configValue); } @Deprecated private static class FilterConfigKey<T extends PathMatchingFilter> extends Key<T> { private Key<T> key; private String configValue; private FilterConfigKey(Key<T> key, String configValue) { super(); this.key = key; this.configValue = configValue; } public Key<T> getKey() { return key; } public String getConfigValue() { return configValue; } } }