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     */
019    package org.apache.shiro.guice.web;
020    
021    import com.google.common.collect.HashBasedTable;
022    import com.google.common.collect.Table;
023    import com.google.inject.Binder;
024    import com.google.inject.Key;
025    import com.google.inject.TypeLiteral;
026    import com.google.inject.binder.AnnotatedBindingBuilder;
027    import com.google.inject.name.Names;
028    import com.google.inject.servlet.ServletModule;
029    import org.apache.shiro.guice.ShiroModule;
030    import org.apache.shiro.config.ConfigurationException;
031    import org.apache.shiro.env.Environment;
032    import org.apache.shiro.mgt.SecurityManager;
033    import org.apache.shiro.session.mgt.SessionManager;
034    import org.apache.shiro.web.env.WebEnvironment;
035    import org.apache.shiro.web.filter.PathMatchingFilter;
036    import org.apache.shiro.web.filter.authc.*;
037    import org.apache.shiro.web.filter.authz.*;
038    import org.apache.shiro.web.filter.mgt.FilterChainResolver;
039    import org.apache.shiro.web.filter.session.NoSessionCreationFilter;
040    import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
041    import org.apache.shiro.web.mgt.WebSecurityManager;
042    import org.apache.shiro.web.session.mgt.ServletContainerSessionManager;
043    
044    import javax.servlet.Filter;
045    import javax.servlet.ServletContext;
046    import java.util.Collection;
047    import java.util.LinkedHashMap;
048    import java.util.Map;
049    
050    /**
051     * Sets up Shiro lifecycles within Guice, enables the injecting of Shiro objects, and binds a default
052     * {@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
053     * using {@link #bindRealm() bindRealm}.
054     * <p/>
055     * Also provides for the configuring of filter chains and binds a {@link org.apache.shiro.web.filter.mgt.FilterChainResolver} with that information.
056     */
057    public abstract class ShiroWebModule extends ShiroModule {
058        @SuppressWarnings({"UnusedDeclaration"})
059        public static final Key<AnonymousFilter> ANON = Key.get(AnonymousFilter.class);
060        @SuppressWarnings({"UnusedDeclaration"})
061        public static final Key<FormAuthenticationFilter> AUTHC = Key.get(FormAuthenticationFilter.class);
062        @SuppressWarnings({"UnusedDeclaration"})
063        public static final Key<BasicHttpAuthenticationFilter> AUTHC_BASIC = Key.get(BasicHttpAuthenticationFilter.class);
064        @SuppressWarnings({"UnusedDeclaration"})
065        public static final Key<NoSessionCreationFilter> NO_SESSION_CREATION = Key.get(NoSessionCreationFilter.class);
066        @SuppressWarnings({"UnusedDeclaration"})
067        public static final Key<LogoutFilter> LOGOUT = Key.get(LogoutFilter.class);
068        @SuppressWarnings({"UnusedDeclaration"})
069        public static final Key<PermissionsAuthorizationFilter> PERMS = Key.get(PermissionsAuthorizationFilter.class);
070        @SuppressWarnings({"UnusedDeclaration"})
071        public static final Key<PortFilter> PORT = Key.get(PortFilter.class);
072        @SuppressWarnings({"UnusedDeclaration"})
073        public static final Key<HttpMethodPermissionFilter> REST = Key.get(HttpMethodPermissionFilter.class);
074        @SuppressWarnings({"UnusedDeclaration"})
075        public static final Key<RolesAuthorizationFilter> ROLES = Key.get(RolesAuthorizationFilter.class);
076        @SuppressWarnings({"UnusedDeclaration"})
077        public static final Key<SslFilter> SSL = Key.get(SslFilter.class);
078        @SuppressWarnings({"UnusedDeclaration"})
079        public static final Key<UserFilter> USER = Key.get(UserFilter.class);
080    
081    
082        static final String NAME = "SHIRO";
083    
084        /**
085         * We use a LinkedHashMap here to ensure that iterator order is the same as add order.  This is important, as the
086         * FilterChainResolver uses iterator order when searching for a matching chain.
087         */
088        private final Map<String, Key<? extends Filter>[]> filterChains = new LinkedHashMap<String, Key<? extends Filter>[]>();
089        private final ServletContext servletContext;
090    
091        public ShiroWebModule(ServletContext servletContext) {
092            this.servletContext = servletContext;
093        }
094    
095        public static void bindGuiceFilter(Binder binder) {
096            binder.install(guiceFilterModule());
097        }
098    
099        @SuppressWarnings({"UnusedDeclaration"})
100        public static void bindGuiceFilter(final String pattern, Binder binder) {
101            binder.install(guiceFilterModule(pattern));
102        }
103    
104        public static ServletModule guiceFilterModule() {
105            return guiceFilterModule("/*");
106        }
107    
108        public static ServletModule guiceFilterModule(final String pattern) {
109            return new ServletModule() {
110                @Override
111                protected void configureServlets() {
112                    filter(pattern).through(GuiceShiroFilter.class);
113                }
114            };
115        }
116    
117        @Override
118        protected final void configureShiro() {
119            bindBeanType(TypeLiteral.get(ServletContext.class), Key.get(ServletContext.class, Names.named(NAME)));
120            bind(Key.get(ServletContext.class, Names.named(NAME))).toInstance(this.servletContext);
121            bindWebSecurityManager(bind(WebSecurityManager.class));
122            bindWebEnvironment(bind(WebEnvironment.class));
123            bind(GuiceShiroFilter.class).asEagerSingleton();
124            expose(GuiceShiroFilter.class);
125    
126            this.configureShiroWeb();
127    
128            setupFilterChainConfigs();
129    
130            bind(FilterChainResolver.class).toProvider(new FilterChainResolverProvider(filterChains));
131        }
132    
133        private void setupFilterChainConfigs() {
134            Table<Key<? extends PathMatchingFilter>, String, String> configs = HashBasedTable.create();
135    
136            for (Map.Entry<String, Key<? extends Filter>[]> filterChain : filterChains.entrySet()) {
137                for (int i = 0; i < filterChain.getValue().length; i++) {
138                    Key<? extends Filter> key = filterChain.getValue()[i];
139                    if (key instanceof FilterConfigKey) {
140                        FilterConfigKey<? extends PathMatchingFilter> configKey = (FilterConfigKey<? extends PathMatchingFilter>) key;
141                        key = configKey.getKey();
142                        filterChain.getValue()[i] = key;
143                        if (!PathMatchingFilter.class.isAssignableFrom(key.getTypeLiteral().getRawType())) {
144                            throw new ConfigurationException("Config information requires a PathMatchingFilter - can't apply to " + key.getTypeLiteral().getRawType());
145                        }
146                        configs.put(castToPathMatching(key), filterChain.getKey(), configKey.getConfigValue());
147                    } else if (PathMatchingFilter.class.isAssignableFrom(key.getTypeLiteral().getRawType())) {
148                        configs.put(castToPathMatching(key), filterChain.getKey(), "");
149                    }
150                }
151            }
152            for (Key<? extends PathMatchingFilter> filterKey : configs.rowKeySet()) {
153                bindPathMatchingFilter(filterKey, configs.row(filterKey));
154            }
155        }
156    
157        private <T extends PathMatchingFilter> void bindPathMatchingFilter(Key<T> filterKey, Map<String, String> configs) {
158            bind(filterKey).toProvider(new PathMatchingFilterProvider<T>(filterKey, configs)).asEagerSingleton();
159        }
160    
161        @SuppressWarnings({"unchecked"})
162        private Key<? extends PathMatchingFilter> castToPathMatching(Key<? extends Filter> key) {
163            return (Key<? extends PathMatchingFilter>) key;
164        }
165    
166        protected abstract void configureShiroWeb();
167    
168        @SuppressWarnings({"unchecked"})
169        @Override
170        protected final void bindSecurityManager(AnnotatedBindingBuilder<? super SecurityManager> bind) {
171            bindWebSecurityManager(bind);
172        }
173    
174        /**
175         * Binds the security manager.  Override this method in order to provide your own security manager binding.
176         * <p/>
177         * By default, a {@link org.apache.shiro.web.mgt.DefaultWebSecurityManager} is bound as an eager singleton.
178         *
179         * @param bind
180         */
181        protected void bindWebSecurityManager(AnnotatedBindingBuilder<? super WebSecurityManager> bind) {
182            try {
183                bind.toConstructor(DefaultWebSecurityManager.class.getConstructor(Collection.class)).asEagerSingleton();
184            } catch (NoSuchMethodException e) {
185                throw new ConfigurationException("This really shouldn't happen.  Either something has changed in Shiro, or there's a bug in ShiroModule.", e);
186            }
187        }
188    
189        /**
190         * Binds the session manager.  Override this method in order to provide your own session manager binding.
191         * <p/>
192         * By default, a {@link org.apache.shiro.web.session.mgt.DefaultWebSessionManager} is bound as an eager singleton.
193         *
194         * @param bind
195         */
196        @Override
197        protected void bindSessionManager(AnnotatedBindingBuilder<SessionManager> bind) {
198            bind.to(ServletContainerSessionManager.class).asEagerSingleton();
199        }
200    
201        @Override
202        protected final void bindEnvironment(AnnotatedBindingBuilder<Environment> bind) {
203            bindWebEnvironment(bind);
204        }
205    
206        protected void bindWebEnvironment(AnnotatedBindingBuilder<? super WebEnvironment> bind) {
207            bind.to(WebGuiceEnvironment.class).asEagerSingleton();
208        }
209    
210        /**
211         * Adds a filter chain to the shiro configuration.
212         * <p/>
213         * NOTE: If the provided key is for a subclass of {@link org.apache.shiro.web.filter.PathMatchingFilter}, it will be registered with a proper
214         * provider.
215         *
216         * @param pattern
217         * @param keys
218         */
219        @SuppressWarnings({"UnusedDeclaration"})
220        protected final void addFilterChain(String pattern, Key<? extends Filter>... keys) {
221            filterChains.put(pattern, keys);
222        }
223    
224        protected static <T extends PathMatchingFilter> Key<T> config(Key<T> baseKey, String configValue) {
225            return new FilterConfigKey<T>(baseKey, configValue);
226        }
227    
228        @SuppressWarnings({"UnusedDeclaration"})
229        protected static <T extends PathMatchingFilter> Key<T> config(TypeLiteral<T> typeLiteral, String configValue) {
230            return config(Key.get(typeLiteral), configValue);
231        }
232    
233        @SuppressWarnings({"UnusedDeclaration"})
234        protected static <T extends PathMatchingFilter> Key<T> config(Class<T> type, String configValue) {
235            return config(Key.get(type), configValue);
236        }
237    
238        private static class FilterConfigKey<T extends PathMatchingFilter> extends Key<T> {
239            private Key<T> key;
240            private String configValue;
241    
242            private FilterConfigKey(Key<T> key, String configValue) {
243                super();
244                this.key = key;
245                this.configValue = configValue;
246            }
247    
248            public Key<T> getKey() {
249                return key;
250            }
251    
252            public String getConfigValue() {
253                return configValue;
254            }
255        }
256    }