Dr. Dobb's Journal May 2000
The HyperText Transfer Protocol (HTTP) is the backbone of the Web, providing the foundation on which it was built. The growth of the Web has brought with it great benefits to the computing community that would not have been possible without this protocol. For fetching simple web pages and images, HTTP is ideal. But for interactive content generated by server-side applications, HTTP raises a few problems.
HTTP is a connectionless protocol, meaning that it does not maintain a persistent connection for its requests. Every request from a browser is made using a new connection, so there is no easy way to distinguish one request from another. You can say that HTTP is a stateless protocol, as state information is not preserved from one request to the next. Other network protocols establish a single connection, through which multiple requests are made. For example, when checking e-mail using POP3 protocol, a single connection is made (see Figure 1). Each message is requested, then the connection is terminated. This makes it simple for the e-mail server to know which mailbox is being accessed each time a message is read. As Figure 2 illustrates, HTTP requests require a separate TCP connection.
This creates a problem for developers. Server-side applications are started by a web server, in response to requests made from a web browser. To provide an interactive experience customized for each user, there must be some way of maintaining state information across subsequent requests. Fortunately, developers have found solutions to overcome the shortcomings of HTTP, including hidden parameters in HTML forms, hyperlinks, and the use of cookies. Developers writing Java Servlets (server-side applications written in Java) can use these techniques to deploy simple web applications. However, the Java Servlet API also offers sophisticated session-management features that take this approach one step further. Rather than just storing simple state data, you can also establish and track user sessions. In this article, I'll examine all three approaches. Also, complete source code for state- and session-tracking programs is available electronically; see "Resource Center," page 5.
CGI is a mechanism for passing parameters between a web browser and server. HTML forms, for example, can include text fields, radio buttons, and checkboxes, which are passed to the server as parameters. While browser users are most often the source of this information, extra information can be included by a server-side application. The simplest way to preserve state information between requests is to store it as CGI parameters, so that when the next request is made, the data is echoed back to the server.
There are two ways this can be achieved. First, hidden parameters can be placed in HTML forms. The reader will most likely be familiar with the most common of HTML form elements, but may not have encountered hidden parameters. A hidden parameter is a parameter with a value that is fixed and that cannot be modified by users. Hidden parameters do not appear as a form of input, but are sent like a normal CGI parameter. A Servlet can specify the value of a hidden parameter when the form is sent to the browser. When submitted, the state information is sent back to the Servlet; see Example 1.
The second method is to encode state information in hyperlinks. If only forms contained the state information, and a user clicked on a hyperlink, the state information would be lost. For this reason, Servlets should encode state information in the URL of every hyperlink. No matter which choice is made in Example 2, for instance, the state information (in this case, the order number and action type) is preserved. This is a simple technique, but must be applied to every URL and HTML form.
To overcome the limitations (and frustrations) involving embedding state information in requests as CGI parameters, Netscape developed "cookies" -- small pieces of information created by a server-side application that are stored by a web browser. Whenever requests are made by browsers, the cookies are attached to them, so that the data can be read by web servers. Unlike CGI parameters (which must be placed in each and every hyperlink or HTML form), cookies are sent automatically, subject to a few security restrictions.
Cookies, in the Servlet API, are represented by the javax.servlet.http.Cookie class. Creating a cookie is easy -- you simply declare a new instance of Cookie, and pass to the constructor a unique name and data value. Both the cookie name and cookie value must be a String.
// Declare a new instance of Cookie
Cookie a_cookie = new Cookie ("customer_name", "John Smith");
When Servlets need to issue new cookies, they attach them to the Servlet response, using the HttpServletResponse.addCookie(Cookie) method. Listing One is a Servlet that stores a user's name and e-mail address in a cookie. Access to cookies is provided through the HttpServletRequest.getCookies() method, which returns an array of cookies. Unfortunately, there isn't a method that produces a specific cookie -- the Servlet must search through a list of cookies for a particular name. Listing Two shows how to do this. It reads any cookie data stored by Listing One. The Servlet must search through every cookie, looking for a particular name. There also exists the possibility that no such cookie is found (either it was not stored or the browser is not retaining cookies), so a check is made.
Cookies are an excellent way of storing simple state information. For example, user preferences could be stored in a cookie, so that Servlets could customize the look and feel of HTML output, or the type of content that is sent. A news site could read a list of subject categories from a cookie, and display targeted content. When the cookie contains long-lasting data, an expiration date must be specified by the Servlet. Without this date, the cookie will be discarded automatically when the browser exists. Expiration dates are specified using Cookie.setMaxAge(int), which takes an integer as a parameter. This represents the number of seconds into the future the cookie should be kept. For example:
// Keep information for three weeks (21 days)
Cookie cookie = new Cookie ("info_type", "computing");
cookie.setMaxAge( 60 * 60 * 24 * 21 );
Rewriting URLs and HTML forms to include state data makes for longer pages and extra work. While good for trivial information, such as user preferences, they also hold a great danger when used incorrectly. It presents a potential security risk if any form of identifier that grants access to resources is stored in this way (for example, an account ID). One of the disadvantages with preserving state information in a web page is that anyone (either deliberately, or inadvertently) who comes across the HTML page can gain access. This is a common fault with proxy servers, which often cache pages generated by server-side applications. (Imagine if someone could gain access to sensitive data or a user account simply because an identifier was cached by an intermediary proxy server.)
Cookies also have their limitations. While easier for you to work with (since state data is sent on every request), they hold a similar security risk. If an account ID or a username/password combination is stored in a cookie, then anyone using the web browser could access Servlet content. Even if the cookie is temporary and expires when the browser exits, forgetful users can leave browsers open, only to find their account accessed by a third party. Not every browser supports cookies, and not every user will have cookie support enabled.
A better solution, which requires less work on your part, is to establish a virtual connection between the browser and Servlet. Each HTTP request is associated with a session, and contains a session identifier that allows requests to be tracked. The session identifier is valid for a limited time -- so even if it is cached by a proxy server, it will be of little value. Other identifiers, such as a username or ID number, aren't stored in a cookie or HTML page, which offers better security.
While you could roll your own session-tracking mechanism, the Servlet API provides a simple yet powerful implementation. The HttpSession class gives you all you need to track sessions in their Servlets. Each user is mapped to a particular session, which is represented by an HttpSession instance. When a browser makes an HTTP request, a session identifier is presented, and the Servlet gains access to the correct session. Session identifiers can be included in HTML output from the server or as a cookie. Figure 3 illustrates the interaction between browser and Servlet.
Sessions can be used as a repository for state data. In fact, using HttpSession is much like using a Hashtable. Both maintain a mapping between string names (the key) and a Java Object (the value). This means that any Java object can be associated with a user session, while cookies are limited only to String values. An added benefit is that with session tracking, the only state data that must be maintained between HTTP requests is the session ID. This means that sensitive data, such as passwords or user details, can be safely associated with a session because it never leaves the Servlet.
Whether a Servlet is creating a new session for a browser or looking up an existing session, the code remains the same. The getSession(boolean) method of HttpServletRequest returns the correct HttpSession instance for each request. It takes a Boolean parameter, which indicates whether a new session should be created if no session exists. This makes for simple code:
HttpSession session = req.getSession (true);
Once a Servlet has a reference to a session, it associates state data with a session or looks it up. Similar to the java.util.Hashtable class, accessor methods are provided for storing objects that can be looked up by a key (though there are slight differences in the method signatures).
Listing Three is a simple session Servlet, which uses HttpSession to track the number of times a page has been reloaded by a particular user. Though a trivial example, it clearly shows how to manage state data using sessions.
Sessions are capable of long durations, like cookies. Since a session identifies a user, it can potentially grant access to sensitive information or resources. For this reason, Servlets should always offer an option for the user to logoff and terminate the session. To permanently void a session, the invalidate() method should be used:
// Void this session and prevent future use
session.invalidate();
Voided sessions can no longer be accessed. If an attempt to put or get a value from a session is made after that, an IllegalStateException is thrown. When this occurs, users should be redirected to a login page, where a new session is created and additional authentication mechanisms can be applied.
Sessions are designed for temporary storage of state information, not for permanent storage. Before voiding a session, state data should be preserved if it is intended to be accessed in the future. For example, a Servlet could write to the local file system or store state information in a database.
Session tracking places a heavy reliance on the use of cookies. Not every browser will have cookie support enabled. While many web developers are happy to restrict access to visitors who enable cookies, this is not always the best design decision to make. A growing number of web visitors are concerned about privacy, and disable cookies.
Fortunately, the session-tracking features of the Servlet API also provide support for URL rewriting. Session information can be encoded in URLs as a way of offering backward compatibility with older browsers or those with cookies disabled. To Servlet developers, it matters little whether a session identifier is stored in a cookie or a URL; the API makes this fact transparent. However, it does involve extra work, as every hyperlink a Servlet outputs must be modified. You can encode session-tracking information in a URL by calling the HttpServletResponse.encodeURL(String) method. Including URL rewriting adds little complexity and gives the benefit of greater compatibility.
// Rewriting the URL to include session // identifier
out.println ("<a href='"+res.encodeURL ("/servlets/Menu")+ "'>Menu</a>");
The problem with this technique is twofold. When using the GET method, the encoded session is overwritten, making it necessary to add an extra hidden field to maintain the session ID. When cookie support is deemed to be a minimum requirement for using a system, HttpSession is a simple mechanism for Servlet tracking. However, if all browser conditions are to be supported, extra work is required.
The technique for tracking sessions without cookies is straightforward. Each session is tracked by a session identifier, which can be obtained by calling the getId() method of HttpSession. This session identifier is manually encoded into both hyperlinks and HTML forms by the Servlet. A list of active sessions can be examined and the correct session returned.
Listing Four shows this technique for a Servlet that uses both hyperlinks and HTML forms. Session IDs are encoded as CGI parameters in hyperlinks, as hidden field entries in forms, or as cookies if the browser supports them. New methods are provided, corresponding to the names of the Servlet API encoding methods. When running, it is advised that cookie support be disabled to demonstrate the effect.
To look up the correct session, the Servlet must have a reference to the current SessionContext. To get the SessionContext, the Servlet must execute the getSessionContext() method of HttpSession. For this reason, the SessionContext is stored as a private member variable as soon as an instance of HttpSession is created. Later, when the Servlet wants to look up the correct session, it calls the SessionContext.getSession(String) method. Please note that as of Version 2.1 of the Java Servlet API, this method is deprecated and may not be used, but works fine for earlier versions of the Servlet SDK.
Listing Four can be used as a template in your own applications, if you choose to go the extra mile and support browsers with cookies disabled. Either way, the session-tracking features of the Servlet API are very powerful mechanisms for achieving persistence of state data.
The Servlet API offers good support for managing state data across HTTP requests. Cookies make it easy to store simple state data storage, and for more sensitive data, the session-tracking features can be used. However, session tracking relies heavily on cookies, so for maximum compatibility, the extra steps outlined in this article can be applied. Even with this extra effort though, session tracking with Java Servlets is a simple way of achieving persistence across HTTP requests.
DDJ
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class WriteCookieServlet extends javax.servlet.http.HttpServlet
{
public void doGet(HttpServletRequest req,
HttpServletResponse res) throws IOException
{
ServletOutputStream out = res.getOutputStream();
res.setContentType ("text/html");
// Output a form to request data
out.println ("<form method=post action='WriteCookieServlet'>");
out.println ("Name : <input type=text name=name size=15><br>");
out.println ("Email: <input type=text name=email size=15><br>");
out.println ("<input type=submit></form>");
}
public void doPost(HttpServletRequest req,
HttpServletResponse res) throws IOException
{
ServletOutputStream out = res.getOutputStream();
res.setContentType ("text/html");
// Check to see if an email was submitted
String email = req.getParameter("email");
if (email != null)
{
// Store as a cookie
Cookie cookie = new Cookie("email", email);
res.addCookie(cookie);
}
// Check to see if an email was submitted
String name = req.getParameter("name");
if ( (name != null) && (!name.equals("")) )
{
// Store as a cookie
Cookie cookie = new Cookie("name", name);
res.addCookie(cookie);
}
out.println ("Thank you for registering");
out.println ("<a href=
'ReadCookieServlet'>Click here to read cookie data</a>");
}
}
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class ReadCookieServlet extends javax.servlet.http.HttpServlet
{
public void doGet(HttpServletRequest req,
HttpServletResponse res) throws IOException
{
String name = null;
String email = null;
ServletOutputStream out = res.getOutputStream();
res.setContentType ("text/html");
// Search for name & email address
Cookie list[] = req.getCookies();
for (int cookieIndex = 0;
cookieIndex < list.length; cookieIndex++)
{
String cookiename = list[cookieIndex].getName();
if ( cookiename.equals ("name") )
{
// Get cookie data value
name = list[cookieIndex].getValue() ;
}
else
if ( cookiename.equals("email") )
{
// Get cookie data value
email = list[cookieIndex].getValue() ;
}
}
if (name != null)
out.println ("Hello " + name + "<br>");
else
out.println ("Cookie 'name' not found<br>");
if (email != null)
out.println ("Your email is " + email + "<br>");
else
out.println ("Cookie 'email' not found<br>");
}
}
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class SessionCounterServlet extends javax.servlet.http.HttpServlet
{
public void doGet(HttpServletRequest req,
HttpServletResponse res) throws IOException
{
ServletOutputStream out = res.getOutputStream();
res.setContentType ("text/html");
// Get user session
HttpSession userSession = req.getSession(true);
// Check to see if there is a sessioncounter key
String sessionCounter =
(String) userSession.getValue("session_counter");
if (sessionCounter == null)
sessionCounter = "1";
else
{
// increment counter for each request
int count = Integer.parseInt(sessionCounter);
count++;
sessionCounter = String.valueOf(count);
}
// Place modified state data back in session
userSession.putValue("session_counter", sessionCounter);
out.println ("Number of times
visited this session : " + sessionCounter);
out.println ("<br>Refresh this
page to see change in session count");
}
}
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class NoCookieSessionServlet extends javax.servlet.http.HttpServlet
{
// Context allows us to lookup sessions
private HttpSessionContext context;
public void doGet(HttpServletRequest req,
HttpServletResponse res) throws IOException
{
ServletOutputStream out = res.getOutputStream();
res.setContentType ("text/html");
try
{
// Get user session
// HttpSession userSession = req.getSession(true);
HttpSession userSession = getSession(req);
// Check to see if there is a sessioncounter key
String sessionCounter = (String) userSession.
getValue("no_cookie_session_counter");
if (sessionCounter == null)
sessionCounter = "1";
else
{
// increment counter for each request
int count = Integer.parseInt(sessionCounter);
count++;
sessionCounter = String.valueOf(count);
}
// Place modified state data back in session
userSession.putValue("no_cookie_session_counter",
sessionCounter);
out.println ("Number of times visited this session :
" + sessionCounter);
out.println ("<br><a href='" + encodeUrl(
"NoCookieSessionServlet", userSession) );
out.println ("'>Click here to reload this page</a>");
// Output a dummy form, to test session-tracking
out.println ("<p><form action=
'NoCookieSessionServlet' method=post>");
out.println ("Test field <input type=text name=test
value='default value'><br>");
out.println ("<input type=submit>");
out.println (encodeForm(userSession));
out.println ("</form>");
}
catch(IllegalStateException ise)
{
out.println ("Invalid session");
}
}
public void doPost(HttpServletRequest req,
HttpServletResponse res) throws IOException
{
doGet(req,res);
}
private HttpSession getSession(HttpServletRequest req)
{
// Create a local variable to hold session
HttpSession session = null;
// Check to see if a session ID was specified as CGI paramater
String sessionID = req.getParameter ( "sessionid" );
// Check to see if this is the first invocation of the servlet
if ( (context == null) || ( sessionID == null) )
{
// Yes, so create brand new sessions.
session = req.getSession(true);
// If session context not stored, get for later
if (context == null)
context = session.getSessionContext();
}
else
{
// No, so we can attempt to look up the session
session = context.getSession (sessionID);
// Check to see session is really valid
if (session == null)
{
// No, so create a new one
session = req.getSession(true);
}
}
return session;
}
private String encodeUrl (String url, HttpSession session)
{
String sessionId = session.getId();
return ( url + "?sessionid=" + sessionId );
}
private String encodeForm (HttpSession session)
{
String sessionId = session.getId();
return ("<input type=hidden name=sessionid value='"
+ sessionId + "'>");
}
}