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.Collection;
022import java.util.HashMap;
023import java.util.LinkedHashMap;
024import java.util.Map;
025
026import javax.servlet.Filter;
027import javax.servlet.ServletContext;
028
029import org.apache.shiro.config.ConfigurationException;
030import org.apache.shiro.env.Environment;
031import org.apache.shiro.guice.ShiroModule;
032import org.apache.shiro.mgt.SecurityManager;
033import org.apache.shiro.session.mgt.SessionManager;
034import org.apache.shiro.web.env.WebEnvironment;
035import org.apache.shiro.web.filter.PathMatchingFilter;
036import org.apache.shiro.web.filter.authc.AnonymousFilter;
037import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
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<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}