[ Team LiB ] Previous Section Next Section

13.2 Application-Controlled Authentication

Using one of the container-provided mechanisms described in the previous section should be your first choice for authentication. But, by definition, being container-provided means the application cannot dynamically add new users and roles to control who is granted access, at least not through a standard API defined by the servlet and JSP specifications.

For some types of applications, it's critical to have a very dynamic authentication model; one that doesn't require an administrator to define access rules before a new user can join the party. I'm sure you have seen countless sites where you can sign up for access to restricted content simply by filling out a form. One example is a project management site, where registered users can access document archives, discussion groups, calendars, and other tools for distributed cooperation. Another example is a personalized news site that you can customize to show news only about things you care about.

Unless you can define new users programmatically in the database used by an external authentication mechanism, you need to roll your own authentication and access-control system for these types of applications. In this section, we'll look at the principles for how to do this. Note that this approach sends the user's password as clear text, so it has the same security issues as the container-provided basic and form-based authentication methods.

Application-controlled authentication and access control requires the following pieces:

  • User registration

  • A login page

  • The authentication mechanism, invoked by the login page

  • Information saved in the session scope as proof of successful authentication

  • Validation of the session information in all JSP pages requiring restricted access

We'll reuse the example from Chapter 12 for user registration; this allows us to focus on the parts of an application that require access control. The application is a simple billboard service, where employees can post messages related to different projects they are involved with. An employee can customize the application to show messages only about the projects he is interested in. Figure 13-1 shows all the pages and how they are related.

Figure 13-1. Application with authentication and access control
figs/Jsp3_1301.gif

Let's go over it step by step. The login.jsp page is our login page. It contains a form that invokes the authenticate.jsp page, where the username and password are compared to the information in the employee information database created in Chapter 12. If a matching user is found, the autheticate.jsp page creates an EmployeeBean object and saves it in the session scope. This bean serves as proof of authentication. It then redirects the client to a true application page. The page the user is redirected to depends on whether the user loaded the login.jsp page or tried to directly access an application page, without first logging in. All application pages, specifically main.jsp, entermsg.jsp, storemsg.jsp, and updateprofile.jsp, look for the EmployeeBean object and forward to the login.jsp page if it's not found which forces the user to log in. When the login.jsp page is loaded this way, it keeps track of the page the user tried to access so it can be displayed automatically after successful authentication. Finally, there's the logout.jsp page. This page can be invoked from a link in the main.jsp page. It simply terminates the session and redirects to the login.jsp page.

13.2.1 A Table for Personalized Information

Since the sample application in this chapter lets the user personalize the content of the billboard, we need a database table to store information about each employee's choices. The new table is shown in Table 13-2.

Table 13-2. EmployeeProjects database table

Column name

SQL data type

Primary key?

UserName
CHAR (text)

Yes

ProjectName
CHAR (text)

Yes

The table holds one row per unique user-project combination. You need to create this table in your database before you can run the example.

13.2.2 Logging In

The login page contains an HTML form with fields for entering the user credentials: a username and a password. This is why the information was included in the Employee table in Chapter 12. Example 13-2 shows the complete login.jsp page.

Example 13-2. Login page (login.jsp)
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
  
<html>
  <head>
    <title>Project Billboard</title>
  </head>
  <body bgcolor="white">
    <h1>Welcome to the Project Billboard</h1>
    Your personalized project news web site.
    <p>
    <font color="red">
      ${fn:escapeXml(param.errorMsg)}
    </font>
  
    <form action="authenticate.jsp" method="post">
  
      <input type="hidden" name="origURL" 
        value="${fn:escapeXml(param.origURL)}">
  
      Please enter your User Name and Password, and click Enter.
      <p>
      Name: 
      <input name="userName" 
        value="${fn:escapeXml(cookie.userName.value)}" 
        size="10">
      Password: 
      <input type="password" name="password" 
        value="${fn:escapeXml(cookie.password.value)}" 
        size="10">
      <input type="submit" value="Enter">
      <p>
      Remember my name and password:
      <input type="checkbox" name="remember"
        ${!empty cookie.userName ? 'checked' : ''}>
      <br>
      (This feature requires cookies to be enabled in your browser)
    </form>
  </body>
</html>

The form contains the fields for the username and password, and the action attribute is set to the authenticate.jsp page as expected. However, it also contains EL expressions that may need an explanation.

The following fragment displays a message that gives a hint as to why the login page is shown after an error:

<font color="red">
  ${fn:escapeXml(param.errorMsg)}
</font>

The errorMsg request parameter may contain an error message, set by the other pages when they forward to the login page, as you will soon see. If so, the EL expression displays the message. When the user loads the login.jsp page directly, the parameter is not available in the request, so nothing is added to the response. Figure 13-2 shows an example of the login page with an error message.

Figure 13-2. Login page with error message
figs/Jsp3_1302.gif

Within the form, you find similar EL expressions:

<input type="hidden" name="origURL" 
  value="${fn:escapeXml(param.origURL)}">

Here, a hidden form field is set to the value of the originally requested URL. The value is passed as a parameter to the login page when another page forwards to it. This is how to keep track of which page the user wasn't allowed access to because he wasn't authenticated yet. Later you'll see how this information is used to load the originally requested page after authentication.

13.2.2.1 Using cookies to remember the username and password

The more web applications with restricted access a web surfer uses, the more usernames and passwords to remember. After a while, it may be tempting to resort to the greatest security sin of all: writing down all usernames and passwords in a file such as mypasswords.txt. This invites anyone with access to the user's computer to roam around in all the secret data.

It can be a big problem keeping track of all accounts. Some sites therefore offer to keep track of the username and password using cookies. Cookies, as you probably remember, are small pieces of text a server sends to the browser. A cookie with an expiration date is saved on the hard disk and is returned to the server every time the user visits the same site until the cookie expires. So is this feature a good thing? Not really, as it amounts to the same security risk as writing down the username and password in a file. Even greater, since anyone with access to the user's computer doesn't even have to find the mypasswords.txt file; the browser takes care of sending the credentials automatically. But for sites that use authentication mainly to provide personalization and don't contain sensitive data, using cookies can be an appreciated tool.

This example shows how it can be done. If you decide to use it, make sure you make it optional so the user can opt out. As you may recall from Chapter 8, all cookies can be read using the cookies property of the request object available through the implicit pageContext variable. When you know the name of the cookie you're looking for, it's easier to use the implicit cookie variable. This variable contains a collection of javax.servlet.http.Cookie objects, which can be used as beans with the properties name and value. The value property is used in Example 13-2 to set the value of the input fields for the username and the password to the values received as cookies:

Name: 
<input name="userName" 
  value="${fn:escapeXml(cookie.userName.value)}" 
  size="10">
Password: 
<input type="password" name="password" 
  value="${fn:escapeXml(cookie.password.value)}" 
  size="10">

The last part of the form creates a checkbox that lets the user decide if cookies should be used or not. An EL expression with the conditional operator tests if one of the cookies is available and adds the checked attribute for the checkbox if it is:

Remember my name and password:
<input type="checkbox" name="remember"
  ${!empty cookie.userName ? 'checked' : ''}>

This snippet means that a user who has previously opted for cookie-based tracking gets the checkbox checked but a first time user doesn't. It's a good strategy, because it forces the user to "opt in."

13.2.3 Authentication Using a Database

To authenticate a user, you need access to information about the registered users. For the sample application in this chapter, all user information is kept in a database. There are other options, including flat files and LDAP directories. When a user fills out the login page form and clicks Enter, the authentication page shown in Example 13-3 is processed. This is a large page, so each part is discussed in detail after the complete page.

Example 13-3. Authentication page (authenticate.jsp)
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="sql" uri="http://java.sun.com/jsp/jstl/sql" %>
<%@ taglib prefix="ora" uri="orataglib" %>
  
<%-- Remove the validUser session bean, if any --%>
<c:remove var="validUser" />
 
<c:if test="${empty param.userName || empty param.password}">
  <c:redirect url="login.jsp" >
    <c:param name="errorMsg" 
      value="You must enter a User Name and Password." />
  </c:redirect>
</c:if>
  
<%-- 
  See if the user name and password combination is valid. If not,
  redirect back to the login page with a message.
--%>
<sql:query var="empInfo">
  SELECT * FROM Employee 
    WHERE UserName = ? AND Password = ?
  <sql:param value="${param.userName}" />
  <sql:param value="${param.password}" />
</sql:query>
  
<c:if test="${empInfo.rowCount == 0}">
  <c:redirect url="login.jsp" >
    <c:param name="errorMsg" 
      value="The User Name or Password you entered is not valid." />
  </c:redirect>
</c:if>
  
<%--    
  Create an EmployeeBean and save it in 
  the session scope and redirect to the appropriate page.
--%>
<c:set var="dbValues" value="${empInfo.rows[0]}" />
<jsp:useBean id="validUser" scope="session"
  class="com.ora.jsp.beans.emp.EmployeeBean" >
  <c:set target="${validUser}" property="userName" 
    value="${dbValues.UserName}" />
  <c:set target="${validUser}" property="firstName" 
    value="${dbValues.FirstName}" />
  <c:set target="${validUser}" property="lastName" 
    value="${dbValues.LastName}" />
  <c:set target="${validUser}" property="dept" 
    value="${dbValues.Dept}" />
  <c:set target="${validUser}" property="empDate" 
    value="${dbValues.EmpDate}" />
  <c:set target="${validUser}" property="emailAddr" 
    value="${dbValues.EmailAddr}" />
</jsp:useBean>
  
<%-- Add the projects --%>
<sql:query var="empProjects">
  SELECT * FROM EmployeeProjects 
    WHERE UserName = ?
  <sql:param value="${param.userName}" />
</sql:query>
  
<c:forEach items="${empProjects.rows}" var="project">
  <c:set target="${validUser}" property="project" 
     value="${project.ProjectName}" />
</c:forEach>
  
<c:choose>
  <c:when test="${!empty param.remember}">
    <ora:addCookie name="userName" 
      value="${param.userName}"
      maxAge="2592000" />
    <ora:addCookie name="password" 
      value="${param.password}"
      maxAge="2592000" />
  </c:when>
  <c:otherwise>
    <ora:addCookie name="userName" 
      value="${param.userName}"
      maxAge="0" />
    <ora:addCookie name="password" 
      value="${param.password}"
      maxAge="0" />
  </c:otherwise>
</c:choose>
   
<%-- 
  Redirect to the main page or to the original URL, if
  invoked as a result of a access attempt to a protected
  page.
--%>
<c:choose>
  <c:when test="${!empty param.origURL}">
    <c:redirect url="${param.origURL}" />
  </c:when>
  <c:otherwise>
    <c:redirect url="main.jsp" />
  </c:otherwise>
</c:choose>

The first thing that happens in Example 13-3 is that the JSTL <c:remove> action (Table 13-3) removes a session scope variable named validUser, if it exists.

This variable holds an EmployeeBean object, and its presence in the session scope indicates that the corresponding user has logged in successfully. If an EmployeeBean object is already present in the session scope, it may represent a user that forgot to log out, so it's important to remove it when a new login attempt is made.

Table 13-3. Attributes for JSTL <c:remove>

Attribute name

Java type

Dynamic value accepted

Description

var
String

No

Mandatory. The name of the variable to remove.

scope
String

No

Optional. The scope where the variable shall be removed, one of page, request, session, or application. Default is to remove the variable from the first scope where it's found.

Next, a <c:if> action makes sure that both the username and the password parameters are received. If one or both parameters are missing, the <c:redirect> action redirects back to the login page again. Here you see how the errorMsg parameter used in the login.page gets its value.

If the request contains both parameters, the <sql:query> action introduced in Chapter 12 checks for a user with the specified name and password in the database:

<sql:query var="empInfo">
  SELECT * FROM Employee 
    WHERE UserName = ? AND Password = ?
  <sql:param value="${param.userName}" />
  <sql:param value="${param.password}" />
</sql:query>
  
<c:if test="${empInfo.rowCount == 0}">
  <c:redirect url="login.jsp" >
    <c:param name="errorMsg" 
      value="The User Name or Password you entered is not valid." />
  </c:redirect>
</c:if>

If the query doesn't match a registered user (i.e., empInfo.rowCount is 0), the <c:redirect> action redirects back to the login page with an appropriate error message. Otherwise, the processing continues.

13.2.3.1 Creating the validation object

If a match is found, the single row from the query result is extracted, and the column values are used to populate the single value properties of an EmployeeBean object. The EmployeeBean has the properties shown in Table 13-4.

Table 13-4. Properties for com.ora.jsp.beans.emp.EmployeeBean

Property name

Java type

Access

Description

username
String

Read/write

The employee's unique username

firstName
String

Read/write

The employee's first name

lastName
String

Read/write

The employee's last name

dept
String

Read/write

The employee's department name

empDate
java.util.Date

Read/write

The employee's employment date

emailAddr
String

Read/write

The employee's email address

projects
String[]

Read/write

A list of all projects the employee is involved in

project
String

Write

The value is added to the list of projects

The bean is named validUser and placed in the session scope using the standard <jsp:useBean> action. The first (and only) row in the database result is saved in a variable named dbValues, which makes it easier to access the individual column values. All bean properties are then set to the values returned from the database using the JSTL <c:set> action:

<c:set var="dbValues" value="${empInfo.rows[0]}" />
<jsp:useBean id="validUser" scope="session"
  class="com.ora.jsp.beans.emp.EmployeeBean" >
  <c:set target="${validUser}" property="userName" 
    value="${dbValues.UserName}" />
  <c:set target="${validUser}" property="firstName" 
    value="${dbValues.FirstName}" />
  <c:set target="${validUser}" property="lastName" 
    value="${dbValues.LastName}" />
  <c:set target="${validUser}" property="dept" 
    value="${dbValues.Dept}" />
  <c:set target="${validUser}" property="empDate" 
    value="${dbValues.EmpDate}" />
  <c:set target="${validUser}" property="emailAddr" 
    value="${dbValues.EmailAddr}" />
</jsp:useBean>

As I mentioned earlier, this application lets the user select the projects she is interested in, so that only messages related to these projects are shown on the main page. The user's choices are stored in the EmployeeProjects database table described in Table 13-2. In the authenticate.jsp page, the projects for the current user are retrieved from the EmployeeProjects table and used to create the corresponding list in the bean:

<sql:query var="empProjects">
  SELECT * FROM EmployeeProjects 
    WHERE UserName = ?
  <sql:param value="${param.userName}" />
</sql:query>
  
<c:forEach items="${empProjects.rows}" var="project">
  <c:set target="${validUser}" property="project" 
     value="${project.ProjectName}" />
</c:forEach>

To populate the bean, a <c:set> action invokes the bean's project property setter method once for each row in the query result. The property setter method adds each value to the list of projects held by the bean.

13.2.3.2 Setting and deleting cookies

If the user asks for the user credentials to be remembered, we need to send the corresponding cookies to the browser. The checkbox value from the login page is sent to the authentication page as a parameter named remember:

<c:choose>
  <c:when test="${!empty param.remember}">
    <ora:addCookie name="userName" 
      value="${param.userName}"
      maxAge="2592000" />
    <ora:addCookie name="password" 
      value="${param.password}"
      maxAge="2592000" />
  </c:when>
  <c:otherwise>
    <ora:addCookie name="userName" 
      value="${param.userName}"
      maxAge="0" />
    <ora:addCookie name="password" 
      value="${param.password}"
      maxAge="0" />
  </c:otherwise>
</c:choose>

The <ora:addCookie> custom action (Table 13-5) sends cookies to the browser. If the remember parameter is received, the cookies are sent with a maximum age value representing 30 days, expressed in seconds (2592000). As long as the user returns to this site within this time frame, the cookies will be sent with the request, and the login page will use the values to automatically fill out the form fields. If the user decides not to use this feature and unchecks the box, the cookies are still sent but with a maximum age of 0. This means that the cookies expire immediately and will never be sent to this server again. If you want to send a cookie to a browser that should be valid only until the user closes the browser, set the maximum age to a negative number (for instance, -1).

Table 13-5. Attributes for <ora:addCookie>

Attribute name

Java type

Dynamic value accepted

Description

name
String

Yes

Mandatory. The name of the cookie.

value
String

Yes

Mandatory. The cookie value.

maxAge
int

Yes

Optional. The number of seconds the cookie shall persist in the browser. Default is -1, causing the cookie to persist until the browser is closed.

13.2.3.3 Redirect to the application page

The only thing left is to redirect the browser to the appropriate page. If the authentication process is started as a result of the user requesting a protected page without being logged in, the original URL is received from the login page as the value of the origURL parameter:

<c:choose>
  <c:when test="${!empty param.origURL}">
    <c:redirect url="${param.origURL}" />
  </c:when>
  <c:otherwise>
    <c:redirect url="main.jsp" />
  </c:otherwise>
</c:choose>

If this parameter has a value, the browser is redirected to the originally requested page, otherwise to the main entry page for the application.

13.2.4 Checking for a Valid Session

Authentication is only half of the solution. We must also add access control to each page in the application. Example 13-4 shows the main.jsp page as an example of a protected page. This page shows all messages for the projects of the user's choice. It also has a form with which the user can change the list of projects of interest and links to a page for posting new messages, and to log out.

Example 13-4. A protected JSP page (main.jsp)
<%@ page contentType="text/html" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
  
<%-- Verify that the user is logged in --%>
<c:if test="${validUser == null}">
  <jsp:forward page="login.jsp">
    <jsp:param name="origURL" value="${pageContext.request.requestURL}" />
    <jsp:param name="errorMsg" value="Please log in first." />
  </jsp:forward>
</c:if>
<html>
  <head>
    <title>Project Billboard</title>
  </head>
  <body bgcolor="white">
  
    <h1>Welcome ${fn:escapeXml(validUser.firstName)}</h1>
    Your profile currently shows you like information about the 
    following checked-off projects. If you like to update your
    profile, make the appropriate changes below and click 
    Update Profile.
    <form action="updateprofile.jsp" method="post">
  
      <c:forEach items="${validUser.projects}" var="current">
        <c:choose>
          <c:when test="${current == 'JSP'}"> 
            <c:set var="jspSelected" value="true" />
          </c:when>
          <c:when test="${current == 'Servlet'}"> 
            <c:set var="servletSelected" value="true" />
          </c:when>
          <c:when test="${current == 'EJB'}"> 
            <c:set var="ejbSelected" value="true" />
          </c:when>
        </c:choose>
      </c:forEach>
      <input type="checkbox" name="projects" value="JSP"
        ${jspSelected ? 'checked' : ''}>JSP<br>
      <input type="checkbox" name="projects" value="Servlet"
        ${servletSelected ? 'checked' : ''}>Servlet<br>
      <input type="checkbox" name="projects" value="EJB"
        ${ejbSelected} ? 'checked' : ''}>EJB<br>
      <input type="submit" value="Update Profile">
    </form>
    <hr>
  
    When you're done reading the news, please <a href="logout.jsp">
    log out</a>.
  
    <hr>
    <a href="entermsg.jsp">Post a new message</a>
    <p>
  
    <%-- Get all new items --%>
    <jsp:useBean id="news" scope="application"
      class="com.ora.jsp.beans.news.NewsBean" />
    <c:set var="newsItems" value="${news.newsItems}" />
  
    <%--
       Loop through all user projects and for each, loop through 
       all news items and display the ones that match the current 
       project.
    --%>
    <table>
      <c:forEach items="${validUser.projects}" var="projectName">
        <tr>
          <td colspan="2">
            <b>Project: ${fn:escapeXml(projectName)}</b>
          </td>
        </tr>
        <c:forEach items="${newsItems}" var="newsItem">
          <c:if test="${newsItem.category == projectName}">
            <tr>
              <td>
                ${fn:escapeXml(newsItem.postedBy)}
              </td>
              <td>
                ${fn:escapeXml(newsItem.postedDate)}
              </td>
            </tr>
            <tr>
              <td colspan="2">
                ${fn:escapeXml(newsItem.msg)}
              </td>
            </tr>
            <tr>
              <td colspan="2"><hr></td>
            </tr>
          </c:if>
        </c:forEach>
      </c:forEach>
    </table>
  </body>
</html>

Here's the most interesting piece of Example 13-4, from an access-control point of view:

<c:if test="${validUser == null}">
  <jsp:forward page="login.jsp">
    <jsp:param name="origURL" value="${pageContext.request.requestURL}" />
    <jsp:param name="errorMsg" value="Please log in first." />
  </jsp:forward>
</c:if>

The proof that a successfully authenticated user requests the page is that there's an EmployeeBean available under the name validUser in the session scope; the authenticate.jsp page places it there only if the username and password are valid. The <c:if> action is used to verify this. If the bean is not found, the request is forwarded to the login page, with the origURL and errorMsg parameters added.

13.2.4.1 Providing personalized content

The rest of the page shown in Example 13-4 produces a personalized page for the authenticated user. Figure 13-3 shows an example of how it may look for one user.

Figure 13-3. Personalized application page
figs/Jsp3_1303.gif

First, the validUser bean properties welcome the user to the site by name. Next comes a form with checkboxes for all projects. The same technique that was used in Chapter 8 is also used here to set the checked attribute for the projects listed in the user's profile. The user can modify the list of projects and click Update Profile to invoke the updateprofile.jsp page. This page modifies the profile information in the database. We'll take a look at how it's done later.

A NewsBean containing NewsItemBean objects then displays news items for all projects matching the user's profile. The implementations of these beans are intended only as examples. Initially, the NewsBean contains one hardcoded message for each news category, and the news items are kept in memory only. A real implementation would likely store all news items permanently in a database.

Example 13-4 also contains a link to a page where a news item can be posted to the list. If you look at the source for the entermsg.jsp file, you'll see that it's just a JSP page with the same access-control test at the top as in Example 13-4 and a regular HTML form that invokes the storemsg.jsp file with a POST request. The POST method is appropriate here, since the form fields update information on the server (the in-memory NewsBean database).

The storemsg.jsp page is shown in Example 13-5.

Example 13-5. POST page with restricted access (storemsg.jsp)
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
  
<%-- Verify that the user is logged in --%>
<c:if test="${validUser == null}">
  <jsp:forward page="login.jsp">
    <jsp:param name="origURL" value="${pageContext.request.requestURL}" />
    <jsp:param name="errorMsg" value="Please log in first." />
  </jsp:forward> 
</c:if>
<%-- Verify that it's a POST method --%>
<c:if test="${pageContext.request.method != 'POST'}">
  <c:redirect url="main.jsp" />
</c:if>
<%-- Create a new news item bean with the submitted info --%>
<jsp:useBean id="newsItem" class="com.ora.jsp.beans.news.NewsItemBean" >
  <jsp:setProperty name="newsItem" property="*" />
  <c:set target="${newsItem}" property="postedBy" 
    value="${validUser.firstName} ${validUser.lastName}" />
</jsp:useBean>
  
<%-- Add the new news item bean to the list --%>
<c:set target="${news}" property="newsItem" 
  value="${newsItem}" />
  
<c:redirect url="main.jsp" />

This page creates a new NewsItemBean and sets all properties based on the field parameters passed from the entermsg.jsp page, plus the postedBy property using the firstName and lastName properties of the validUser bean. It then adds the new news item to the NewsBean with the <c:set> action and redirects to the main page, where the new item is displayed along with the old ones.

Let's focus on the access-control aspects. At the top of the page, you find the same access-control logic as in all other protected pages. If a user fills out the form in entermsg.jsp and walks away from the computer without submitting the form, the session may time out. When the user then returns and clicks Submit, the validUser bean is not found in the session. The body of the <c:if> action is therefore processed, forwarding the request to the login page with the origURL parameter set to the URL of the storemsg.jsp. After successful authentication, the authentication page redirects to the original URL, the storemsg.jsp. However, a redirect is always a GET request.[1] All the parameters sent with the original POST request for storemsg.jsp are lost; a POST request carries the parameter values in the message body, instead of in the URL (as a query string) as a GET request does. This mean the original URL saved by the login.jsp page doesn't include the parameters. If you don't take care of this special case, an empty NewsItemBean is added to the list.

[1] The HTTP specification (RFC 2616) states that a browser is not allowed to change the method for the request when it receives a redirect response (status code 302). But, as acknowledged by HTTP specification, all major browsers available today change a POST request to a GET anyway.

There are at least two ways to deal with this. In Example 13-5, the access-control logic is followed by a <c:if> action checking that the request for this page is a POST request. If not, it redirects to the main page without processing the request. This is the easiest way to deal with the problem, but it also means that the user will have to retype the message again. The chance that a session times out before a form is submitted is small, so in most cases this is not a big deal; it's therefore the solution I recommend.

If you absolutely must find a way to not lose the POST parameters when a session times out, here is a brief outline of a solution:

  • Use a <c:forEach> action in the login page to loop through all POST parameter values and save them as hidden fields in the form, along with a hidden field that tells if the original request was a GET or a POST request.

  • In the authentication page, forward to the originally requested URL if the method was a POST and redirect only if it was a GET. The authentication page is always invoked as a POST request. A forward is just a way to let another page continue to process the same request, so the originally requested page will be invoked with a POST request as it expects, along with all the originally submitted parameters saved as hidden fields in the login page.

Depending on your application, you may also need to save session data as hidden fields in the page that submits the POST request, so that the requested page doesn't have to rely on session information. But this leads to another problem. What if someone other than the user who filled out the form comes along and submits it? Information will then be updated on the server with information submitted by a user that's no longer logged in. One way out of this is to also save information about the current user as a hidden field in the form that sends the POST request and let the authentication page compare this information with information about the new user. If they don't match, the client can be redirected to the main application page instead of forwarded to the originally requested URL.

As you can see, there are a number of things to think about. Whether it makes sense to take care of all the issues depends on the application. My general advice is to keep it simple and stick to the first solution unless your application warrants a more complex approach.

13.2.5 Updating the User Profile

The updateprofile.jsp page, used if the user makes new project selections in the main page and clicks Update Profile, is also invoked through the POST method. It follows the same access-control approach as the storemsg.jsp page and is shown in Example 13-6. But what's more interesting with this page is that it shows how to replace multivalue bean and database data, and is an instance of when you need to care about database transactions.

Example 13-6. Updating multiple database rows (updateprofile.jsp)
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="sql" uri="http://java.sun.com/jsp/jstl/sql" %>
  
<%-- Verify that the user is logged in --%>
<c:if test="${validUser == null}">
  <jsp:forward page="login.jsp">
    <jsp:param name="origURL" value="${pageContext.request.requestURL}" />
    <jsp:param name="errorMsg" value="Please log in first." />
  </jsp:forward> 
</c:if>
  
<%-- Verify that it's a POST method --%>
<c:if test="${pageContext.request.method != 'POST'}">
  <c:redirect url="main.jsp" />
</c:if>
  
<%-- Update the project list in the bean --%>
<c:set target="${validUser}" property="projects"
  value="${paramValues.projects}" />
  
<sql:transaction>
  <%-- Delete the old project (if any) and insert the new ones --%>
  <sql:update>
    DELETE FROM EmployeeProjects
      WHERE UserName = ?
    <sql:param value="${validUser.userName}" />
  </sql:update>
  <c:forEach items="${validUser.projects}" var="project">
    <sql:update>
      INSERT INTO EmployeeProjects
        (UserName, ProjectName) VALUES(?, ?)
      <sql:param value="${validUser.userName}" />
      <sql:param value="${project}" />
    </sql:update>
  </c:forEach>
</sql:transaction>
<%-- Redirect to main page --%>
<c:redirect url="main.jsp" />

The list of new projects selected by the user is sent to the updateprofile.jsp page as the projects request parameter, with one value per checked checkbox. The projects bean property is updated using the <c:set> action with the value of an EL expression that returns all values of the parameter as an array (note that the paramValues implicit variable is used, as opposed to the param variable). The data type for the projects property is String[], meaning it can be set to an array of strings.

If the user deselects all checkboxes in the main.jsp page (Example 13-4), all projects must be removed from the bean as well. Using the <c:set> action takes care of this requirement. When no checkbox is selected, the projects request parameter is not sent, and the EL expression returns null, clearing the project list property value.

The EmployeeProjects table (Table 13-1) contains one row per project for a user, with the username in the UserName column and the project name in the ProjectName column. The easiest way to update the database information is to first delete all existing rows, if any, and then insert rows for the new projects selected by the user. Because this requires execution of multiple SQL statements, and all must either succeed or fail, the <sql:update> actions are placed within the body of a <sql:transaction> action element. If the first <sql:update> action is successful but one of the others fails, the database information deleted by the first is restored so the database correctly reflects the state before the change.

To delete the rows in the database, the <sql:update> action is used with an SQL DELETE statement. The WHERE clause condition restricts the statement so that only the rows for the current user are deleted. The <c:forEach> action then loops through all projects registered in the validUser bean. The body of the <c:forEach> action contains an <sql:update> action that executes an INSERT statement for each project:

<c:forEach items="${validUser.projects}" var="project">
  <sql:update>
    INSERT INTO EmployeeProjects
      (UserName, ProjectName) VALUES(?, ?)
    <sql:param value="${validUser.userName}" />
    <sql:param value="${project}" />
  </sql:update>
</c:forEach>

Within the action element's body, the <sql:param> action sets the value for the ProjectName column to the current iteration value; a new value is used for each pass through the projects property array. The UserName column has the same value in each row, so the <sql:param> action always sets it to the validUser bean's userName property value.

13.2.6 Logging Out

Because the proof of authentication is kept in the session scope, the user is automatically logged out when the session times out. Even so, an application that requires authentication should always provide a way for the user to explicitly log out. This way a user can be sure that if he leaves the desk, no one else can come by and use the application.

The main page in the example application contains a link to the logout page, shown in Example 13-7.

Example 13-7. Logout page (logout.jsp)
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="ora" uri="orataglib" %>
  
<%-- 
  Terminate the session and redirect to the login page.
--%>
<ora:invalidateSession/>
  
<c:redirect url="login.jsp" />

This page explicitly terminates the session using the <ora:invalidateSession> custom action (no attributes supported) and then redirects back to the login page. Invalidating the session means that all session scope variables are removed, and the session is marked as invalid. The next time someone logs in, a new session is created.

The <ora:invalidateSession> custom action implementation is very simple and arguable overkill. If you don't mind using JSP scripting elements (described in Chapter 16) in your pages, this scriptlet is an alternative to using the custom action:

<% session.invalidate(  ); %>

If you want to test the sample application described in this chapter, you must first create at least one user with the example application developed in Chapter 12. To see how the automatic redirect to the originally requested page works, you can open two browser windows and log in from both. They both share the same session (assuming cookies are enabled), so if you log out using one window and then try to load the "post a new message" page with the other, you are redirected to the login page. After you enter your username and password, you're redirected to the page for posting a message.

    [ Team LiB ] Previous Section Next Section