/** * Copyright (c) 2013-2016, The SeedStack authors <http://seedstack.org> * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.seedstack.seed.web.security.internal.shiro; import com.google.common.collect.HashBasedTable; import com.google.common.collect.Table; import com.google.inject.Key; import com.google.inject.TypeLiteral; import com.google.inject.binder.AnnotatedBindingBuilder; import com.google.inject.name.Names; import org.apache.shiro.config.ConfigurationException; import org.apache.shiro.env.Environment; import org.apache.shiro.guice.ShiroModule; import org.apache.shiro.guice.web.GuiceShiroFilter; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.mgt.SessionManager; 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 javax.servlet.Filter; import javax.servlet.ServletContext; import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; /** * 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}. * * 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, FilterKey[]> filterChains = new LinkedHashMap<>(); private final ServletContext servletContext; public ShiroWebModule(ServletContext servletContext) { this.servletContext = servletContext; } @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(); setupFilterChainConfigs(); bind(FilterChainResolver.class).toProvider(new FilterChainResolverProvider(filterChains)); } private void setupFilterChainConfigs() { Table<Key<? extends PathMatchingFilter>, String, String> configs = HashBasedTable.create(); for (Map.Entry<String, FilterKey[]> filterChain : filterChains.entrySet()) { for (int i = 0; i < filterChain.getValue().length; i++) { FilterKey configKey = filterChain.getValue()[i]; if (!configKey.getValue().isEmpty()) { filterChain.getValue()[i] = configKey; Key<? extends Filter> key = configKey.getKey(); if (!PathMatchingFilter.class.isAssignableFrom(key.getTypeLiteral().getRawType())) { throw new ConfigurationException("Config information requires a PathMatchingFilter - can't apply to " + key.getTypeLiteral().getRawType()); } configs.put(castToPathMatching(key), filterChain.getKey(), configKey.getValue()); } else if (PathMatchingFilter.class.isAssignableFrom(configKey.getKey().getTypeLiteral().getRawType())) { configs.put(castToPathMatching(configKey.getKey()), filterChain.getKey(), ""); } } } for (Key<? extends PathMatchingFilter> filterKey : configs.rowKeySet()) { bindPathMatchingFilter(filterKey, configs.row(filterKey)); } } private <T extends PathMatchingFilter> void bindPathMatchingFilter(Key<T> filterKey, Map<String, String> configs) { bind(filterKey).toProvider(new PathMatchingFilterProvider<>(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) { bindWebSecurityManager(bind); } /** * Binds the security manager. Override this method in order to provide your own security manager binding. * * By default, a {@link org.apache.shiro.web.mgt.DefaultWebSecurityManager} is bound as an eager singleton. * * @param bind the binding builder */ 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. * * By default, a {@link org.apache.shiro.web.session.mgt.DefaultWebSessionManager} is bound as an eager singleton. * * @param bind the binding builder */ @Override protected void bindSessionManager(AnnotatedBindingBuilder<SessionManager> bind) { bind.to(ServletContainerSessionManager.class).asEagerSingleton(); } @Override protected final void bindEnvironment(AnnotatedBindingBuilder<Environment> bind) { bindWebEnvironment(bind); } protected void bindWebEnvironment(AnnotatedBindingBuilder<? super WebEnvironment> bind) { bind.to(WebGuiceEnvironment.class).asEagerSingleton(); } /** * Adds a filter chain to the shiro configuration. * * 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 the pattern * @param keys the filter keys */ @SuppressWarnings({"UnusedDeclaration"}) protected final void addFilterChain(String pattern, FilterKey... keys) { filterChains.put(pattern, keys); } public static class FilterKey { private final Key<? extends Filter> key; private final String value; public FilterKey(Key<? extends Filter> key, String value) { this.key = key; this.value = value; } Key<? extends Filter> getKey() { return key; } String getValue() { return value; } } }