Accessing Persistent Resources

 < Free Open Study > 



We've illustrated how to use initialization parameters, XML, and JNDI to configure properties and resource factories for servlet applications. Now we're ready to illustrate how to actually use such resource factories to work with persistent data from external data sources.

To do this, we'll develop a sample user authentication servlet application, which authenticates a supplied user name and password against information stored in an underlying data source.

The data source can be specified at deployment time, via properties in an XML configuration file. To accomplish such a level of configuration, we will make use of a common enterprise design pattern - the Data Access Object (DAO) pattern. This pattern specifies the separation of business logic from persistence logic, which can have many advantages.

The Data Access Object Pattern

If a business component is independent of the persistence logic, it requires no changes should the persistence logic change. This allows the business logic to be reused with different data stores.

start sidebar

The Data Access Object (DAO) pattern describes a common pattern that can be used to develop business components that are independent of the persistence logic.

end sidebar

In the DAO pattern, a business object dynamically loads an instance of a specified data access object from properties specified at deployment time. The actual data access object class is either an abstract class or interface, so a specific implementation is required in order to use it.

The benefits of database independence often come at the price of performance (as we saw in our implementation of the DataSource interface). By using the DAO pattern, we can ensure data source independence, and still enjoy as many of the vendor-specific features and extensions.

When we use the DAO pattern we have to implement a lot of components each time we decided to support another database. In such cases, the performance advantages must be evaluated against the increased complexity in design, and development cost. You can find more information on the DAO design pattern at http://java.sun.com/blueprints/patterns/j2ee_patterns/data_access_object.

An Authentication Application Framework

To illustrate the use of the DAO pattern in an enterprise servlet scenario, we'll implement a simple servlet application that accepts a user name and password and then attempts to look up a corresponding user entry from an underlying data store. If the specified user is found, we construct an instance of a User class, and associate it with an array of Group objects, each of which represents a particular group that the user is part of. Finally, the User object is bound to the HttpSession of the requesting user, and can be used by other parts of the application to validate the user's access to various resources.

The servlet we use to authenticate the users is called AuthenticationServlet. Once initialized, the AuthenticationServlet reads a list of properties from an XML configuration file through an instance of a AuthenticationConfig class (which extends our abstract Config class). To authenticate each user, AuthenticationServlet makes use of an instance of the Authenticator interface, which defines general methods for authenticating a user.

This design follows the DAO pattern: the AuthenticatonServlet is plays the role of the business object, and Authenticator serves as the abstract data access object.

Once we have set up the framework for our application, we'll illustrate two implementations of the Authenticator class: one using a relational database and another using a LDAP directory.

Implementing the User Class

An instance of the User class represents a particular real-life user. Each User instance stores the name of the user in question and an array of groups that user is member of:

    package persistence;    import java.util.Hashtable;    public class User {      private Hashtable groups;      private String name;      public User(String name) {        this.name = name;        groups = new Hashtable();      }      public String getName() {        return name;      }      public void addGroup(Group grp) {        groups.put(grp.getName(), grp);      }      public void removeGroup(Group grp) {        groups.remove(grp.getName());      }      public boolean isInGroup(Group grp) {        return groups.containsKey(grp.getName());      }    } 

Implementing the Group Class

Each instance of the Group class represents a particular group of users. A group is identified only by its name (although a more detailed implementation might include other properties):

    package persistence;    public class Group {      private String name;      public Group(String name) {        this.name = name;      }      public String getName() {        return name;      }    } 

Implementing the Authenticator Interface

The Authenticator interface defines common methods used to authenticate a set of user credentials against a persistent data store:

    package persistence;    import javax.servlet.ServletContext;    public interface Authenticator {      public void init(ServletContext sctx) throws Exception; 

The authenticate() method defined by the Authenticator interface takes a user name and password as arguments. It returns a corresponding User instance if the credentials are valid. If they are not, an AuthenticationException (a custom exception we'll define shortly), should be thrown. If an unknown error occurs, an UnknownException (another custom exception) should be thrown:

      public User authenticate(String username, String password)        throws AuthenticationException, UnknownException;    } 

Implementing the Custom Exceptions

When a user supplies invalid credentials in a call to the authenticate() method of Authenticator an AuthenticationException is thrown:

    package persistence;    public class AuthenticationException extends Exception {      public AuthenticationException() {        super("Invalid user name/password");      }    } 

If some unexpected persistence error occurs while authenticating a user, an UnknownException is thrown:

    package persistence;    public class UnknownException extends Exception {      public UnknownException(Exception e) {        super("Unknown exception has occured: " + e.getMessage());      }    } 

Configuring the Framework

Next, we implement a class to read configuration data from an XML file. This class, AuthenticationConfig, extends our abstract Config class from earlier. It attempts to read properties from an XML file of the following structure:

    <?xml version="1.0" encoding="ISO-8859-1"?>    <AuthenticationConfig>      <Authenticator>[your-authentication-class]</Authenticator>      <AuthenticationExceptionURI>        /authentication_exception.html      </AuthenticationExceptionURI>      <AuthenticatedURI>/authenticated.html</AuthenticatedURI>      <UnknownErrorURI>/error.html</UnknownErrorURI>    </AuthenticationConfig> 

The AuthenticationConfig class is defined below. Its init() method starts by invoking the parent init() method of the Config class, and then proceeds to read properties from the XML element. Finally, it calls the cleanup() method, in order to free up some memory:

    package persistence.servlet;    import persistence.*;    import java.io.InputStream;    import java.util.*;    import javax.servlet.*;    import org.w3c.dom.*;    import org.xml.sax.InputSource;    import org.apache.xerces.parsers.DOMParser;    public class AuthenticationConfig extends Config {      private static final String AUTHENTICATOR = "Authenticator";      private static final String AUTHENTICATION_EXCPTION_URI =                                                   "AuthenticationExceptionURI";      private static final String UNKNOWN_ERROR_URI = "UnknownErrorURI";      private static final String AUTHENTICATED_URI = "AuthenticatedURI";      private Authenticator auth;      private String authenticationExceptionURI;      private String authenticatedURI;      private String unknownErrorURI;      public void init(ServletContext sctx, String xmlFile) throws Exception {        super.init(sctx, xmlFile);        authenticationExceptionURI =                              getElementText(root, AUTHENTICATION_EXCPTION_URI);        authenticatedURI = getElementText(root, AUTHENTICATED_URI);        unknownErrorURI = getElementText(root, UNKNOWN_ERROR_URI);        String authname = getElementText(root, AUTHENTICATOR);        auth = (Authenticator) Class.forName(authname).newInstance();        auth.init(sctx);        cleanup();      }      public Authenticator getAuthenticator() {        return auth;      }      public String getAuthenticationExceptionURI() {        return authenticationExceptionURI;      }      public String getAuthenticatedURI() {        return authenticatedURI;      }      public String getUnknownErrorURI() {        return unknownErrorURI;      }    } 

Implementing the Authentication Servlet

Now that we have declared the foundations of our authentication application framework, we're ready to implement the actual AuthenticationServlet:

    package persistence.servlet;    import persistence.*;    import java.io.IOException;    import javax.servlet.*;    import javax.servlet.http.*;    public class AuthenticationServlet extends HttpServlet { 

The AuthenticationServlet declares a few public constants that it uses to identify request attributes:

      public static final String USER = "user";      public static final String USER_NAME = "username";      public static final String PASSWORD = "password";      private Authenticator auth;      private String authenticationExceptionURI;      private String authenticatedURI;      private String unknownExceptionURI; 

When initialized, the AuthenticationServlet obtains an instance of the AuthenticationConfig class. Through public methods of this class, the servlet obtains information on the Authenticator implementation it should use, in addition to a list of URIs that it should use after an authentication has been attempted. A client requests an authentication (by submitting a user name and password), and if it succeeds it is redirected to a specified authenticated locations. If an error occurs or the user is not authenticated, the client is redirected accordingly:

      public void init() throws ServletException {        try {          ServletContext sctx = getServletContext();          ServletConfig sc = getServletConfig();          AuthenticationConfig cfig = new AuthenticationConfig();          cfig.init(sctx, sc.getInitParameter("AuthenticationConfigXML"));          auth = cfig.getAuthenticator();          authenticationExceptionURI = cfig.getAuthenticationExceptionURI();          authenticatedURI = cfig.getAuthenticatedURI();          unknownExceptionURI = cfig.getUnknownErrorURI();        } catch (Throwable t) {          getServletContext().log("AuthenticationServlet", t);        }      } 

The doPost() method determines whether the specified request contains the user name and password, and if it does, attempts to authenticate the requesting user. The request is then redirected to the appropriate location:

      protected void doPost(HttpServletRequest request,                            HttpServletResponse response)          throws ServletException, IOException {        if (request.getParameter(USER_NAME) == null ||            request.getParameter(PASSWORD) == null) {          response.sendRedirect(authenticationExceptionURI);        }        try {          User usr = auth.authenticate(request.getParameter(USER_NAME),                                       request.getParameter(PASSWORD));          request.getSession().setAttribute(USER, usr);          response.sendRedirect(authenticatedURI);        } catch (AuthenticationException a) {          response.sendRedirect(authenticationExceptionURI);        } catch (UnknownException e) {          getServletContext().log("AuthenticationServlet", e);          response.sendRedirect(unknownExceptionURI);        }      }    } 

Deploying the Application

To deploy the authentication servlet, we need to add the following to our application's deployment descriptor:

    <web-app>      ...      <servlet>        <servlet-name>AuthenticationServlet</servlet-name>        <servlet-class>persistence.servlet.AuthenticationServlet</servlet-class>        <init-param>          <param-name>AuthenticationConfigXML</param-name>          <param-value>/xml/authentication-config.xml</param-value>        </init-param>      </servlet>      <servlet-mapping>        <servlet-name>AuthenticationServlet</servlet-name>        <url-pattern>/servlet/AuthenticationServlet</url-pattern>      </servlet-mapping>      ...    <web-app> 

In order to actually use the application, we need a valid Authenticator implementation, which we'll create next.

Accessing Resources Using JDBC

In this section, we'll illustrate how to implement our Authenticator interface for a relational database. Using what we learnt earlier, we'll be able to access the database through a Datasource object that is looked up from a JNDI tree, using a JNDI name obtained from a servlet initialization parameter.

Creating the Database

Before we start to implement the actual Java class, we need to set up the database tables used to store user and group information. In this example, we'll be using MySQL. The only changes you need to make if you want to run this for another type of database, are at the database level (for example, modifying the SQL used to create the database tables).

Use the following SQL script to create the database and tables:

    CREATE DATABASE persistence;    USE persistence;    CREATE TABLE users (        user_id INTEGER PRIMARY KEY,        user_name VARCHAR(20),        password VARCHAR(20)    );    CREATE TABLE groups (        group_id INTEGER PRIMARY KEY,        group_name VARCHAR(20)    );    CREATE TABLE users_groups (        user_id INTEGER,        group_id INTEGER    ); 

We specify a user with a username of danr with a password of wrox for this database:

    GRANT ALL PRIVILEGES ON persistence.* to danr IDENTIFIED BY 'wrox' 

We'll also populate the database with some sample data:

    INSERT INTO users VALUES (1, 'foo', 'bar');    INSERT INTO groups VALUES (10, 'Finance');    INSERT INTO groups VALUES (20, 'Research');    INSERT INTO users_groups VALUES (1, 10);    INSERT INTO users_groups VALUES (1, 20); 

Using these values, we can log in to our application using the user name foo and a password bar.

Implementing the DatabaseAuthenticator Class

Now that we have set up the necessary database tables, it's time to implement the actual database authenticator. This class assumes that the application has already configured and bound a valid DataSource object to a JNDI logical name, as we discussed earlier. DatabaseAuthenticator stores an instance of the DataSource object it uses to obtain connections to the database:

    package persistence.database;    import java.sql.*;    import javax.naming.InitialContext;    import javax.sql.DataSource;    import javax.servlet.ServletContext;    import persistence.*;    public class DatabaseAuthenticator implements Authenticator {      private DataSource ds; 

When initialized, the DatabaseAuthenticator looks up a data source object from the initial context, using the data source JNDI name specified in the application's deployment descriptor (as we have previously described).

start sidebar

In order for this component to properly work, you must set up the data source configuration we described earlier.

end sidebar

      public void init(ServletContext sctx) throws Exception {        InitialContext ctx = new InitialContext();        ds = (DataSource) ctx.lookup(sctx.getInitParameter("DataSource"));      } 

The authenticate() method opens a connection to the database and attempts to look up the ID of a user with the supplied user credentials:

      public User authenticate(String username, String password)          throws AuthenticationException, UnknownException {        Connection conn = null;        PreparedStatement pstmt = null;        ResultSet rs;        try {          conn = ds.getConnection();          pstmt = conn.prepareStatement("SELECT user_id " +                                        "FROM users " +                                        "WHERE user_name = ? " +                                        "AND password = ?");          pstmt.setString(1, username);          pstmt.setString(2, password);          rs = pstmt.executeQuery(); 

If no matching user was found, an AuthenticationException is thrown:

          if (!rs.next()) {            throw new AuthenticationException();          } 

Otherwise, we construct a new User object and select the groups this user is part of:

          User usr = new User(username);          long userID = rs.getLong("user_id");          pstmt = conn.prepareStatement("SELECT g.group_name AS name " +                                        "FROM groups g, users_groups ug " +                                        "WHERE g.group_id = ug.group_id " +                                        "AND ug.user_id = ?");          pstmt.setLong(1, userID);          rs = pstmt.executeQuery();          while (rs.next()) {            usr.addGroup(new Group(rs.getString("name")));          }          return usr;        } catch (SQLException e) { 

Finally, we make sure that all database resources are properly shut down, even in the case of an exception:

        } finally {          if (pstmt != null) {            try {              pstmt.close();            } catch (SQLException ignored) {}          }          if (conn != null) {            try {              conn.close();            } catch (SQLException ignored) {}          }        }      }    } 

Using the DatabaseAuthenticator

Once we have compiled DatabaseAuthenticator, we can use it by altering the value of the <Authenticator> element in authentication-config.xml:

    <?xml version="1.0" encoding="ISO-8859-1"?>    <AuthenticationConfig>      <Authenticator>persistence.database.DatabaseAuthenticator</Authenticator>      <AuthenticationExceptionURI>        /persistence/authentication_exception.html      </AuthenticationExceptionURI>      <AuthenticatedURI>/persistence/authenticated.jsp</AuthenticatedURI>      <UnknownErrorURI>/persistence/error.html</UnknownErrorURI>    </AuthenticationConfig> 

To run the example you should have the following web application set up:

    persistence/                authenticated.jsp                authentication_exception.html                error.html                login.jsp                WEB-INF/                        web.xml                        classes/                                persistence/                                            AuthenticationException.class                                            Authenticator.class                                            Config.class                                            Group.class                                            UknownException.class                                            User.class                                            database/                                                     DatabaseAuthenticator.class                                                     DataSourceConfig.class                                                     DataSourceListener.class                                                     mysql/                                                            MySQLConfig.class                                            servlet/                                                    AuthenticationConfig.class                                                    AuthenticationServlet.class                xml/                    authentication-config.xml                    datasource-config.xml 

We'll learn more about JavaServer Pages (JSP) in Chapter 8. For now, we just present them without comment. First login.jsp:

    <%@ page import="persistence.servlet.AuthenticationServlet" %>    <html>      <head>        <title>Login Page</title>      </head>      <body style="font-family:verdana;font-size:10pt;">        <h3>Login Page</h3>        <form action="servlet/AuthenticationServlet" method="post">          <p>            Login name:            <input name="<%=AuthenticationServlet.USER_NAME%>"><br>            Password:            <input name="<%=AuthenticationServlet.PASSWORD%>" type="password">           </p>           <input type="submit">         </form>      </body>    </html> 

Then authenticated.jsp:

    <%@ page import="persistence.*" %>    <%@ page import="persistence.servlet.*" %>    <html>      <body style="font-family:verdana;font-size:10pt;">        <h3>You have been authenticated</h3>        <%          Group grp = new Group("Finance");          User usr = (User) session.getAttribute(AuthenticationServlet.USER);         %>         <p>You are         <%if (!usr.isInGroup(grp)) {%>           not         <%}%>         a memeber of group <b><%=grp.getName()%></b></p>      </body>    </html> 

We also have a simple HTML page, error.html, that we use to present an error message for general errors:

    <html>     <head>       <title>Error Page</title>     </head>     <body style="font-family:verdana;font-size:10pt;">       <h3>An error occurred</h3>     </body>    </html> 

We have a more specific HTML page, authentication_error.html, to deal with incorrect usernames and passwords:

    <html>     <head>       <title>Error Page</title>     </head>     <body style="font-family:verdana;font-size:10pt;">       <h3>An error occurred: Invalid username or password</h3>     </body>    </html> 

Deploy the application as shown, start Tomcat, and navigate to http://localhost:8080/persistence/login.jsp. You will see the following login page:

click to expand

Enter a Login name of foo and a Password of bar and click on Submit Query:

click to expand

If we enter an incorrect login name or password we see the following page:

click to expand

Accessing Resources Using LDAP

Another common method of storing user information is to use the LDAP directory service. We'll illustrate how to use LDAP in a servlet application, by developing an LDAP implementation of the Authenticator interface.

Installing the LDAP Service Provider

Before you can start using LDAP in your applications, you must install the LDAP service provider extension to your classpath. This extension can be freely downloaded from http://java.sun.com/products/jndi/, as a compressed archive, which contains documentation and two JAR files, provider.jar and ldap.jar. You should extract and copy them to the application server's library path (%CATALINA_HOME%/common/lib for Tomcat). Additionally, you'll need to include these JAR files in your development classpath when compiling the classes.

Implementing the LdapConfig Class

So that we can maintain the configuration for a LDAP server outside of our source code, we again extend our abstract Config class. Our extension, LdapConfig, reads properties from a simple configuration file that contains the name and port of the LDAP server that we will connect to, in addition to the name of the JNDI context factory, which should be set to com.sun.jndi.ldap.LdapCtxFactory. For example:

    <?xml version="1.0" encoding="ISO-8859-1"?>    <LdapConfig>      <Server>ldap.myhost.com</Server>      <Port>389</Port>      <ContextFactory>com.sun.jndi.ldap.LdapCtxFactory</ContextFactory>    </LdapConfig> 

Additionally, our class stores a LDAP-structures string that contains the search base we will use when looking up user credentials from the LDAP server. This search base takes the form:

    dc=[root domain name],dc=[domain extension] 

For example, if we specify ldap.myhost.com as an LDAP server, we would use the following as a search base:

    dc=myhost,dc=com 

The implementation of LdapConfig follows:

    package persistence.ldap;    import persistence.Config;    import java.io.*;    import java.util.StringTokenizer;    import javax.servlet.*;    public class LdapConfig extends Config {      public static final String SERVER = "Server";      public static final String PORT = "Port";      public static final String CONTEXT_FACTORY = "ContextFactory";      private String contextFactory;      private String server;      private String searchBase;      private int port; 

Once initialized, the LdapConfig class invokes its parent init() method. It then reads the properties and constructs the searchBase property:

      public void init(ServletContext sctx, String xmlFile) throws Exception {        super.init(sctx, xmlFile);        server = getElementText(root, SERVER);        contextFactory = getElementText(root, CONTEXT_FACTORY);        try {          port = Integer.parseInt(getElementText(root, PORT));        } catch (NumberFormatException n) {          port = 389;        } 

Resolve the search base:

        StringTokenizer tokenizer = new StringTokenizer(server, ".");        if (tokenizer.countTokens() > 1) {          for (int i=0; i < tokenizer.countTokens()-2; i++) {            tokenizer.nextToken();          }          searchBase = "dc=" + tokenizer.nextToken() + ",dc=" +                       tokenizer.nextToken();        } else {          searchBase = "dc=" + server;        }        cleanup();      }      public String getServer() {        return server;      }      public String getContextFactory() {        return contextFactory;      }      public String getSearchBase() {        return searchBase;      }      public int getPort() {        return port;      }    } 

Implementing the LdapAuthenticator Class

The actual LDAP authenticator class, LdapAuthenticator, looks up the supplied user credentials in the LDAP directory specified by the server name in the configuration file. Each LdapAuthenticator instance stores a reference of the server name, the port it connects to, and a hash table of JNDI properties used each time a user makes a connection:

    package persistence.ldap;    import persistence.*;    import java.util.Hashtable;    import javax.naming.*;    import javax.naming.ldap.*;    import javax.naming.directory.*;    import javax.servlet.ServletContext;    public class LdapAuthenticator implements Authenticator {      private Hashtable env;      private int port;      private String contextFactory;      private String server;      private String searchBase; 

When initialized, LdapAuthenticator constructs a new LdapConfig instance from a context-wide initialization parameter that states the location of the LDAP configuration file:

      public void init(ServletContext sctx) throws Exception {        LdapConfig cfig = new LdapConfig();        cfig.init(sctx, sctx.getInitParameter("LdapConfig"));        server = cfig.getServer();        searchBase = cfig.getSearchBase();        contextFactory = cfig.getContextFactory();        port = cfig.getPort(); 

The init() method also prepares the JNDI properties table that is used when making a connection to the directory:

        env = new Hashtable();        env.put(Context.INITIAL_CONTEXT_FACTORY,contextFactory);        env.put(Context.PROVIDER_URL, "ldap://" + server + ":" + port + "/");        env.put(Context.SECURITY_AUTHENTICATION, "simple");      } 

The authenticate() method attempts to look up a user corresponding to the given user name in the underlying LDAP directory. This method follows the same basic principles as our previous JDBC implementation. If the user is not found, an AuthenticationException is thrown:

      public User authenticate(String username, String password)          throws AuthenticationException, UnknownException {        LdapContext ctx = null;        String login = "cn=" + username + ",cn=users," + searchBase;        try {          env.put(Context.SECURITY_PRINCIPAL, login);          env.put(Context.SECURITY_CREDENTIALS, password);          ctx = new InitialLdapContext(env, null);        } catch (Exception e) {          throw new AuthenticationException();        } 

If we manage to connect to the directory server using the supplied user credentials, then we have already authenticated the user. We then proceed to constructing a new User object and fetching the groups the user is part of:

        try {          User usr = new User(username);          Group grp;          String groupname;          String filter = "(|(uniquemember=" + login + ")(member=" +                          login + "))"; 

Set search attributes:

          String[] attrIDs = {"cn"};          SearchControls ctls = new SearchControls();          ctls.setReturningAttributes(attrIDs);          ctls.setSearchScope(SearchControls.SUBTREE_SCOPE); 

Search for the groups the user is member of:

          Attribute attr;          NamingEnumeration results = ctx.search(searchBase, filter, ctls);          NamingEnumeration ne;          SearchResult sr;          int i = 0;          while (results.hasMoreElements()) {            sr = (SearchResult) results.next();            ne = sr.getAttributes().getAll();            attr = ((Attribute) ne.next());            if (ne.hasMore()) {              groupname = (String) attr.getAll().next();              usr.addGroup(new Group(groupname));            }          }          return usr;        } catch (Exception e) {          throw new UnknownException(e); 

We shut down all external resources at the end of the call:

        } finally {          if (ctx != null) {            try {              ctx.close();            } catch (Exception ignored) {}          }        }      }    } 

As with the DatabaseAuthenticator, we can use the LdapAuthenticator in an application by altering the value of the <Authenticator> element of the application's configuration file. We also need to add a single initialization parameter to the application's deployment descriptor, specifying the location of the LDAP configuration file:

    <context-param>      <param-name>LdapConfigXML</param-name>      <param-value>/xml/ldap-config.xml</param-value>    </context-param> 



 < Free Open Study > 



Professional Java Servlets 2.3
Professional Java Servlets 2.3
ISBN: 186100561X
EAN: 2147483647
Year: 2006
Pages: 130

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net