Optimize with a SATA RAID Storage Solution
Range of capacities as low as $1250 per TB. Ideal if you currently rely on servers/disks/JBODs
Page 4 of 4
After the user clicks on the Back button, no round trip back to the Web servers (generally speaking) or the application servers
(in Java's case) takes place. The interaction occurs among the user, the browser, and the cache. So even with the presence
of Listing 3's code in the protected JSP pages such as home.jsp, secure1.jsp, secure2.jsp, and logout.jsp, this code never gets the chance to execute when the Back button is clicked.
Depending on whom you ask, the caches that sit between the application servers and the browsers can either be a good thing or a bad thing. These caches do in fact offer a few advantages, but that's mostly for static HTML pages or pages that are graphic- or image-intensive. Web applications, on the other hand are more data-oriented. As data in a Web application is likely to change frequently, it is more important to display fresh data than save some response time by going to the cache and displaying stale or out-of-date information.
Fortunately, the HTTP "Expires" and "Cache-Control" headers offer the application servers a mechanism for controlling the browsers' and proxies' caches. The HTTP Expires header dictates to the proxies' caches when the page's "freshness" will expire. The HTTP Cache-Control header, which is new under the HTTP 1.1 Specification, contains attributes that instruct the browsers to prevent caching on any desired page in the Web application. When the Back button encounters such a page, the browser sends the HTTP request to the application server for a new copy of that page. The descriptions for necessary Cache-Control headers' directives follow:
no-cache: forces caches to obtain a new copy of the page from the origin server
no-store: directs caches not to store the page under any circumstance
For backward compatibility to HTTP 1.0, the Pragma:no-cache directive, which is equivalent to Cache-Control:no-cache in HTTP 1.1, can also be included in the header's response.
By leveraging the HTTP headers' cache directives, the second sample Web application, logoutSampleJSP2, that accompanies this
article remedies logoutSampleJSP1. logoutSampleJSP2 differs from logoutSampleJSP1 in that Listing 4's code snippet is placed
at the top of all protected JSP pages, such as home.jsp, secure1.jsp, secure2.jsp, and logout.jsp:
Listing 4
//...
response.setHeader("Cache-Control","no-cache"); //Forces caches to obtain a new copy of the page from the origin server
response.setHeader("Cache-Control","no-store"); //Directs caches not to store the page under any circumstance
response.setDateHeader("Expires", 0); //Causes the proxy cache to see the page as "stale"
response.setHeader("Pragma","no-cache"); //HTTP 1.0 backward compatibility
String userName = (String) session.getAttribute("User");
if (null == userName) {
request.setAttribute("Error", "Session has ended. Please login.");
RequestDispatcher rd = request.getRequestDispatcher("login.jsp");
rd.forward(request, response);
}
//...
The combination of setting the headers directives and checking for the username in the HttpSession objects ensures that the browsers will not cache the JSP page. Also, if the user is not logged in, the JSP page's dynamic
content will not be sent to the browsers, but instead the login.jsp page will.
The following behavior results from running the sample Web application logoutSampleJSP2:
logout.jsp, secure1.jsp, and secure2.jsp JSP pages to reappear on the browser. Instead, the login page will appear on the browser, with the message stating "Session
has ended. Please log in.".
loginAction.jsp), the IE and Avant browsers will display a page with the following message:Warning: Page has Expired
The page you requested was created using information you submitted in a form. This page is no longer available. As a security precaution, Internet Explorer does not automatically resubmit your information for you.
To resubmit your information and view this Webpage, click the Refresh button.
Mozilla and FireFox browsers on the other hand display a dialog box with the following message:
The page you are trying to view contains POSTDATA that has expired from cache. If you resend the data, any action from the form carried out (such as a search or online purchase) will be repeated. To resend the data, click OK. Otherwise, click Cancel.
Selecting the Refresh command from the IE and Avant browsers or resending the data from the Mozilla and FireFox browsers will result in the previous JSP page reappearing on the browsers. Obviously, this is not desirable as it defeats the purpose of the logout action. When this happens, the implication is a malicious user can still access someone else's data. This problem, however, only manifests itself when the browsers, as a result of the Back button, return to a page that was the action target of a POST request.
The above problem occurs because the browsers resubmit the data from their cache. In this case, the data consists of the username and password. Despite displaying a security warning message, as in the case of IE, the browsers in fact create an opposite effect.
To fix this problem experienced in logoutSampleJSP2, logoutSampleJSP3's login.jsp should contain—in addition to the username and password—a hidden field called lastLogon that is dynamically initialized with a long value. This long value is obtained by calling System.currentTimeMillis() and denotes the current number of milliseconds since January 1, 1970. When the form in login.jsp is submitted, loginAction.jsp first compares the value from this field against the lastLogon field's value in the User database table. Only when the lastLogon value from the form is greater than the value in the database will it be considered a valid login.
For valid login, the field lastLogon in the database should be updated with the form's value to update the lastLogon's time. When the browsers resubmit from the cache as in the case above, the form's lastLogon value is not greater than the database's lastLogon value, therefore loginAction.jsp forwards the control flow back to login.jsp with an error message stating "Session has ended. Please log in.". Listing 5 shows the code snippet from loginAction.jsp:
Listing 5
//...
RequestDispatcher rd = request.getRequestDispatcher("home.jsp"); //Forward to homepage by default
//...
if (rs.getString("password").equals(password)) { //If valid password
long lastLogonDB = rs.getLong("lastLogon");
if (lastLogonForm > lastLogonDB) {
session.setAttribute("User", userName); //Saves username string in the session object
stmt.executeUpdate("update USER set lastLogon= " + lastLogonForm + " where userName = '" + userName + "'");
}
else {
request.setAttribute("Error", "Session has ended. Please login.");
rd = request.getRequestDispatcher("login.jsp"); }
}
else { //Password does not match, i.e., invalid user password
request.setAttribute("Error", "Invalid password.");
rd = request.getRequestDispatcher("login.jsp");
}
//...
rd.forward(request, response);
//...
To facilitate the above approach, you must track each user's lastLogon time. For the RDBMS security realm, this can be easily accomplished by extending the schema of the User or an equivalent table by adding a column denoting the lastLogon time. LDAP or other security realms require more thought, but the lastLogon approach can certainly work with those realms.
There are probably many ways to represent the lastLogon time. The sample logoutSampleJSP3 utilizes the number of milliseconds since January 1, 1970. This method would also work
in an extreme case where the same user account is used by multiple people from multiple separate browsers for logging in.
Running the sample logoutSampleJSP3 shows that it handles the logout problem properly. Once the user has logged out, clicking on the browser's Back button will not cause any of the protected JSP pages to display under any circumstances. This sample Web application shows how the logout process is correctly handled without requiring any additional training or assumption on the users. No user experience is compromised.
In production-quality code, some redundant code can be eliminated. One way is to factor out the code in Listing 4 into a separate
JSP page, which, in turn, can be included in other JSP pages by using the tag <jsp:include>.
Another alternative and a better approach to developing Web applications rather than straight JSP pages or JSP pages/servlets
is to use Struts. For Struts-based Web applications, adding a framework to handle the logout problem properly can be easily
and elegantly achieved. This is partially due to the fact that Struts is based on the Model-View-Controller design pattern
and therefore allows a clear separation between the code in the model and in the view. In addition, as Java is an object-oriented
language and supports inheritance, code reuse can be better achieved than any kind of scripting in JSP. With Struts, the code
in Listing 4 can be moved out of the JSP page and into Action class's execute() method.
Furthermore, using inheritance, a base Action class can be defined to extend the Struts Action class and contain the code similar to Listing 4 in its execute() method. Other action classes can subclass the base Action class and, by virtue of inheritance, inherit the common logic for setting the header directives and checking for the username
string in the HttpSession object. The base Action class is made abstract and defines an abstract method executeAction(). All subclasses of the base Action class must now implement the executeAction() method rather than override the execute() method. With this inheritance hierarchy in place, all of the base Action's subclasses no longer need to worry about any plumbing logout code. They would instead only contain code pertaining to their
normal business logic. Listing 6 shows a portion of the base Action class:
Listing 6
public abstract class BaseAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
response.setHeader("Cache-Control","no-cache"); //Forces caches to obtain a new copy of the page from the origin server
response.setHeader("Cache-Control","no-store"); //Directs caches not to store the page under any circumstance
response.setDateHeader("Expires", 0); //Causes the proxy cache to see the page as "stale"
response.setHeader("Pragma","no-cache"); //HTTP 1.0 backward compatibility
if (!this.userIsLoggedIn(request)) {
ActionErrors errors = new ActionErrors();
errors.add("error", new ActionError("logon.sessionEnded"));
this.saveErrors(request, errors);
return mapping.findForward("sessionEnded");
}
return executeAction(mapping, form, request, response);
}
protected abstract ActionForward executeAction(ActionMapping mapping,
ActionForm form, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException;
private boolean userIsLoggedIn(HttpServletRequest request) {
if (request.getSession().getAttribute("User") == null) {
return false;
}
return true;
}
}
Listing 6 resembles Listing 4, with the only difference being that ActionMapping findForward replaces RequestDispatcher forward. In Listing 6, if the username string is not found in HttpSession, the ActionMapping object will find the forward element named sessionEnded and forward the control flow to its corresponding path. Otherwise, the subclasses will be given a chance to carry their own
business operations through their implementation of the executeAction() method. For this reason, it is important that the action forwards specified in the struts-web.xml configuration file for all the base Action's subclasses to declare a forward element named sessionEnded, with its path pointing to /login.jsp. Listing 7 uses the secure1 action to illustrate such a declaration:
Listing 7
<action path="/secure1" type="com.kevinhle.logoutSampleStruts.Secure1Action" scope="request"> <forward name="success" path="/WEB-INF/jsps/secure1.jsp"/> <forward name="sessionEnded" path="/login.jsp"/> </action>
The Secure1Action class subclasses from the BaseAction class, therefore providing implementation for the executeAction() method instead of overriding the execute() method. Secure1Action does not need to perform any logout plumbing code, as shown in Listing 8:
Listing 8
public class Secure1Action extends BaseAction {
public ActionForward executeAction(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
HttpSession session = request.getSession();
return (mapping.findForward("success"));
}
}
The above solution is effective and elegant since it does not require any convoluted work-around code—it only requires defining
a BaseAction class. Factoring out common behavior in some sort of base Action class (that extends the Struts Action class) is recommended anyway and a common practice already for many Struts development projects.
As simple and as elegant as the solutions presented above are for both JSP- and Struts-based Web applications, they do have a couple of limitations, one of which has a work-around. In any case, in my opinion, these limitations are rather minor:
Action class needs to subclass from the Struts Action class instead of the BaseAction class.
User agents often have history mechanisms, such as "Back" buttons and history lists, which can be used to redisplay an entity retrieved earlier in a session.
History mechanisms and caches are different. In particular history mechanisms SHOULD NOT try to show a semantically transparent view of the current state of a resource. Rather, a history mechanism is meant to show exactly what the user saw at the time when the resource was retrieved.
Fortunately, Microsoft Internet Explorer and Mozilla-based browsers outnumber Opera browsers. The solution presented still satisfies a large number of users. In addition, with or without the solution presented, Opera browsers have always had the logout problem. Nothing has changed as far as Opera browsers are concerned. However, as specified in RFC 2616, with the headers' directives above, Opera browsers disallow stale pages to be served from the cache when a link is clicked.
This article has described the solutions to the logout problem. The solutions are surprisingly simple yet functional in all conditions. Whether for JSP or for Struts, all it takes is no more than 50 lines of code and a way to track the user's last logon time. Incorporating these solutions in a password-protected Web application benefits users by ensuring their private data cannot be compromised under any condition while improving the user experience.
Read more about Enterprise Java in JavaWorld's Enterprise Java section.
Archived Discussions (Read only)
(