Question

(Update 2nd Jan 2013) I've now added all the code plus a pom.xml to github at https://github.com/AndyWi/GuiceJerseyJettyShiroExample (End update)

I'm trying to add Form based authentication to my simple application using Shiro. The application uses Guice 3.0, Jersey 1.16 and Shiro 1.2.1, running on embedded Jetty 9.0.0.M4.

My problem is that (as far as I understand) Shiro needs login.jsp to be made available through Guice, and then added to Shiro's filter chain. However, when I do so, Jetty can't find login.jsp. When I exclude login.jsp from the Guice filter, Jetty can find the jsp, but then it's not available to Shiro so the authentication doesn't work.

So, in my bootstrap code, I use this line to add login.jsp to the Guice Filter:

webAppContext.addFilter(GuiceFilter.class, "/login.jsp", null);

In my ShiroWebModule, I add the login.jsp like so:

addFilterChain("/login.jsp", AUTHC);

I've spent a ridiculously long time searching for an answer to this without any hint that anyone else has had the same problem - which means I'm obviously doing something really simple wrong! But I can't work out what it is. I'd be very grateful if anyone could help me get past this.

I've stripped down my project to a small example to demonstrate the problem. All this should do is accept a rest url of /api/uuid, redirect the user to login.jsp, accept any username/password combination for authentication, and then return a new UUID from the /api/uuid service; the user should also remain logged in for future requests. Here's the full code in the hope that it will help someone out there spot the problem:

Bootstrap:

package eg.guicejerseyjettyshiro;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.webapp.WebAppContext;
import com.google.inject.servlet.GuiceFilter;
import eg.guicejerseyjettyshiro.modules.EgGuiceServletContextListener;

public class Bootstrap {

    public static void main(String[] args) throws Exception {
        Server server = new Server(8081);

        WebAppContext webAppContext = new WebAppContext();
        webAppContext.setContextPath("/");
        webAppContext.setResourceBase("src/main/webapp/");
        webAppContext.setParentLoaderPriority(true);

        webAppContext.addEventListener(new EgGuiceServletContextListener());

        webAppContext.addFilter(GuiceFilter.class, "/api/*", null);

        // **** Shiro needs login.jsp to go through the GuiceFilter,
        // but Jetty can't find the jsp when this happens. Commenting
        // out this line lets Jetty find the jsp, but Shiro can't see it:
        webAppContext.addFilter(GuiceFilter.class, "/login.jsp", null);

        webAppContext.addServlet(DefaultServlet.class, "/");

        server.setHandler(webAppContext);

        server.start();
        server.join();
    }

}

EgGuiceServletContextListener:

package eg.guicejerseyjettyshiro.modules;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.servlet.GuiceServletContextListener;

public class EgGuiceServletContextListener extends GuiceServletContextListener {

    private ServletContext servletContext;

    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        this.servletContext = servletContextEvent.getServletContext();
        super.contextInitialized(servletContextEvent);
    }

    @Override
    protected Injector getInjector() {
        return Guice.createInjector(
                new EgJerseyServletModule(),
                new EgShiroWebModule(this.servletContext));
    }

}

EgJerseyServletModule:

package eg.guicejerseyjettyshiro.modules;

import org.apache.shiro.guice.web.GuiceShiroFilter;
import com.sun.jersey.guice.JerseyServletModule;
import com.sun.jersey.guice.spi.container.servlet.GuiceContainer;
import eg.guicejerseyjettyshiro.dao.UuidDao;
import eg.guicejerseyjettyshiro.services.UuidService;

public class EgJerseyServletModule extends JerseyServletModule {

    @Override
    protected void configureServlets() {
        bindings();
        filters();
    }

    private void bindings() {
        bind(UuidDao.class);
        bind(UuidService.class);
    }

    private void filters() {
        filter("/*").through(GuiceShiroFilter.class);
        filter("/*").through(GuiceContainer.class);
    }

}

EgShiroWebModule:

package eg.guicejerseyjettyshiro.modules;

import javax.servlet.ServletContext;
import org.apache.shiro.guice.web.ShiroWebModule;
import com.google.inject.name.Names;
import eg.guicejerseyjettyshiro.realms.EgAuthorizingRealm;

public class EgShiroWebModule extends ShiroWebModule {

    public EgShiroWebModule(ServletContext servletContext) {
        super(servletContext);
    }

    @Override
    protected void configureShiroWeb() {
        bindConstant().annotatedWith(Names.named("shiro.globalSessionTimeout")).to(30000L);

        bindRealm().to(EgAuthorizingRealm.class).asEagerSingleton();

        addFilterChain("/login.jsp", AUTHC);
        addFilterChain("/api/*", AUTHC);
    }

}

EgAuthorizingRealm:

package eg.guicejerseyjettyshiro.realms;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class EgAuthorizingRealm extends AuthorizingRealm {

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        System.out.println("In EgAuthorizingRealm.doGetAuthenticationInfo for: " + upToken.getUsername() + "/" + new String(upToken.getPassword()) + " - remember=" + upToken.isRememberMe());
        return new SimpleAuthenticationInfo(upToken.getUsername(), upToken.getPassword(), getName());
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection pc) {
        System.out.println("In EgAuthorizingRealm.doGetAuthorizationInfo");
        // Doing nothing just now
        return null;
    }

}

UuidService:

package eg.guicejerseyjettyshiro.services;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import eg.guicejerseyjettyshiro.dao.UuidDao;

@Path("/api/uuid")
@Produces({MediaType.APPLICATION_XML})
public class UuidService {

    private final UuidDao uuidDao;

    @Inject
    public UuidService(UuidDao uuidDao) {
        this.uuidDao = uuidDao;
    }

    @GET
    public String get() {
        Subject currentUser = SecurityUtils.getSubject();
        System.out.println("UuidService current user: " + currentUser.getPrincipal().toString());
        return "<uuid>" + this.uuidDao.generateUuid().toString() + "</uuid>";
    }

}

UuidDao:

package eg.guicejerseyjettyshiro.dao;

import java.util.UUID;

public class UuidDao {

    public UUID generateUuid() {
        return UUID.randomUUID();
    }
}

login.jsp:

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Please Login</title>
</head>
<body>

<form name="loginForm" action="" method="post">
<table align="left" border="0" cellspacing="0" cellpadding="3">
    <tr>
        <td>Username:</td>
        <td><input type="text" name="username" maxlength="30"></td>
    </tr>
    <tr>
        <td>Password:</td>
        <td><input type="password" name="password" maxlength="30"></td>
    </tr>
    <tr>
        <td colspan="2" align="left"><input type="checkbox" name="rememberMe"><font size="2">Remember Me</font></td>
    </tr>
    <tr>
        <td colspan="2" align="right"><input type="submit" name="submit" value="Login"></td>
    </tr>
</table> 
</form>

</body>
</html>

OK, I think that's all. I realise this is quite long, so thanks very much for reading this far. This has been driving me crazy, so any help you can give me will be very appreciated!

Thanks, Andy.

Was it helpful?

Solution

This has been answered thanks to Milan Baran on the Shiro users forums. The github repo has been updated, here's a quick summary in case anyone is interested:

In the Bootstrap class, we only need one GuiceFilter added, for /*, and the default server is not needed at all. So, that becomes:

public static void main(String[] args) throws Exception {
    Server server = new Server(8081);

    WebAppContext webAppContext = new WebAppContext();
    webAppContext.setContextPath("/");
    webAppContext.setResourceBase("src/main/webapp/");
    webAppContext.setParentLoaderPriority(true);

    webAppContext.addEventListener(new EgGuiceServletContextListener());

    webAppContext.addFilter(GuiceFilter.class, "/*", null);

    server.setHandler(webAppContext);

    server.start();
    server.join();
}

We then need to update the jersey servlet module to bind DefaultServlet and GuiceContainer, and change the filter through GuiceContainer to be through /api instead of /*, like so:

public class EgJerseyServletModule extends JerseyServletModule {

@Override
protected void configureServlets() {
    bindings();
    filters();
}

private void bindings() {
    bind(UuidDao.class);
    bind(UuidService.class);
    bind(DefaultServlet.class).asEagerSingleton();
    bind(GuiceContainer.class).asEagerSingleton();
    serve("/*").with(DefaultServlet.class);
}

private void filters() {
    filter("/*").through(GuiceShiroFilter.class);
    filter("/api/*").through(GuiceContainer.class);
}

}

Thanks everyone for helping with this! Andy.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top