Sunday, 25 March 2012

Guice, Jersey & Shiro

Introduction

This is a little write-up I did after some experiences with Jersey, Guice and Shiro. The application used to be secured with Spring-Security, which lead to issues when migrating to Guice3. It didn't really fit in the scene anyways so I decided to replace it with Apache Shiro.

Parameters

The code examples are taken from an enterprise application that publishes a rest webservice using Jersey. So we have the following parameters:

  • Jersey
  • Guice3
  • Apache Shiro
  • Security with Microsoft Active Directory and a Custom Database where separate user accounts are maintained

I will not explain how to setup Guice, Guice-Persistence or Jersey. The following paragraphs will only show how Apache Shiro is integrated esp. with Guice.

Apache Shiro provides some documentation about this topic (see Shiro Guice Documentation) but this is a little thin from my point of view.

Maven Dependencies

As this is a web application we need the "shiro-web" artifact. Shiro's latest version ships Guice Integration, so we want this, too:

<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-web</artifactId>
   <version>1.2.0</version>
</dependency>

<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-guice</artifactId>
   <version>1.2.0</version>
</dependency>

Implementing the Realms

To use security with ADS it's best to extend the ActiveDirectoryRealm:

public class CustomActiveDirectoryRealm extends ActiveDirectoryRealm {

    private static final String SEARCH_BASE = "dc=example,dc=org";

    @Inject
    public CustomActiveDirectoryRealm(LdapContextFactory contextFactory, CacheManager cacheManager) {
            setLdapContextFactory(contextFactory);
            setSearchBase(SEARCH_BASE);
            setCacheManager(cacheManager);
            setCachingEnabled(true); 
    }
}

The LdapContextFactory is required to provide a LDAP Connection, the CacheManager is a chaching instance to decrease lookups. As the instances of our CustomerActiveDirectory Realm will be managed by Guice we simply inject both.

Our second Realm will use a database to query user data. Therefore we extend Shiro Authorizing Realm; this requires us to implement two methods to return the account data and role memberships:

public class DatabaseRealm extends AuthorizingRealm {

    private final UserDAO dao;

    @Inject
    public DatabaseRealm(UserDAO dao, CacheManager cacheManager,
                    @Named("SHA1") HashedCredentialsMatcher credentialsMatcher) {
            super(cacheManager, credentialsMatcher);
            setCachingEnabled(true);
            this.dao = dao;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
                    AuthenticationToken token) throws AuthenticationException {

            final UsernamePasswordToken upToken = (UsernamePasswordToken) token;
            final User user = dao.findByUsername(upToken.getUsername());

            return user == null ? null : new SimpleAuthenticationInfo(
                            user.getUsername(), user.getPassword(), getName());
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

            if (principals.fromRealm(getName()).isEmpty()) {
                    return null;
            }

            final String username = (String) principals.fromRealm(getName())
                            .iterator().next();

            final User user = dao.findByUsername(username);
            if (user == null) {
                    return null;
            }

            final SimpleAuthorizationInfo authzInfo = new SimpleAuthorizationInfo();
            for (final UserAuthority role : user.getAuthorities()) {
                    authzInfo.addRole(role.toString());
            }

            return authzInfo;
    }
}

Again, dependencies are injected via the constructor. Our DatabaseRealm is simply linked to a UserDAO that is used to retrieve the user information. Note that depending on how passwords are stored you will need to provide the Credentials Matcher - in this case we inject a SHA1-Credentials Matcher.

Configure the Shiro Module

To configure security we need to setup a Module that we can install later. This is accomplished by extening ShiroWebModule (which extends PrivateModule):

public class ShiroSecurityModule extends ShiroWebModule {

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

    @Provides
    @Singleton
    public LdapContextFactory provideContextFactory() {
            final JndiLdapContextFactory contextFactory = new JndiLdapContextFactory();
            contextFactory.setUrl("ldap://ldaphost:3268/");

            return contextFactory;
    }

    @Override
    protected void configureShiroWeb() {
            bind(CacheManager.class).to(MemoryConstrainedCacheManager.class);
            expose(CacheManager.class);

            bind(AuthenticationStrategy.class).to(FirstSuccessfulStrategy.class);
            expose(AuthenticationStrategy.class);

            bind(HashedCredentialsMatcher.class).annotatedWith(Names.named("SHA1"))
                            .toInstance(new HashedCredentialsMatcher("SHA1"));
            expose(HashedCredentialsMatcher.class).annotatedWith(
                            Names.named("SHA1"));

            bindRealm().to(CustomActiveDirectoryRealm.class);
            bindRealm().to(DatabaseRealm.class);

            addFilterChain("/web/**", USER);
            addFilterChain("/api/**", config(NO_SESSION_CREATION, "true"),
                            AUTHC_BASIC);
    }
}

In here things for the Realms are provided - this is pretty straight forward and self-explanatory. The bindRealm() method will create a MultiBinding - the Realms will be processed in the order they are bound.

Furthermore path-specific authentication methods are declared.

Servlet Configuration

Here we pull it all together: modules for Shiro, persistence and the Jersey resources are installed. Note how we pass Jersey's "RolesAllowedResourceFilterFactory" - this allows us to use the Java security @RolesAllowed Annotations to secure resources and methods. Jersey will examine the SecurityContext which will keep the authorization Information:
public class MyServletModule extends ServletModule {

    @Override
    protected void configureServlets() {

            install(new ShiroSecurityModule(getServletContext()));

            install(new PersistenceModule());
            install(new RestApiModule());

            final Map<String, String> params = new HashMap<String, String>();

            params.put(PackagesResourceConfig.PROPERTY_RESOURCE_FILTER_FACTORIES,
                            "com.sun.jersey.api.container.filter.RolesAllowedResourceFilterFactory");

            filter("/*").through(PersistFilter.class);

            filter("/*").through(GuiceShiroFilter.class);

            serve("/api/*").with(GuiceContainer.class, params);

    }
}

Security with Jersey

This is a simple example how to secure a resource:
@Path("helloworld")
@RolesAllowed("USER")
public class InfoResource {

        @GET
        public String getHelloWorld() {
              return "Hello World";
        }

}

3 comments:

  1. Thanks for the tutorial. It will help a lot to better understand this if you could provide a source code as well. Thanks

    ReplyDelete
  2. I think instead of binding the filter with "filter("/*").through(GuiceShiroFilter.class);"
    one should use "ShiroWebModule.bindGuiceFilter(binder());" in the ShiroSecurityModule. At least the javadoc states so.

    regards and thanks for your post : )

    ReplyDelete
    Replies
    1. I tried it as your form, and the problem is that bindGuiceFilter install another ServletModule.

      We can't have two ServletModule, because each one tries to bind the Scopes and this make errors getting the Injector:

      1) Scope ServletScopes.REQUEST is already bound to com.google.inject.servlet.RequestScoped. Cannot bind ServletScopes.REQUEST.
      at com.google.inject.servlet.InternalServletModule.configure(InternalServletModule.java:77)

      Delete