Looking for a JavaEE Architect or Potential Tech Co-Founder?
Please don't hesitate to contact me.

How to use Shiro with JDBC on JavaEE6 and Glassfish

Before you proceed with this tutorial, it's best to from this link to know how to setup a JavaEE6 project with Shiro integrated:
http://czetsuya-tech.blogspot.com/2012/10/how-to-integrate-apache-shiro-with.html

From there we will focus on the changes:
1.) Update shiro.ini

[main]
saltedJdbcRealm=com.ctr.mdl.commons.web.security.shiro.JdbcRealmImpl

# any object property is automatically configurable in Shiro.ini file
saltedJdbcRealm.jndiDataSourceName=dummyDS 

# the realm should handle also authorization
saltedJdbcRealm.permissionsLookupEnabled=true

# If not filled, subclasses of JdbcRealm assume "select password from users where username = ?"
# first result column is password, second result column is salt 
saltedJdbcRealm.authenticationQuery = SELECT password, salt FROM crm_users WHERE username = ?

# If not filled, subclasses of JdbcRealm assume "select role_name from user_roles where username = ?"
saltedJdbcRealm.userRolesQuery = SELECT name FROM crm_roles a INNER JOIN crm_user_roles b ON a.id=b.role_id INNER JOIN crm_users c ON c.id=b.user_id WHERE c.username = ?

# If not filled, subclasses of JdbcRealm assume "select permission from roles_permissions where role_name = ?"
saltedJdbcRealm.permissionsQuery = SELECT action FROM crm_permissions WHERE role = ?

# password hashing specification, put something big for hasIterations
sha256Matcher = org.apache.shiro.authc.credential.HashedCredentialsMatcher
sha256Matcher.hashAlgorithmName=SHA-256
sha256Matcher.hashIterations=1
saltedJdbcRealm.credentialsMatcher = $sha256Matcher

cacheManager=org.apache.shiro.cache.ehcache.EhCacheManager 
cacheManager.cacheManagerConfigFile=classpath:ehcache.xml
securityManager.cacheManager=$cacheManager 

shiro.loginUrl = /login.xhtml

[urls]
/login.xhtml = authc
/logout = logout

Things to take note:
1.) We should extend JdbcRealm to implement a salted password.
2.) Create a datasource in Glassfish named: dummyDS. For this project, I've use postgresql.
3.) I've enabled permission lookup.
4.) I've enabled ehcache, by adding dependency to pom.xml:
<dependency>
 <groupId>org.apache.shiro</groupId>
 <artifactId>shiro-ehcache</artifactId>
 <version>1.2.1</version>
</dependency>

New/Updated classes:
JdbcRealmImpl:
package com.ctr.mdl.commons.web.security.shiro;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

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.AuthorizationException;
import org.apache.shiro.realm.jdbc.JdbcRealm;
import org.apache.shiro.util.JdbcUtils;
import org.apache.shiro.util.SimpleByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author Edward P. Legaspi
 * @since Oct 15, 2012
 */
public class JdbcRealmImpl extends JdbcRealm {
 private static final Logger log = LoggerFactory
   .getLogger(JdbcRealmImpl.class);

 protected String jndiDataSourceName;

 public JdbcRealmImpl() {

 }

 public String getJndiDataSourceName() {
  return jndiDataSourceName;
 }

 public void setJndiDataSourceName(String jndiDataSourceName) {
  this.jndiDataSourceName = jndiDataSourceName;
  this.dataSource = getDataSourceFromJNDI(jndiDataSourceName);
 }

 private DataSource getDataSourceFromJNDI(String jndiDataSourceName) {
  try {
   InitialContext ic = new InitialContext();
   return (DataSource) ic.lookup(jndiDataSourceName);
  } catch (NamingException e) {
   log.error("JNDI error while retrieving " + jndiDataSourceName, e);
   throw new AuthorizationException(e);
  }
 }

 @Override
 protected AuthenticationInfo doGetAuthenticationInfo(
   AuthenticationToken token) throws AuthenticationException {
  // identify account to log to
  UsernamePasswordToken userPassToken = (UsernamePasswordToken) token;
  String username = userPassToken.getUsername();

  if (username == null) {
   log.debug("Username is null.");
   return null;
  }

  // read password hash and salt from db
  PasswdSalt passwdSalt = getPasswordForUser(username);

  if (passwdSalt == null) {
   log.debug("No account found for user [" + username + "]");
   return null;
  }

  // return salted credentials
  SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username,
    passwdSalt.password, getName());
  info.setCredentialsSalt(new SimpleByteSource(passwdSalt.salt));

  return info;
 }

 private PasswdSalt getPasswordForUser(String username) {
  PreparedStatement statement = null;
  ResultSet resultSet = null;
  Connection conn = null;
  try {
   conn = dataSource.getConnection();
   statement = conn.prepareStatement(authenticationQuery);
   statement.setString(1, username);

   resultSet = statement.executeQuery();

   boolean hasAccount = resultSet.next();
   if (!hasAccount)
    return null;

   String salt = null;
   String password = resultSet.getString(1);
   if (resultSet.getMetaData().getColumnCount() > 1)
    salt = resultSet.getString(2);

   if (resultSet.next()) {
    throw new AuthenticationException(
      "More than one user row found for user [" + username
        + "]. Usernames must be unique.");
   }

   return new PasswdSalt(password, salt);
  } catch (SQLException e) {
   final String message = "There was a SQL error while authenticating user ["
     + username + "]";
   if (log.isErrorEnabled()) {
    log.error(message, e);
   }
   throw new AuthenticationException(message, e);

  } finally {
   JdbcUtils.closeResultSet(resultSet);
   JdbcUtils.closeStatement(statement);
   JdbcUtils.closeConnection(conn);
  }
 }

 class PasswdSalt {
  public String password;
  public String salt;

  public PasswdSalt(String password, String salt) {
   super();
   this.password = password;
   this.salt = salt;
  }
 }
}

And the 3 model classes which basically contains:
User.java
@Entity
@Table(name = "CRM_USERS")
@SequenceGenerator(name = "ID_GENERATOR", sequenceName = "CRM_USERS_SEQ")
public class User implements Serializeable { 
        private static final long serialVersionUID = 6142315693769197546L;

 @Id
 @GeneratedValue(generator = "ID_GENERATOR")
 private Long id;

 @Column(name = "USERNAME", length = 50, unique = true)
 private String userName;

 @Column(name = "PASSWORD", length = 250)
 private String password;

 @Column(name = "SALT", length = 100)
 private String salt;

 @ManyToMany(fetch = FetchType.LAZY)
 @JoinTable(name = "CRM_USER_ROLES", joinColumns = @JoinColumn(name = "USER_ID"), inverseJoinColumns = @JoinColumn(name = "ROLE_ID"))
 private List roles = new ArrayList();
}
Role.java
@Entity(name = "com.ctr.mdl.models.user.Role")
@Table(name = "CRM_ROLES")
@SequenceGenerator(name = "ID_GENERATOR", sequenceName = "CRM_ROLES_SEQ")
public class Role implements Serializable {
 private static final long serialVersionUID = 6142315693769197546L;

 @Id
 @GeneratedValue(generator = "ID_GENERATOR")
 private Long id;

 @Column(name = "NAME", nullable = false, length = 50)
 private String name;

 @Column(name = "DESCRIPTION", nullable = false, length = 50)
 private String description;

 @ManyToMany(fetch = FetchType.LAZY)
 @JoinTable(name = "CRM_USER_ROLES", joinColumns = @JoinColumn(name = "ROLE_ID"), inverseJoinColumns = @JoinColumn(name = "USER_ID"))
 private List users = new ArrayList();
}
Permission.java
@Entity
@Table(name = "CRM_PERMISSIONS")
@SequenceGenerator(name = "ID_GENERATOR", sequenceName = "CRM_PERMISSIONS_SEQ")
public class Permission implements Serializeable {
 private static final long serialVersionUID = -2844386098501951453L;

 @Column(name = "ROLE", nullable = false)
 private String role;

 @Column(name = "ACTION", nullable = false, length = 1500)
 private String action;

 public String getRole() {
  return role;
 }
} 

 Reference: http://meri-stuff.blogspot.com/2011/04/apache-shiro-part-2-realms-database-and.html

Related:
Seam Security: http://czetsuya-tech.blogspot.com/2013/06/how-to-setup-seam3-security-in-jboss-7.html
How to use Shiro with JDBC on JavaEE6 and Glassfish How to use Shiro with JDBC on JavaEE6 and Glassfish Reviewed by Edward Legaspi on Monday, October 15, 2012 Rating: 5

4 comments:

Anonymous said...

hi
can we use shiro withour maven , just a simple project with jdbc, jpa toplink and apache tomcat?

Edward Legaspi said...

hi, yes of course maven is a dependency management so it's not really a requirement in building an app that uses shiro.

Anonymous said...

This looks awful similar to this code https://github.com/SomMeri/SimpleShiroSecuredApplication/blame/authentication_stored_in_database/src/main/java/org/meri/simpleshirosecuredapplication/realm/JNDIAndSaltAwareJdbcRealm.java

Edward Legaspi said...

Well of course, didn't you see the reference at the end of the blog? My point is how to setup shiro in JavaEE6.

Powered by Blogger.