If your planning to outsource enterprise level software development, put up an IT company here in the Philippines or simply looking for good developers, feel free to contact me at czetsuya@gmail.com. View my profile.

How to integrate Apache Shiro with JavaEE6


This tutorial will show the readers, how I was able to integrate Apache Shiro with JavaEE6.

My technology Stack:
JavaEE6, Glassfish3.1.2.2, Apache Shiro1.2

Steps:
1.) Create a maven project and include the following dependency.

<dependencies>
 <dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-web</artifactId>
  <version>1.2.1</version>
 </dependency>
 <dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>2.2.2</version>
 </dependency>
 <dependency>
  <groupId>asm</groupId>
  <artifactId>asm</artifactId>
  <version>3.3.1</version>
 </dependency>
 <dependency>
  <groupId>org.jboss.weld</groupId>
  <artifactId>weld-logger</artifactId>
  <version>1.0.0-CR2</version>
 </dependency>
 <dependency>
  <groupId>javax</groupId>
  <artifactId>javaee-api</artifactId>
  <version>6.0</version>
 </dependency>
 <dependency>
  <groupId>javax</groupId>
  <artifactId>javaee-endorsed-api</artifactId>
  <version>6.0</version>
 </dependency>
 <dependency>
  <groupId>javax.enterprise</groupId>
  <artifactId>cdi-api</artifactId>
  <version>1.1.EDR1.2</version>
 </dependency>
</dependencies>

2.) Create a the shiro ini file: shiro.ini inside /src/main/resources folder:

#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

# INI configuration is very powerful and flexible, while still remaining succinct.
# Please http://shiro.apache.org/configuration.html and
# http://shiro.apache.org/web.html for more.

[main]
# listener = org.apache.shiro.config.event.LoggingBeanListener

shiro.loginUrl = /login.xhtml

[users]
# format: username = password, role1, role2, ..., roleN
root = secret,admin
guest = guest,guest
presidentskroob = 12345,president
darkhelmet = ludicrousspeed,darklord,schwartz
lonestarr = vespa,goodguy,schwartz

[roles]
# format: roleName = permission1, permission2, ..., permissionN
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5

[urls]
# The /login.jsp is not restricted to authenticated users (otherwise no one could log in!), but
# the 'authc' filter must still be specified for it so it can process that url's
# login submissions. It is 'smart' enough to allow those requests through as specified by the
# shiro.loginUrl above.
/login.xhtml = authc
/logout = logout
/account/** = authc
/remoting/** = authc, roles[b2bClient], perms["remote:invoke:lan,wan"]

3.) In my case I'm only interested with RequiresAuthenticated, RequiresRoles and RequiresPermissions, so I only implement them. How? Create an interceptor:

package com.czetsuya.commons.web.security.shiro;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.inject.Inject;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;

import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.authz.permission.WildcardPermission;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author Edward P. Legaspi
 * @since Oct 10, 2012
 */
@Secured
@Interceptor
public class SecurityInterceptor {

 @Inject
 private Subject subject;

 private Logger log = LoggerFactory.getLogger(SecurityInterceptor.class);

 @AroundInvoke
 public Object interceptGet(InvocationContext ctx) throws Exception {
  log.info("Securing {}.{}({})", new Object[] { ctx.getClass().getName(),
    ctx.getMethod(), ctx.getParameters() });

  final Class<? extends Object> runtimeClass = ctx.getTarget().getClass();

  // Check if user is authenticated
  boolean requiresAuthentication = false;
  try { // check method first
   ctx.getMethod().getAnnotation(RequiresAuthentication.class);
   requiresAuthentication = true;
  } catch (NullPointerException e) {
   requiresAuthentication = false;
  }

  if (!requiresAuthentication) { // check class level
   try {
    runtimeClass.getAnnotation(RequiresAuthentication.class);
    requiresAuthentication = true;
   } catch (NullPointerException e) {
    requiresAuthentication = false;
   }
  }

  if (requiresAuthentication) {
   log.debug("[security] checking for authenticated user.");
   try {
    if (!subject.isAuthenticated()) {
     throw new AuthorizationException();
    }
   } catch (Exception e) {
    log.error("Access denied - {}: {}", e.getClass().getName(),
      e.getMessage());
    throw e;
   }
  }
  /************************************************************/

  // check if user has roles
  boolean requiresRoles = false;
  List<String> listOfRoles = null;

  try { // check method first
   RequiresRoles roles = ctx.getMethod().getAnnotation(
     RequiresRoles.class);
   listOfRoles = Arrays.asList(roles.value());
   requiresRoles = true;
  } catch (NullPointerException e) {
   requiresRoles = false;
  }

  if (!requiresRoles || listOfRoles == null) { // check class
   try {
    RequiresRoles roles = runtimeClass
      .getAnnotation(RequiresRoles.class);
    listOfRoles = Arrays.asList(roles.value());
    requiresRoles = true;
   } catch (NullPointerException e) {
    requiresRoles = false;
   }
  }

  if (requiresRoles && listOfRoles != null) {
   log.debug("[security] checking for roles.");
   try {
    boolean[] boolRoles = subject.hasRoles(listOfRoles);
    boolean roleVerified = false;
    for (boolean b : boolRoles) {
     if (b) {
      roleVerified = true;
      break;
     }
    }
    if (!roleVerified) {
     throw new AuthorizationException(
       "Access denied. User doesn't have enough privilege Roles:"
         + listOfRoles + " to access this page.");
    }
   } catch (Exception e) {
    log.error("Access denied - {}: {}", e.getClass().getName(),
      e.getMessage());
    throw e;
   }
  }
  /************************************************************/

  // and lastly check for permissions
  boolean requiresPermissions = false;
  List<String> listOfPermissionsString = null;

  try { // check method first
   RequiresPermissions permissions = ctx.getMethod().getAnnotation(
     RequiresPermissions.class);
   listOfPermissionsString = Arrays.asList(permissions.value());
   requiresPermissions = true;
  } catch (NullPointerException e) {
   requiresPermissions = false;
  }

  if (!requiresPermissions || listOfPermissionsString == null) {
   // check class
   try {
    RequiresPermissions permissions = runtimeClass
      .getAnnotation(RequiresPermissions.class);
    listOfPermissionsString = Arrays.asList(permissions.value());
    requiresPermissions = true;
   } catch (NullPointerException e) {
    requiresPermissions = false;
   }
  }

  if (requiresPermissions && listOfPermissionsString != null) {
   log.debug("[security] checking for permissions.");
   List<Permission> listOfPermissions = new ArrayList<Permission>();
   for (String p : listOfPermissionsString) {
    listOfPermissions.add((Permission) new WildcardPermission(p));
   }
   try {
    boolean[] boolPermissions = subject
      .isPermitted(listOfPermissions);
    boolean permitted = false;
    for (boolean b : boolPermissions) {
     if (b) {
      permitted = true;
      break;
     }
    }
    if (!permitted) {
     throw new AuthorizationException(
       "Access denied. User doesn't have enough privilege Permissions:"
         + listOfRoles + " to access this page.");
    }
   } catch (Exception e) {
    log.error("Access denied - {}: {}", e.getClass().getName(),
      e.getMessage());
    throw e;
   }
  }

  return ctx.proceed();
 }
}

3.2) We need JavaEE6 to see the interceptor by including it in beans.xml (which normally resides in /src/main/resources/META-INF of the project where you define the interceptor class).

<beans xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
 <interceptors>
  <class>com.czetsuya.commons.web.security.shiro.SecurityInterceptor</class>
 </interceptors>
</beans>

4.) We need an interface for interceptor binding:

package com.czetsuya.commons.web.security.shiro;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.interceptor.InterceptorBinding;

/**
 * @author Edward P. Legaspi
 * @since Oct 10, 2012
 */
@Inherited
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@InterceptorBinding
public @interface Secured {
}

5.) Then we need to create a Singleton class where we will instantiate the SecurityManager by reading the shiro.ini file and produce the Subject and SecurityManager, so that we can inject them later if we want.

package com.czetsuya.commons.web.security.shiro;

import javax.annotation.PostConstruct;
import javax.enterprise.inject.Produces;
import javax.inject.Named;
import javax.inject.Singleton;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author Edward P. Legaspi
 * @since Oct 10, 2012 Produces an instance of Shiro's subject so that it can be
 *        injected.
 */
@Singleton
public class SecurityProducer {
 Logger logger = LoggerFactory.getLogger(SecurityProducer.class);
 private SecurityManager securityManager;

 @PostConstruct
 public void init() {
  final String iniFile = "classpath:shiro.ini";
  logger.info("Initializing Shiro INI SecurityManager using " + iniFile);
  securityManager = new IniSecurityManagerFactory(iniFile).getInstance();
  SecurityUtils.setSecurityManager(securityManager);
 }

 @Produces
 @Named("securityManager")
 public SecurityManager getSecurityManager() {
  return securityManager;
 }

 @Produces
 public Subject getSubject() {
  return SecurityUtils.getSubject();
 }
}

6.) Sample Usage
package com.czetsuya.mbeans;

import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.inject.Inject;
import javax.inject.Named;

import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;

import com.czetsuya.commons.web.security.shiro.Secured;

/**
 * @author Edward P. Legaspi
 * @since Oct 10, 2012
 */
@LocalBean
@Named
@Stateless
public class LoginBean {
 @Inject
 private Subject subject;

 @Inject
 private Logger log;

 private String username;
 private String password;

 public String login() {
  if (subject.isAuthenticated()) {
   log.debug("[czetsuya-ejbs] active subject={}, user={}", subject,
     subject.getPrincipal());
   return redirect();
  } else {
   log.debug(
     "[czetsuya-ejbs] login to the system with user={}, password={}",
     getUsername(), getPassword());
   AuthenticationToken token = new UsernamePasswordToken(
     getUsername(), getPassword());
   try {
    subject.login(token);

    return redirect();
   } catch (Exception e) {
    log.error("[czetsuya-ejbs] error login {}", e);
    FacesContext.getCurrentInstance().addMessage(null,
      new FacesMessage("Error login"));
   }
  }

  return "home.xhtml";
 }

 public String logout() {
  log.debug("[czetsuya-ejbs] logout");
  if (subject.isAuthenticated()) {
   subject.logout();
  }

  FacesContext.getCurrentInstance().addMessage(null,
    new FacesMessage("Logout Ok"));

  return "login.xhtml";
 }

 private String redirect() {
  log.debug("[czetsuya-ejbs] redirect");
  if (subject.hasRole("admin")) {
   return "admin.xhtml";
  } else if (subject.hasRole("schwartz")) {
   return "schwartz.xhtml";
  } else if (subject.hasRole("goodguy")) {
   return "goodguy.xhtml";
  }
  return "home.xhtml";
 }

 public String getUsername() {
  return username;
 }

 public void setUsername(String username) {
  this.username = username;
 }

 public String getPassword() {
  return password;
 }

 public void setPassword(String password) {
  this.password = password;
 }

 @Secured
 @RequiresAuthentication
 public String requiresAuthentication() {
  return "";
 }

 @Secured
 @RequiresRoles({ "admin" })
 public void requiresRolesMerchant() {
  log.debug("admin");
 }

 @Secured
 @RequiresRoles({ "schwartz" })
 public void requiresRolesSupport() {
  log.debug("schwartz");
 }

 @Secured
 @RequiresPermissions({ "lightsaber" })
 public void requiresPermissionlightsaber() {
  log.debug("lightsaber.action");
 }

 @Secured
 @RequiresPermissions({ "winnebago" })
 public void requiresPermissionSupport() {
  log.debug("winnebago.action");
 }

 @Secured
 @RequiresPermissions({ "winnebago", "lightsaber" })
 public void requiresPermissionlightsaberOrwinnebago() {
  log.debug("winnebago+lightsaber.action");
 }
}

Related:
Seam Security: http://czetsuya-tech.blogspot.com/2013/06/how-to-setup-seam3-security-in-jboss-7.html

{ 5 comments... read them below or add one }

Les Hazlewood said...

This is great!!!! Thanks so much for sharing - this is something many in the Shiro community have been waiting for. Thanks!

Bauke Scholtz said...

I wouldn't consider this code "great". See this blog for an extensive tutorial on integrating Shiro in JSF2+CDI+EJB3. A more clean interceptor implementation is descibed here.

Edward Legaspi said...

Yes I agree, I actually have a problem with this implementation and go with the web.xml approach and use the http default session. I don't have enough time to figure out how to integrate shiro with interceptor, I'm be interested to try yours :-).

Anonymous said...

could provide a link to the source code.
I have trouble using the pages.

Edward Legaspi said...

Feel free to download the code from:

http://code.google.com/p/czetsuya/

Note that I've used the Security Interceptor from Balus.