We are going to assume that you have a working simple Shiro setup (Step 1 in link), that Guice is integrated into Servlets already (link) and that you just need to link the two together.
The biggest inherent challenge in this comes from the following facts:
- You need to add a subclass of ShiroWebModule to the Google Injector
- You create the Google Injector when the GuiceServletContextListener calls getInjector()
- The ShiroWebModule needs a ServletContext to initialize
- getInjector() is called before any calls of contextInitialized()
- Lastly, GAE only supports Servlets 2.5 so setting up a ServletContainerInitializer is out too
It seems that we are caught in a classic catch 22, where we need the ServletContext to create the Injector so that we can create the ServletContext. An answer to this, used here, is to:
- create a class to hold the current ServletContext and expose it statically
- to create another class that implements ServletContext and delegates to the static calls and
- to pass this proxy object to the new ShiroWebModule
Here is the GuiceServletContextListener:
package ...; import java.util.List; import org.apache.shiro.guice.web.ShiroWebModule; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Module; import com.google.inject.servlet.GuiceServletContextListener; import com.google.inject.servlet.ServletModule; import com.peninsulawebsolutions.ioc.PwsGuiceModule; import com.peninsulawebsolutions.security.shiro.PwsShiroWebModule; import com.peninsulawebsolutions.servlet.PwsServletContextProxy; public class PwsGuiceServletContextListener extends GuiceServletContextListener { @Override protected Injector getInjector() { // get list of generic modules, must be List and not Set since ordering counts final List<module> pwsGuiceModules = getGuiceModules(); // add Google Guice servlet integration first pwsGuiceModules.add(0, new ServletModule()); // add the Shiro Web Module and ShiroFilterModule last pwsGuiceModules .add(new PwsShiroWebModule(new PwsServletContextProxy())); pwsGuiceModules.add(ShiroWebModule.guiceFilterModule()); final Injector injector = Guice.createInjector(pwsGuiceModules); return injector; } ... }
Here is the ServletContextHolder:
package ...; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; public class PwsServletContextHolder implements ServletContextListener { private static ServletContext servletContext; public static ServletContext getServletContext() { // may want assert statement here to ensure not null return servletContext; } @Override public void contextInitialized(ServletContextEvent sce) { servletContext = sce.getServletContext(); } @Override public void contextDestroyed(ServletContextEvent sce) { servletContext = null; } }
We're almost done with the Java code, this is the ServletContextProxy:
package ...; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.Enumeration; import java.util.Set; import javax.servlet.RequestDispatcher; import javax.servlet.Servlet; import javax.servlet.ServletContext; import javax.servlet.ServletException; public class PwsServletContextProxy implements ServletContext { @Override public String getContextPath() { return getServletContext().getContextPath(); } @Override public ServletContext getContext(String uripath) { return getServletContext().getContext(uripath); } @Override public int getMajorVersion() { return getServletContext().getMajorVersion(); } @Override public int getMinorVersion() { return getServletContext().getMinorVersion(); } @Override public String getMimeType(String file) { return getServletContext().getMimeType(file); } @SuppressWarnings("rawtypes") @Override public Set getResourcePaths(String path) { return getServletContext().getResourcePaths(path); } @Override public URL getResource(String path) throws MalformedURLException { return getServletContext().getResource(path); } @Override public InputStream getResourceAsStream(String path) { return getServletContext().getResourceAsStream(path); } @Override public RequestDispatcher getRequestDispatcher(String path) { return getServletContext().getRequestDispatcher(path); } @Override public RequestDispatcher getNamedDispatcher(String name) { return getNamedDispatcher(name); } @SuppressWarnings("deprecation") @Override public Servlet getServlet(String name) throws ServletException { return getServletContext().getServlet(name); } @SuppressWarnings({ "rawtypes", "deprecation" }) @Override public Enumeration getServlets() { return getServletContext().getServlets(); } @SuppressWarnings({ "rawtypes", "deprecation" }) @Override public Enumeration getServletNames() { return getServletContext().getServletNames(); } @Override public void log(String msg) { getServletContext().log(msg); } @SuppressWarnings("deprecation") @Override public void log(Exception exception, String msg) { getServletContext().log(exception, msg); } @Override public void log(String message, Throwable throwable) { getServletContext().log(message, throwable); } @Override public String getRealPath(String path) { return getServletContext().getRealPath(path); } @Override public String getServerInfo() { return getServletContext().getServerInfo(); } @Override public String getInitParameter(String name) { return getServletContext().getInitParameter(name); } @SuppressWarnings("rawtypes") @Override public Enumeration getInitParameterNames() { return getServletContext().getInitParameterNames(); } @Override public Object getAttribute(String name) { return getServletContext().getAttribute(name); } @SuppressWarnings("rawtypes") @Override public Enumeration getAttributeNames() { return getServletContext().getAttributeNames(); } @Override public void setAttribute(String name, Object object) { getServletContext().setAttribute(name, object); } @Override public void removeAttribute(String name) { getServletContext().removeAttribute(name); } @Override public String getServletContextName() { return getServletContext().getServletContextName(); } protected ServletContext getServletContext() { return PwsServletContextHolder.getServletContext(); } }
That one had taken me a lot of typing to create.
Lastly for Java code we have the ShiroWebModule subclass:
package ...; import javax.servlet.ServletContext; import org.apache.commons.io.FilenameUtils; import org.apache.shiro.config.Ini; import org.apache.shiro.guice.web.ShiroWebModule; import org.apache.shiro.realm.text.IniRealm; import com.google.inject.Provides; import com.peninsulawebsolutions.assertions.PwsAssertUtils; import com.peninsulawebsolutions.exceptions.PwsCheckedException; import com.peninsulawebsolutions.os.PwsFileUtils; import com.peninsulawebsolutions.os.PwsWindowsCommandLine; public class PwsShiroWebModule extends ShiroWebModule { public PwsShiroWebModule(ServletContext servletContext) { super(servletContext); } @Override protected void configureShiroWeb() { try { // realm should be created with the IniRealm(Ini) constructor // bind to different constructor (e.g. an SQL one) as needed bindRealm().toConstructor(IniRealm.class.getConstructor(Ini.class)); } catch (final NoSuchMethodException e) { addError(e); } // can add Shiro API calls here, e.g. addFilterChain } @Provides public Ini loadShiroIni() throws PwsCheckedException { // will probably look like "file:[cwd]/WebContent/WEB-INF/shiro.ini // during test, WebContent/ will need to be omitted when run inside // of container final String path = getIniPath(); final Ini ret = Ini.fromResourcePath(path); PwsAssertUtils.isFalse("Processed Shiro ini file was empty.", ret.isEmpty()); return ret; } ... }
The listener order is important here; we need the current servlet context in the holder before we access it from the Guice listener.
If you keep the old EnvironmentLoaderListener you will get an error about users being defined twice. Also note that this code technically does the same thing as the default behavior and just loads an ini file! The important part is that you have a place to freely call the Shiro API now.
Almost lastly, we finally have the web.xml configuration:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5"> ... <filter> <filter-name>guiceFilter</filter-name> <filter-class>com.google.inject.servlet.GuiceFilter</filter-class> </filter> <filter-mapping> <filter-name>guiceFilter</filter-name> <url-pattern>/*</url-pattern> <dispatcher>FORWARD</dispatcher> <dispatcher>REQUEST</dispatcher> </filter-mapping> ... <filter> <filter-name>shiroFilter</filter-name> <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> <dispatcher>REQUEST</dispatcher> <dispatcher>FORWARD</dispatcher> <dispatcher>INCLUDE</dispatcher> <dispatcher>ERROR</dispatcher> </filter-mapping> <listener> <listener-class>com.peninsulawebsolutions.servlet.PwsServletContextHolder</listener-class> </listener> <listener> <listener-class>com.peninsulawebsolutions.guice.PwsGuiceServletContextListener</listener-class> </listener> <!-- No longer needed with Web configuration! --> <!-- <listener> --> <!-- <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class> --> <!-- </listener> --> ... </web-app>
For the last bit of xml we have the Maven dependencies:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> ... <dependencies> <!-- Google Dependencies --> <dependency> <groupId>com.google.inject</groupId> <artifactId>guice</artifactId> <version>${guice.version}</version> </dependency> <dependency> <groupId>com.google.inject.extensions</groupId> <artifactId>guice-servlet</artifactId> <version>${guice.version}</version> </dependency> <dependency> <groupId>xml-apis</groupId> <artifactId>xml-apis</artifactId> <version>1.4.01</version> </dependency> <!-- Faces and Facelet Dependencies --> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <scope>provided</scope> </dependency> <!-- Shiro Security Dependencies --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>${shiro.version}</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-guice</artifactId> <version>${shiro.version}</version> </dependency> ... </dependencies> ... </project>
Our last note / gotcha is that it is easy to get tripped up with NoClassDefFoundError if any of your Shiro modules have different versions or your Guice modules have different versions (including NO_AOP!) This can be non-trivial if you have a multi-module Maven project.
In conclusion we have the basics for getting Shiro, Guice, Maven and the Google App Engine to play nice with each other. This is done by having a ServletContextHolder wrapped by a ServletContextProxy put into the Guice Injector in the GuiceServletContextListener. We also configured the web.xml to use our two listeners and Maven to have the appropriate dependencies. We also covered a last gotcha.
This should be everything you need to get going. Code was taken from a working Peninsula Web Solutions (PWS) project. Good luck!