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