4. Session tracking
4.1. The problem
A web application may consist of several form exchanges between the server and the client. The process works as follows:
- Step 1
- Client C1 opens a connection with the server and makes its initial request.
- The server sends form F1 to client C1 and closes the connection opened in step 1.
- Step 2
- Client C1 fills it out and sends it back to the server. To do this, the browser opens a new connection with the server.
- The server processes the data from form 1, calculates information I1 from it, sends form F2 to client C1, and closes the connection opened in step 3.
- Step 3
- The cycle of steps 3 and 4 repeats in steps 5 and 6. At the end of step 6, the server will have received two forms, F1 and F2, and will have calculated information I1 and I2 from them.
The problem at hand is: how does the server keep track of information I1 and I2 associated with client C1? This problem is called tracking the session of client C1. To understand its root cause, let’s examine the diagram of a TCP-IP server application serving multiple clients simultaneously:
![]() |
In a classic TCP-IP client-server application:
- the client establishes a connection with the server
- exchanges data with the server through this connection
- the connection is closed by one of the two parties
The two key points of this mechanism are:
- a single connection is created for each client
- this connection is used for the entire duration of the server’s dialogue with its client
What allows the server to know at any given moment which client it is working with is the connection—or, in other words, the "channel"—that links it to its client. Since this channel is dedicated to a specific client, everything that comes through this channel originates from that client, and everything sent through this channel reaches the client.
The HTTP client-server mechanism closely follows the previous model, with the exception that the client-server dialogue is limited to a single exchange between the client and the server:
- the client opens a connection to the server and makes its request
- the server sends its response and closes the connection
If at time T1, a client C makes a request to the server, it obtains a connection C1 that will be used for the single request-response exchange. If, at time T2, this same client makes a second request to the server, it will obtain a connection C2 that is different from connection C1. For the server, there is then no difference between this second request from user C and their initial request: in both cases, the server treats the client as a new client. For there to be a link between client C’s different connections to the server, client C must be “recognized” by the server as a “regular” and the server must retrieve the information it has on this regular.
Let’s imagine a system that works as follows:
- There is a single queue
- There are several service counters. This means that several customers can be served at the same time. When a counter becomes available, a customer leaves the line to be served at that counter
- If this is the customer’s first visit, the teller gives them a ticket with a number. The customer may ask only one question. Once they receive their answer, they must leave the teller window and go to the back of the line. The teller records the customer’s information in a file bearing their number.
- When it is their turn again, the customer may be served by a different teller than the previous time. The teller asks for their token and retrieves the file with the token number. Once again, the customer makes a request, receives an answer, and information is added to their file.
- And so on... Over time, the customer will receive answers to all their requests. Tracking between different requests is achieved through the token and the file associated with it.
The session tracking mechanism in a client-server web application works similarly:
- Upon making their first request, a client is issued a token by the web server
- they will present this token with each subsequent request to identify themselves
The token can take various forms:
- a hidden field in a form
- the client makes its first request (the server recognizes it because the client has no token)
- The server sends its response (a form) and places the token in a hidden field within it. At this point, the connection is closed (the client leaves the session with its token). The server may have associated information with this token.
- The client makes a second request by resubmitting the form. The server retrieves the token from the form. It can then process the client’s second request by accessing, via the token, the information calculated during the first request. New information is added to the file associated with the token, a second response is sent to the client, and the connection is closed for the second time. The token has been placed back into the response form so that the user can present it during their next request.
- and so on...
The main drawback of this technique is that the token must be placed in a form. If the server’s response is not a form, the hidden field method can no longer be used.
- The cookie method
- The client makes its first request (the server recognizes this because the client has no token)
- The server responds by adding a cookie to the HTTP headers. This is done using the HTTP Set-Cookie command:
Set-Cookie: param1=value1;param2=value2;....
where param1, param2, etc., are parameter names and their corresponding values. Among the parameters will be the token. Very often, only the token is included in the cookie, with the server storing the other information in the folder associated with the token. The browser that receives the cookie will store it in a file on the disk. After the server’s response, the connection is closed (the client leaves the session with its token).
- (continued)
- The client makes its second request to the server. Each time a request is made to a server, the browser checks among all the cookies it has to see if it has one from the requested server. If so, it sends it to the server, always in the form of an HTTP command—the Cookie command, which has a syntax similar to that of the Set-Cookie command used by the server:
Cookie: param1=value1;param2=value2;....
Among the parameters sent by the browser, the server will find the token that allows it to recognize the client and retrieve the information associated with it.
This is the most commonly used form of token. It has one drawback: a user can configure their browser to reject cookies. Such users then cannot access web applications that use cookies.
- URL rewriting
- The client makes its first request (the server recognizes this because the client has no token)
- The server sends its response. This response contains links that the user must use to continue using the application. In the URL of each of these links, the server adds the token in the form URL;token=value.
- When the user clicks on one of the links to continue using the application, the browser sends a request to the web server, including the requested URL (URL;token=value) in the HTTP headers. The server is then able to retrieve the token.
4.2. The Java API for session tracking
We will now present the main methods useful for session tracking:
Retrieves the Session object to which the current request belongs. If the request was not yet part of a session, one is created. | |
identifier of the current session | |
creation date of the current session (number of milliseconds elapsed since January 1, 1970, 00:00). | |
date of the client's last access to the session | |
maximum duration in seconds of inactivity for a session. After this period, the session is invalidated. | |
Sets the maximum duration of inactivity for a session in seconds. After this period, the session is invalidated. | |
true if the session has just been created | |
associates a value with a parameter in a given session. This mechanism allows information to be stored that will remain available throughout the session. | |
Removes the parameter from the session data. | |
returns the value associated with the parameter in the session. Returns null if the parameter does not exist. | |
lists all attributes of the current session as an enumeration | |
closes the current session. All information associated with it is destroyed. |
4.3. Example 1
We present an example taken from the excellent book "Programming with J2EE" published by Wrox and distributed by Eyrolles. This book is a treasure trove of high-level information for developers of Java web solutions. The application presented in this book as a single Java servlet has been adapted here as a main servlet that calls JSP pages to display the various possible responses to the client.
The application is called "sessions" and is configured as follows in the <tomcat>\conf\server.xml file:
In the docBase folder above, you will find the following elements:

The files error.jsp, invalid.jsp, and valid.jsp are all associated with the sessions application. In the WEB-INF folder above, we find:

Shown above is the web.xml configuration file for the sessions application. In the classes folder, you will find the servlet class file:

The application's web.xml file is as follows:
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
<servlet-name>cycledevie</servlet-name>
<servlet-class>cycledevie</servlet-class>
<init-param>
<param-name>validSessionURL</param-name>
<param-value>/validate.jsp</param-value>
</init-param>
<init-param>
<param-name>urlSessionInvalid</param-name>
<param-value>/invalid.jsp</param-value>
</init-param>
<init-param>
<param-name>urlError</param-name>
<param-value>/error.jsp</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>lifecycle</servlet-name>
<url-pattern>/lifecycle</url-pattern>
</servlet-mapping>
</web-app>
The main servlet is named cycledevie (servlet-name) and is associated with the cycledevie.class file (servlet-class). It has an alias /cycledevie (servlet-mapping) that allows it to be called via the URL http://localhost:8080/sessions/cycledevie. It has three initialization parameters:
URL of the page displaying the current session's properties | |
URL of the page displayed after the current session is invalidated | |
URL of the page displayed in the event of an initialization error for the main servlet cycledevie |
The components of the sessions application are as follows:
main servlet - analyzes the client request:
| |
| |
displayed when the user has invalidated the current session. It then offers to create a new one. | |
displayed when the main servlet encounters errors during initialization. |
The main servlet lifecycle is as follows:
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class cycledevie extends HttpServlet{
// instance variables
String errorMessage = null;
String invalidSessionUrl = null;
String validSessionURL = null;
String errorUrl = null;
//-------- GET
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException{
// Did initialization succeed?
if (errorMessage != null) {
// forward to the error page
getServletContext().getRequestDispatcher(errorURL).forward(request, response);
}
// retrieve the current session
HttpSession session = request.getSession();
// Analyze the action to be taken
String action = request.getParameter("action");
// invalidate the current session
if (action != null && action.equals("invalidate")) {
// invalidate the current session
session.invalidate();
// Pass control to the URL `urlSessionInvalide`
getServletContext().getRequestDispatcher(urlSessionInvalid).forward(request, response);
}
// other cases
// Pass control to the URL sessionInvalid
getServletContext().getRequestDispatcher(validURL).forward(request, response);
}
//-------- POST
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException{
doGet(request, response);
}
//-------- INIT
public void init(){
// retrieve the initialization parameters
ServletConfig config = getServletConfig();
urlSessionInvalid = config.getInitParameter("urlSessionInvalid");
validUrlSession = config.getInitParameter("validUrlSession");
urlError = config.getInitParameter("urlError");
// Are the parameters OK?
if (validUrlSession == null || invalidUrlSession == null) {
errorMessage = "Incorrect configuration";
}
}
}
Note the following points:
- In its initialization method, the servlet retrieves its three parameters
- When processing (doGet) a request, the servlet:
- first checks that no errors occurred during initialization. If there were any, it redirects to the error.jsp page.
- checks the value of the action parameter. If this parameter has the value "invalid", the servlet redirects to the invalid.jsp page; otherwise, to the valid.jsp page.
The JSP page **valide.jsp** displays the characteristics of the current session:
<%@ page import="java.util.*" %>
<%
// jspService
// here we are in the case where we need to describe the current session
String status = session.isNew() ? "New session" : "Existing session";
%>
<!-- start of HTML page -->
<html>
<meta http-equiv="pragma" content="no-cache">
<head>
<title>Session Lifecycle</title>
</head>
<body>
<h3>Session Lifecycle</h3>
<hr>
<br>Session status: <%= status %>
<br>Session ID: <%= session.getId() %>
<br>Creation time: <%= new Date(session.getCreationTime()) %>
<br>Last access time: <%= new Date(session.getLastAccessedTime()) %>
<br>Maximum inactivity interval: <%= session.getMaxInactiveInterval() %>
<br><a href="/sessions/cycledevie?action=invalider">Invalidate session</a>
<br><a href="/sessions/cycledevie">Reload page</a>
<body>
</html>
Note that in the line
we use a session object that appears out of nowhere. In fact, this object is one of the implicit objects made available to JSP pages, just like the request, response, out, config (ServletConfig), and context (ServletContext) objects we’ve already encountered. The two links on the page reference the lifecycle servlet presented earlier:
<br><a href="/sessions/cycledevie?action=invalider">Invalidate session</a>
<br><a href="/sessions/cycledevie">Reload the page</a>
The link to invalidate the session includes the action=invalidate parameter, which allows the lifecycle servlet to recognize that the user wants to invalidate the current session. The other link reloads the page. To prevent the browser from fetching the page from a cache, the HTML directive:
has been used. It instructs the browser not to use the cache for the page it receives.
The invalidate.jsp page is as follows:
<!-- Start of HTML page -->
<html>
<head>
<title>Session Lifecycle</title>
</head>
<body>
<h3>Session Lifecycle</h3>
<hr>
Your session has expired
<a href="/sessions/cycledevie">Create a new session</a>
</body>
</html>
It provides a link to the cycledevie servlet without the action parameter. This link will cause the cycledevie servlet to create a new session.
The error.jsp page is as follows:
<%
// jspService
// here we are in the case where we need to describe the current session
String errorMessage = request.getAttribute("errorMessage");
if(msgError==null) msgError="Unidentified error";
%>
<!-- start of HTML page -->
<html>
<head>
<title>Session Lifecycle</title>
</head>
<body>
<h3>Session Lifecycle</h3>
<hr>
Application unavailable(<%= msgError %>)
</body>
</html>
Its role is to display the error message sent to it by the lifecycle servlet. Let’s now look at some examples of execution. The servlet is requested for the first time:

The page above indicates that we are in a new session. We use the "Reload page" link:

The previous result indicates that we are still in the same session as on the previous page (same ID). Note that the time of the last access to this session has changed. Now let’s use the “Invalidate session” link:

Note the URL of this new page with the action=invalidate parameter. Let’s use the “Create a new session” link to create a new session:

We can see that a new session has started. In the previous examples, the session relies on the cookie mechanism. Let’s now disable cookies in our browser and repeat the tests. The following examples were performed using Netscape Communicator. For some unexplained reason, tests performed with IE6 yielded unexpected results, as if IE6 continued to use cookies even though they had been disabled. The cycledevie servlet is requested for the first time:

We now use the "Reload Page" link:

We can see two things:
- the session ID has changed
- the servlet detects the session as a new session
The Tomcat server provides a solution to the problem of users who disable cookies in their browser. It uses two mechanisms to implement the token mentioned at the beginning of this paragraph: cookies and URL rewriting. If the session cookie is not available, it will attempt to retrieve the token from the URL requested by the client. For this to work, the URL must contain the token. Generally speaking, all links generated in an HTML document that point to the web application must contain the application’s token. This can be done using the encodeURL method:
adds the current session token to the URL passed as a parameter in the form URL;jsessionid=xxxx |
We modify our application as follows:
- In the cycledevie.java servlet, the URLs are encoded:
// we pass control to the error page
getServletContext().getRequestDispatcher(response.encodeURL(urlError)).forward(request, response);
....
// we redirect to the urlSessionInvalid URL
getServletContext().getRequestDispatcher(response.encodeURL(urlSessionInvalide)).forward(request, response);
....
// Pass control to the invalid URL session
getServletContext().getRequestDispatcher(response.encodeURL(urlSessionValide)).forward(request, response);
- In the valide.jsp page, the URLs are encoded:
<%
// jspService
// Here we are in the case where we need to describe the current session
String status = session.isNew() ? "New session" : "Existing session";
// URL encoding for lifecycle
String URLcycledevie = response.encodeURL("/sessions/cycledevie");
%>
............
<br><a href="<%= URLcycledevie %>?action=invalider">Invalidate session</a>
<br><a href="<%= URLcycledevie %>">Reload page</a>
- In the invalide.jsp page, the URLs are encoded:
<%
// jspservice - invalidate the current session
session.invalidate();
// URL encoding for lifecycle
String URLcycledevie = response.encodeURL("/sessions/cycledevie");
%>
..........
<a href="<%= URLcycledevie %>">Create a new session</a>
Now we are ready for testing. We are using Netscape 4.5 and cookies have been disabled. We request the cycledevie servlet for the first time:

and we reload the page using the "Reload page" link:

We can see that:
- the session has not changed (same ID)
- the URL of the cycledevie servlet does indeed contain the token, as shown in the Address field above
- the Tomcat server therefore retrieves the session token from the requested URL (if the developer took care to encode it).
4.4. Example 2
We now present an example showing how to store information in a client’s session. Here, the only piece of information will be a counter that is incremented each time the user calls the servlet’s URL. When it is called for the first time, the following page appears:

If you click the "Reload page" link above, you get the following new page:

The application has three components:
- a servlet that processes the client's request
- a JSP page that displays the counter value
- a JSP page that displays any errors
These three components are installed in the "sessions" web application already in use. Its web.xml file has been modified to configure the new servlets:
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
...
<servlet>
<servlet-name>counter</servlet-name>
<servlet-class>counter</servlet-class>
<init-param>
<param-name>counterDisplayURL</param-name>
<param-value>/counter.jsp</param-value>
</init-param>
<init-param>
<param-name>error-url</param-name>
<param-value>/counter-error.jsp</param-value>
</init-param>
</servlet>
...
<servlet-mapping>
<servlet-name>counter</servlet-name>
<url-pattern>/counter</url-pattern>
</servlet-mapping>
</web-app>
- The servlet is called counter (servlet-name) and is linked to the class file counter.class (servlet-class)
- It has two initialization parameters:
- displayCounterURL: URL of the JSP page displaying the counter
- urlErreur: URL of the JSP page displaying any errors
- and an alias /compteur, which means it will be called via the URL http://localhost:8080/sessions/compteur
The compteur.java servlet is as follows:
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class counter extends HttpServlet{
// instance variables
String errorMessage = null;
String counterDisplayURL = null;
String errorURL = null;
//-------- GET
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException{
// Did initialization succeed?
if (errorMessage != null) {
// forward to the error page
getServletContext().getRequestDispatcher(errorURL).forward(request, response);
}
// retrieve the current session
HttpSession session = request.getSession();
// and the counter
String counter = (String) session.getAttribute("counter");
if(counter == null) counter = "0";
// Increment the counter
try{
counter = "" + (Integer.parseInt(counter) + 1);
} catch (Exception ex) {}
// Store counter in session
session.setAttribute("counter", counter);
// and in the request
request.setAttribute("counter", counter);
// forward to the counter display URL
getServletContext().getRequestDispatcher(counterDisplayURL).forward(request, response);
}
//-------- POST
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException{
doGet(request, response);
}
//-------- INIT
public void init(){
// retrieve the initialization parameters
ServletConfig config = getServletConfig();
counterDisplayUrl = config.getInitParameter("counterDisplayUrl");
errorURL = config.getInitParameter("errorURL");
// Are the settings OK?
if(counterDisplayURL == null) {
errorMessage = "Incorrect configuration";
}
}
}
This servlet has the same structure as the servlets we have already encountered. Note the handling of the counter:
- the session is retrieved via request.getSession()
- the counter is retrieved from this session via session.getAttribute("counter")
- if a null value is retrieved, it means the session has just started. The counter is then set to 0.
- the counter is incremented, stored back in the session (session.setAttribute("counter", counter)), and placed in the request that will be passed to the display servlet (request.setAttribute("counter", counter)).
The display page, compteur.jsp, is as follows:
<%
// jspService
// retrieve the counter
String counter = (String) request.getAttribute("counter");
if(counter==null) counter="unknown";
%>
<!-- start of HTML page -->
<html>
<head>
<title>Counting throughout a session</title>
</head>
<body>
<h3>Counting throughout a session (requires cookies to be enabled)</h3>
<hr>
counter = (<%= counter %>)
<br><a href="/sessions/compteur">Reload page</a>
</body>
</html>
The page above simply retrieves the counter attribute (request.getAttribute("counter")) passed to it by the main servlet and displays it.
The error page, *erreurcompteur.jsp,* is as follows:
<%
// jspService
// an error has occurred
String errorMessage = request.getAttribute("errorMessage");
if(msgError==null) msgError="Unidentified error";
%>
<!-- start of HTML page -->
<html>
<head>
<title>Counting throughout a session</title>
</head>
<body>
<h3>Counting throughout a session (requires cookies to be enabled)</h3>
<hr>
Application unavailable(<%= msgError %>)
</body>
</html>
4.5. Example 3
We propose to write a Java application that would act as a client for the previous counter application. It would call it N times in a row, where N is passed as a parameter. Our goal is to demonstrate a programmed web client and how to manage cookies. Our starting point will be a generic web client presented in the Java handout by the same author. It is called as follows:
webclient URL GET/HEAD
- URL: requested URL
- GET/HEAD: GET to request the page’s HTML code, HEAD to limit the response to HTTP headers only
Here is an example using the URL http://localhost:8080/sessions/compteur:
E:\data\serge\JAVA\SOCKETS\client web>java clientweb http://localhost:8080/sessions/compteur GET
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 14:21:18 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
Set-Cookie: JSESSIONID=B8A9076E552945009215C34A97A0EC5D;Path=/sessions
<!-- Start of HTML page -->
<html>
<head>
<title>Counting throughout a session</title>
</head>
<body>
<h3>Counting throughout a session (requires cookies to be enabled)</h3>
<hr>
counter = (1)
<br><a href="/sessions/compteur">Reload page</a>
</body>
</html>
The clientweb program displays everything it receives from the server. Above, we see the HTTP Set-cookie command, which the server uses to send a cookie to its client. Here, the cookie contains two pieces of information:
- JSESSIONID, which is the session token
- Path, which defines the URL to which the cookie belongs. Path=/sessions tells the browser that it must send the cookie back to the server every time it requests a URL starting with /sessions. In the sessions application, we used various servlets, including the /sessions/lifecycle and /sessions/counter servlets. If we call the /sessions/cycledevie servlet, the browser will receive a J token. If, using the same browser, we then call the /sessions/compteur servlet, the browser will send the J token back to the server because it applies to all URLs starting with /sessions. In our example, the cycledevie and compteur servlets do not need to share the same session token. They should therefore not have been placed in the same web application. This is an important point to remember: all servlets within the same application share the same session token.
- A cookie can also have an expiration time. Here, this information is missing. The cookie will therefore be deleted when the browser is closed. A cookie can have an expiration time of N days, for example. As long as it is valid, the browser will send it back every time one of the URLs in its domain (Path) is accessed. Consider an online CD store. It can track a customer’s browsing path through its catalog and gradually determine their preferences—classical music, for example. These preferences can be stored in a cookie with a lifespan of 3 months. If that same customer returns to the site after a month, the browser will send the cookie back to the server application. Based on the information contained in the cookie, the server application can then tailor the generated pages to the customer’s preferences.
The web client code follows. It will later serve as the starting point for another client.
// imported packages
import java.io.*;
import java.net.*;
public class webClient{
// requests a URL
// displays its content on the screen
public static void main(String[] args){
// syntax
final String syntax = "pg URI GET/HEAD";
// number of arguments
if(args.length != 2)
error(syntax, 1);
// note the requested URI
String URLString = args[0];
String command = args[1].toUpperCase();
// Check the validity of the URI
URL url = null;
try{
url = new URL(URLString);
} catch (Exception ex) {
// Invalid URI
error("The following error occurred: " + ex.getMessage(), 2);
}//catch
// Check the request method
if(! request.equals("GET") && ! request.equals("HEAD")){
// Incorrect request
error("The second parameter must be GET or HEAD", 3);
}
// Extract the relevant information from the URL
String path = url.getPath();
if(path.equals("")) path="/";
String query = url.getQuery();
if(query!=null) query="?"+query; else query="";
String host = url.getHost();
int port = url.getPort();
if (port == -1) port = url.getDefaultPort();
// we can proceed
Socket client = null; // the client
BufferedReader IN = null; // the client's read stream
PrintWriter OUT = null; // the client's write stream
String response = null; // server response
try{
// connect to the server
client = new Socket(host, port);
// Create the TCP client's input/output streams
IN = new BufferedReader(new InputStreamReader(client.getInputStream()));
OUT = new PrintWriter(client.getOutputStream(), true);
// Request the URL - send HTTP headers
OUT.println(command + " " + path + query + " HTTP/1.1");
OUT.println("Host: " + host + ":" + port);
OUT.println("Connection: close");
OUT.println();
// read the response
while((response = IN.readLine()) != null) {
// process the response
System.out.println(response);
}//while
// done
client.close();
} catch (Exception e) {
// handle the exception
error(e.getMessage(), 4);
}//catch
}//main
// display errors
public static void error(String msg, int exitCode) {
// display error
System.err.println(msg);
// exit with error
System.exit(exitCode);
}//error
}//class
We now create the clientCounter program, which is called as follows:
clientCounter URL N [JSESSIONID]
- URL: URL of the counter servlet
- N: number of calls to make to this servlet
- JSESSIONID: optional parameter—session token
The purpose of the program is to call the counter servlet N times by managing the session cookie and displaying the counter value returned by the server each time. At the end of the N calls, the counter value must be N. Here is a first example of execution:
E:\data\serge\Servlets\sessions\jb7>java.bat clientCounter http://localhost:8080/sessions/compteur 3
--> GET /sessions/counter HTTP/1.1
--> Host: localhost:8080
--> Connection: close
-->
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, Aug 8, 2002 6:25:00 PM GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
Set-Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A;Path=/sessions
Cookie found: 92DB3808CE8FCB47D47D997C8B52294A
counter: 1
--> GET /sessions/counter HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:00 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
counter: 2
--> GET /sessions/counter HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:00 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
counter: 3
The program displays:
- the HTTP headers it sends to the server in the form -->
- the HTTP headers it receives
- the counter value after each call
We can see that during the first call:
- the client does not send a cookie
- the server sends one
For subsequent requests:
- the client systematically sends back the cookie it received from the server during the first request. This is what allows the server to recognize it and increment its counter.
- the server no longer sends a cookie
We rerun the previous program by passing the token above as the third parameter:
E:\data\serge\Servlets\sessions\jb7>java.bat clientCounter http://localhost:8080/sessions/compteur 3 92DB3808CE8FCB47D47D997C8B52294A
--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:25 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
counter: 4
--> GET /sessions/counter HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:25 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
counter: 5
--> GET /sessions/counter HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:25 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
counter: 6
Here we see that as soon as the client makes its first request, the server receives a valid session cookie. It is important to note that for Tomcat, the default maximum inactivity time for a session is 20 minutes (this is actually configurable). If the program’s second request sends the cookie received during the first request quickly enough, the server will treat it as the same session. This highlights a potential security vulnerability. If I am able to intercept a session token on the network, I can then impersonate the user who initiated the session. In our example, the first call represents the user who initiates the session (perhaps with a username and password that grant them the right to receive a token), and the second call represents the user who has "hacked" the session token from the first call. If the current operation is a banking transaction, this can become very problematic...
The client code is as follows:
// imported packages
import java.io.*;
import java.net.*;
import java.util.regex.*;
public class clientCounter{
// requests a URL
// displays its content on the screen
public static void main(String[] args){
// syntax
final String syntax = "pg METER-URL N [JSESSIONID]";
// number of arguments
if(args.length != 2 && args.length != 3)
error(syntax, 1);
// note the requested URL
String URLString = args[0];
// Check the validity of the URL
URL url = null;
try{
url = new URL(URLString);
} catch (Exception ex) {
// Invalid URI
error("The following error occurred: " + ex.getMessage(), 2);
}//catch
// Check the number of calls N
int N = 0;
try{
N = Integer.parseInt(args[1]);
if (N <= 0) throw new Exception();
} catch (Exception ex) {
// Incorrect argument N
error("The number of calls N must be a positive integer", 3);
}
// Was the JSESSIONID token passed as a parameter?
String JSESSIONID="";
if (args.length == 3) JSESSIONID = args[2];
// extract the relevant information from the URL
String path = url.getPath();
if(path.equals("")) path="/";
String query = url.getQuery();
if (query != null) query = "?" + query; else query = "";
String host = url.getHost();
int port = url.getPort();
if (port == -1) port = url.getDefaultPort();
// we can proceed
Socket client = null; // the client
BufferedReader IN = null; // the client's read stream
PrintWriter OUT = null; // the client's write stream
String response = null; // server response
// the pattern to search for in the HTTP headers
Pattern cookiePattern = Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
// the pattern to search for in the HTML code
Pattern counterPattern = Pattern.compile("counter = .*?(\\d+)");
// result of the pattern match
Matcher result = null;
// a boolean indicating the result of the counter search
boolean counterFound;
try{
// make the N calls to the server
for (int i = 0; i < N; i++) {
// connect to the server
client = new Socket(host, port);
// create the TCP client's input/output streams
IN = new BufferedReader(new InputStreamReader(client.getInputStream()));
OUT = new PrintWriter(client.getOutputStream(), true);
// Request the URL - send HTTP headers
send(OUT, "GET " + path + query + " HTTP/1.1");
send(OUT, "Host: " + host + ":" + port);
if(! JSESSIONID.equals("")){
send(OUT, "Cookie: JSESSIONID=" + JSESSIONID);
}
send(OUT, "Connection: close");
send(OUT, "");
// Read the response until the end of the headers, checking for any cookies
while((response = IN.readLine()) != null) {
// track response
System.out.println(response);
// Empty line?
if(response.equals("")) break;
// non-empty HTTP line
// if we don't have the session token, look for it
if (JSESSIONID.equals("")){
// compare the HTTP line to the cookie pattern
result = cookieTemplate.matcher(response);
if(result.find()){
// We found the cookie
JSESSIONID = result.group(1);
}
}
}//while
// That's it for the HTTP headers—moving on to the HTML code
foundCounter=false;
while((response = IN.readLine()) != null) {
// Does the current line contain the counter?
if (!counterFound){
result = counterModel.matcher(response);
if(result.find()){
// The counter was found—display it
System.out.println("counter: " + result.group(1));
counterFound = true;
}
}
}//while
// done
client.close();
}//for
} catch (Exception e) {
// handle the exception
error(e.getMessage(), 4);
}//catch
}//main
// display errors
public static void error(String msg, int exitCode){
// display error
System.err.println(msg);
// exit with error
System.exit(exitCode);
}//error
// track client-server communication
public static void send(PrintWriter OUT, String msg) {
// send message to server
OUT.println(msg);
// screen output
System.out.println("--> " + msg);
}//error
}//class
Let's break down the key points of this program:
- We need to perform N client-server exchanges. That is why they are in a loop
- For each exchange, the client opens a TCP/IP connection with the server. Once the connection is established, it sends the HTTP headers of its request to the server:
// request the URL - send HTTP headers
send(OUT, "GET " + path + query + " HTTP/1.1");
send(OUT, "Host: " + host + ":" + port);
if(! JSESSIONID.equals("")){
send(OUT, "Cookie: JSESSIONID=" + JSESSIONID);
}
send(OUT, "Connection: close");
send(OUT, "");
If the JSESSIONID token is available, it is sent as a cookie; otherwise, it is not.
- Once its request is sent, the client waits for the server's response. It begins by examining the HTTP headers of this response to look for a possible cookie. To find it, it compares the lines it receives to the cookie's regular expression:
// the pattern searched for in the HTTP headers
Pattern modelCookie = Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
...........................
// read the response until the end of the headers, looking for any cookies
while((response = IN.readLine()) != null) {
// response tracking
System.out.println(response);
// empty line?
if(response.equals("")) break;
// non-empty HTTP line
// if we don't have the session token, look for it
if (JSESSIONID.equals("")){
// compare the HTTP line to the cookie pattern
result = cookieTemplate.matcher(response);
if(result.find()){
// We found the cookie
JSESSIONID = result.group(1);
}
}
}//while
- Once the token has been found the first time, it will no longer be searched for in subsequent calls to the server. Once the HTTP headers of the response have been processed, we move on to the HTML code of that same response. In it, we look for the line that gives the counter value. This search is also performed using a regular expression:
// the pattern for the counter searched for in the HTML code
Pattern counterPattern = Pattern.compile("counter = .*?(\\d+)");
..................................
// done with the HTTP headers—moving on to the HTML code
counterFound = false;
while((response = IN.readLine()) != null) {
// Does the current line contain the counter?
if (!counterFound){
result = counterModel.matcher(response);
if(result.find()){
// We found the counter—let's display it
System.out.println("counter: " + result.group(1));
counterFound = true;
}
}
}//while
4.6. Example 4
In the previous example, the web client returns the token as a cookie. We saw that it could also return it within the requested URL itself in the form URL;jsessionid=xxx. Let’s verify this. The clientCompteur.java program is renamed to clientCompteur2.java and modified as follows:
....
// request the URL - send HTTP headers
if(JSESSIONID.equals(""))
send(OUT, "GET " + path + query + " HTTP/1.1");
else send(OUT,"GET " + path + query + ";jsessionid=" + JSESSIONID + " HTTP/1.1");
send(OUT, "Host: " + host + ":" + port);
send(OUT, "Connection: close");
send(OUT, "");
....
The client therefore requests the counter URL via GET URL;jsessionid=xx HTTP/1.1 and no longer sends a cookie. This is the only change. Here are the results of an initial call:
E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur2 http://localhost:8080/sessions/compteur 2
--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Connection: close
-->
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, Aug 8, 2002 6:49:30 PM GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
Set-Cookie: JSESSIONID=48A6DBA8357D808EC012AAF3A2AFDA63;Path=/sessions
cookie found: 48A6DBA8357D808EC012AAF3A2AFDA63
counter: 1
--> GET /sessions/counter;jsessionid=48A6DBA8357D808EC012AAF3A2AFDA63 HTTP/1.1
--> Host: localhost:8080
--> Connection: close
-->
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, Aug 8, 2002 6:49:30 PM GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
counter: 2
On the first request, the client requests the URL without a session token. The server responds by sending the token. The client then re-requests the same URL, appending the received token to it. We can see that the counter is incremented, proving that the server correctly recognized that it was the same session.
4.7. Example 5
This example shows an application consisting of three pages, which we will call page0, page1, and page2. The user must access them in this order:
- page0 is a form requesting information: a name
- page1 is a form received in response to the submission of the form on page0. It requests a second piece of information: an age
- page2 is an HTML document that displays the name obtained from page0 and the age obtained from page1.
There are three client-server exchanges here:
- In the first exchange, the page0 form is requested by the client and sent by the server
- In the second exchange, the page1 form is requested by the client and sent by the server. The client sends the name to the server.
- In the third exchange, the page3 document is requested by the client and sent by the server. The client sends the age to the server. The page3 document must display the name and age. The name was obtained by the server in the second exchange and has since been "forgotten." A session is used to store the name from exchange 2 so that it is available during exchange 3.
The page0 document obtained in the first exchange is as follows:

We fill in the name field:

We click the "Next" button and then get the following page1:

We fill in the age field:

We click the "Next" button and are then presented with the following page2:

When page0 is submitted to the server, the server may return it with an error code if the name field is empty:

When you submit page1 to the server, it may return an error code if the age is invalid:

The application consists of a servlet and four JSP pages:
displays page0 | |
displays page1 | |
displays page2 | |
displays an error page |
The web application is called suitedepages and is configured as follows in Tomcat's server.xml file:
The web.xml configuration file for the suitedepages application is as follows:
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
<servlet-name>main</servlet-name>
<servlet-class>main</servlet-class>
<init-param>
<param-name>urlPage0</param-name>
<param-value>/page0.jsp</param-value>
</init-param>
<init-param>
<param-name>urlPage1</param-name>
<param-value>/page1.jsp</param-value>
</init-param>
<init-param>
<param-name>urlPage2</param-name>
<param-value>/page2.jsp</param-value>
</init-param>
<init-param>
<param-name>errorUrl</param-name>
<param-value>/error.jsp</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>main</servlet-name>
<url-pattern>/main</url-pattern>
</servlet-mapping>
</web-app>
The main servlet is named main and, thanks to its alias (servlet-mapping), is accessible via the URL http://localhost:8080/suitedepages/main*. It has four initialization parameters, which are the URLs of the four JSP pages used for the different views. The code for the main* servlet is as follows:
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.util.*;
import java.util.regex.*;
public class Main extends HttpServlet {
// instance variables
String errorMessage = null;
String urlPage0 = null;
String urlPage1 = null;
String urlPage2 = null;
String errorUrl = null;
//-------- GET
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException{
// Did initialization succeed?
if (errorMessage != null) {
// forward to the error page
getServletContext().getRequestDispatcher(errorURL).forward(request, response);
}
// Retrieve the "step" parameter
String step = request.getParameter("step");
// retrieve the current session
HttpSession session = request.getSession();
// process the current stage
if (stage == null) stage0(request, response, session);
if (step.equals("1")) step1(request, response, session);
if(step.equals("2")) step2(request, response, session);
// other cases are invalid
step0(request, response, session);
}
//-------- POST
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException{
doGet(request, response);
}
//-------- INIT
public void init(){
// retrieve the initialization parameters
ServletConfig config = getServletConfig();
urlPage0 = config.getInitParameter("urlPage0");
urlPage1 = config.getInitParameter("urlPage1");
urlPage2 = config.getInitParameter("urlPage2");
errorURL = config.getInitParameter("errorURL");
// Are the parameters OK?
if(urlPage0 == null || urlPage1 == null || urlPage2 == null) {
msgError = "Incorrect configuration";
}
}
//-------- step0
public void step0(HttpServletRequest request, HttpServletResponse response, HttpSession session)
throws IOException, ServletException{
// Set some attributes
request.setAttribute("name", "");
// display page 0
request.getRequestDispatcher(urlPage0).forward(request, response);
}
//-------- step1
public void step1(HttpServletRequest request, HttpServletResponse response, HttpSession session)
throws IOException, ServletException{
// retrieve the name from the request
String name = request.getParameter("name");
// Is "name" set?
if (name == null) step0(request, response, session);
// remove any spaces from the name
name = name.trim();
// Store it in a request attribute
request.setAttribute("name", name);
// Is name empty?
if(name.equals("")){
// that's an error
ArrayList errors = new ArrayList();
errors.add("You did not provide a name");
// add the errors to the request
request.setAttribute("errors", errors);
// Return to page 0
step0(request, response, session);
}
// Valid name - store it in the current session
session.setAttribute("name", name);
// Set the age attribute in the request
request.setAttribute("age", "");
// display page 1
request.getRequestDispatcher(urlPage1).forward(request, response);
}
//-------- step2
public void step2(HttpServletRequest request, HttpServletResponse response, HttpSession session)
throws IOException, ServletException{
// retrieve the name from the session
String name = (String) session.getAttribute("name");
// Is "name" set?
if (name == null) step0(request, response, session);
// Set it as a request attribute
request.setAttribute("name", name);
// retrieve the age from the request
String age = request.getParameter("age");
// Is age set?
if (age == null) {
// return to page 1
request.setAttribute("age", "");
request.getRequestDispatcher(urlPage1).forward(request, response);
}
// store the age in the request
age = age.trim();
request.setAttribute("age", age);
// Is the age valid?
if(! Pattern.matches("^\\s*\\d+\\s*$", age)){
// It's an error
ArrayList errors = new ArrayList();
errors.add("Invalid age");
// add the errors to the request
request.setAttribute("errors", errors);
// Return to page 1
request.getRequestDispatcher(urlPage1).forward(request, response);
}
// Age valid - display page 2
request.getRequestDispatcher(urlPage2).forward(request, response);
}
}
- The init method retrieves the four initialization parameters and sets an error message if any of them are missing
- We have seen that the request consists of three exchanges. To track the current stage of these exchanges, the page0 and page1 forms have a hidden variable "etape" with a value of 1 (page0) or 2 (page1). This number can be interpreted as the number of the next page to be displayed. In the doGet method, this parameter is retrieved from the request, and depending on its value, processing is delegated to three other methods:
- étape0 processes the initial request and sends page0
- étape1 processes the form on page0 and sends page1 or page0 again if an error occurred
- step2 processes the form on page1 and sends page2 or page1 again if there was an error
- step0
- displays page0 with an empty name
- Step 1
- Retrieves the "name" parameter from the page0 form.
- Checks that the name exists (is not null). If not, page0 is displayed again as if it were the first call.
- checks that the name is not empty. If it is, page0 is displayed again along with an error message.
- stores the name in the current session and displays page1 if the name is valid.
- step2
- Retrieves the name parameter from the current session.
- Checks that the name exists (is not null). If not, displays page0 again as if it were the first call.
- Retrieves the age parameter from the current request sent by page1.
- checks that the age is valid. If not, displays page1 again with an error message.
- Stores the name and age as request attributes and displays page2 if the name and age are valid.
The page0.jsp page is as follows:
<%@ page import="java.util.*" %>
<% // page0.jsp
// retrieve the request attributes
String name = (String) request.getAttribute("name");
ArrayList errors = (ArrayList) request.getAttribute("errors");
// Are the attributes valid?
if(name == null) {
// return to the main servlet
request.getRequestDispatcher("/main").forward(request, response);
}
%>
<html>
<head>
<title>page 0</title>
</head>
<body>
<h3>Page 0/2</h3>
<form name="frmNom" method="POST" action="/suitedepages/main">
<input type="hidden" name="step" value="1">
<table>
<tr>
<td>Your name</td>
<td><input type="text" name="nom" value="<%= nom %>"></td>
</tr>
</table>
<input type="submit" value="Continue">
</form>
<% // errors?
if (errors != null) {
%>
<hr>
<font color="red">
The following errors occurred
<ul>
<% for(int i=0;i<errors.size();i++){ %>
<li><%= errors.get(i) %>
<% }//for %>
</ul>
<% }//if %>
</body>
</html>
- The page0.jsp page can be called by the main servlet in two cases:
- during the initial request
- after processing the page0 form when an error occurs
- The parameter `nameToDisplay` is provided by the main servlet, along with any error list. The page0.jsp servlet therefore begins by retrieving these two pieces of information.
- The form is "posted" to the main servlet with the hidden field "etape," which indicates which stage of the application the user is in.
The page1.jsp page is as follows:
<%@ page import="java.util.*" %>
<% // page1.jsp
// retrieve the request attributes
String name = (String)request.getAttribute("name");
String age = (String) request.getAttribute("age");
ArrayList errors = (ArrayList) request.getAttribute("errors");
// Are the attributes valid?
if(name == null || age == null){
// return to the main servlet
request.getRequestDispatcher("/main").forward(request, response);
}
%>
<html>
<head>
<title>Page 1</title>
</head>
<body>
<h3>Page 1/2</h3>
<form name="frmAge" method="POST" action="/suitedepages/main">
<input type="hidden" name="etape" value="2">
<table>
<tr>
<td>Last Name</td>
<td><font color="green"><%= name %></font></td>
</tr>
<tr>
<td>Your age</td>
<td><input type="text" name="age" size="3" value="<%= age %>"></td>
</tr>
</table>
<input type="submit" value="Continue">
</form>
<% // errors?
if (errors != null) {
%>
<hr>
<font color="red">
The following errors occurred
<ul>
<% for(int i=0;i<errors.size();i++){ %>
<li><%= errors.get(i) %>
<% }//for %>
</ul>
<% }//if %>
</body>
</html>
The page1.jsp page has a structure similar to that of the page0.jsp page, with the exception that it now receives two attributes from the main servlet: name and age. Finally, the page2.jsp page is as follows:
<%
// page2.jsp
// retrieve the request attributes
String name = (String) request.getAttribute("name");
String age = (String)request.getAttribute("age");
// Are the attributes valid?
if(name == null || age == null){
// return to the main servlet
request.getRequestDispatcher("/main").forward(request, response);
}
%>
<html>
<head>
<title>page 2</title>
</head>
<body>
<h3>Page 2/2</h3>
<table>
<tr>
<td>Name</td>
<td><font color="green"><%= name %></font></td>
</tr>
<tr>
<td>Your age</td>
<td><font color="green"><%= age %></font></td>
</tr>
</table>
</body>
</html>
The page2.jsp page also receives the name and age attributes from the main servlet. It simply displays them. Finally, the error.jsp page, which is responsible for displaying an error in the event of incorrect servlet initialization, is as follows:
<%
// jspService
// An error has occurred
String errorMessage = request.getAttribute("errorMessage");
if(errorMessage == null) errorMessage = "Unidentified error";
%>
<!-- start of HTML page -->
<html>
<head>
<title>Next page</title>
</head>
<body>
<h3>Next pages</h3>
<hr>
Application unavailable(<%= msgError %>)
</body>
</html>
It displays the msgError attribute passed to it by the main servlet.
In conclusion, we can see that during the three stages of the application, it is always the main servlet that is queried first by the browser. However, it is not the main servlet that generates the response to be displayed, but one of the four JSP pages. The user does not notice this, as the browser continues to display the initially requested URL—that of the main servlet—in its "Address" field.
