| [ Team LiB ] |
|
19.5 Centralized Request Processing Using a ServletWith resource initialization and access control out of the way, delegated to appropriate component types, we can focus on the implementation of the main application logic. We have already decided to use a servlet as a Controller. With a servlet as the common entry point for all application requests, you gain control over the page flow of the application. The servlet can decide which type of response to generate depending on the outcome of the requested action, such as returning a common error page for all requests that fail, or different responses depending on the type of client making the request. With the help from some utility classes, it can also provide services such as input validation, I18N preparations, and in general, encourage a more streamlined approach to request handling. When you use a servlet as a Controller, you must deal with the following basic requirements:
Here are other features you may want to support, even though they may not be requirements for all applications:
You can, of course, develop a servlet that fulfills these requirements yourself, but there are servlets available as open source that do all of this and more. In this chapter, I describe how to use the servlet from the Apache Struts project (http://jakarta.apache.org/struts/), Version 1.0.2.[3] It's probably the most popular framework for integration with JSP, and its servlet satisfies all our requirements. Using Struts gives you the following benefits:
If you decide to develop your own Controller servlet anyway, the description of how Struts deals with the requirements gives you some good ideas about how to do it. Struts is a large framework. In addition to the Controller servlet and the associated classes, it also contains a number of custom tag libraries[4] that you can use in your JSP pages. I use only a fraction of the Struts servlet functionality in this example and none of the tag libraries. You may want to read the Struts documentation as well to see if your application can use the other features.
19.5.1 Struts Request Processing OverviewWith power comes complexity, unfortunately. Before jumping into the details, here's a brief summary of the parts of Struts I use for the Project Billboard application. The Struts servlet delegates the real processing of requests to classes that extends the Struts Action class. The main method in this class is the perform( ) method. For each type of request the application supports, you create a separate action class and provide the code for processing this request type in the perform( ) method. Figure 19-3 shows the action classes used by the Project Billboard application. Figure 19-3. Controller split over dispatcher servlet and action classes![]() The Struts servlet uses parts of the request URI to figure out which type of request it is, locates the corresponding action class (using configuration information), and invokes the perform( ) method. Note that this method doesn't render a response; it takes care of business logic only, for instance, updating a database. The perform( ) method returns a Struts ActionForward instance, containing information about the JSP page that should be invoked to render the response. The page is identified by a logical name (errorPage, mainPage, etc.), mapped to the real page path in a configuration file. The page flow can therefore be controlled, at least to some extent, by reconfiguration instead of code changes. 19.5.2 Mapping Application Requests to the ServletThe first requirement for using a Controller servlet is that all requests must pass through it. This can be satisfied in many ways. If you have played around a bit with servlets previously, you're probably used to invoking a servlet with a URI that starts with /myApp/servlet. This is a convention introduced by Sun's Java Web Server (JWS), the first product to support servlets before the API was standardized. Some servlet containers still support this convention,[5] even though it's not formally defined in the servlet specification. But using this type of URI has a couple of problems. First, it makes it perfectly clear to a user (at least a user who knows about servlets) what technology implements the application. Not that you shouldn't be proud of using servlets, but a hint like this can help a hacker explore possible security holes; it never hurts to be a bit paranoid when it comes to security. The other problem is of a more practical nature.
As I described in Chapter 17, using relative URIs to refer to resources within an application makes life a lot easier. If a servlet must be invoked using the conventional type of URI, you typically end up with absolute references to the servlet in HTML link and form elements, for example: <form action="/ora/servlet/controller/someAction"> This works, but because the context path (/ora) is part of the URI, it makes it hard to deploy the application with a different context path; you have to change the context path in all pages. There are many ways around this issue, but the best solution is to define a mapping rule for the servlet that makes it possible to invoke the servlet with a URI that has the same structure as requests for the application's JSP and HTML pages. Three types of mapping rules can be defined in the web application's deployment descriptor:
The web container compares each request URI to the defined mapping rules, looking for matches in the order "exact-match," "longest path-prefix," and "extension" and invokes the servlet that's mapped to the first pattern that matches. The exact match rule is rarely used, and the Struts servlet works only with the path- prefix and extension rules. The extension rule, using the extension .do, is the one that's recommended for mapping requests that should be processed by Struts. To define this mapping for the Struts servlet, we add these elements to the application's deployment descriptor (the WEB-INF/web.xml file): <web-app>
...
<servlet>
<servlet-name>action</servlet-name>
<servlet-class>
org.apache.struts.action.ActionServlet
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>action</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
...
</web-app>
First we define a name for the Struts servlet class using the <servlet> element and the nested <servlet-name> and <servlet-class> elements. This definition associates the logical name action with the fully qualified class name org.apache.struts.action.ActionServlet. An extension mapping for this servlet is defined by the <servlet-mapping> element and the nested <servlet-name> and <url-pattern> elements. Note how the value of the <servlet-name> elements in the <servlet> and <servlet-mapping> elements match. With this mapping in place, the container invokes the Struts servlet for all requests that end with .do, making it possible to use a relative reference like this in HTML: <form action="someAction.do"> If you prefer the path-prefix mapping, you need to change the <servlet-mapping> element like this for the Project Billboard application: <servlet-mapping> <servlet-name>action</servlet-name> <url-pattern>/ch19/do/*</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>action</servlet-name> <url-pattern>/ch19/protected/do/*</url-pattern> </servlet-mapping> Note that you need two mappings: one for requests that don't need access control and another for those that do. These mappings tell the container to invoke the Struts servlets for all requests that start with /ch19/do or /ch19/protected/do, allowing a relative reference like this in a page invoked with /ora/ch19/login.jsp: <form action="do/someAction"> It turns out, however, that even with a separate mapping for protected resources, it's easy to bypass the access control for a Struts action when you use the path prefix mapping. I'll show you why in a moment. To avoid security issues, I recommend you stick to the extension-mapping model. 19.5.3 Dispatching Requests to an Action ClassThe second requirement for using a Controller servlet is that the servlet must be able to distinguish requests for different types of actions. The Struts servlet uses a configuration file with mappings from a part of the request path to the corresponding action class to handle this. The file is named struts-config.xml and is located in the WEB-INF directory for the application by default. Example 19-8 shows the configuration file used for the Project Billboard application. Example 19-8. Struts configuration file<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE struts-config PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 1.0//EN"
"http://jakarta.apache.org/struts/dtds/struts-config_1_0.dtd">
<struts-config>
<global-forwards>
<forward name="login" path="/ch19/login.jsp" redirect="true" />
<forward name="main" path="/ch19/protected/main.jsp"
redirect="true" />
</global-forwards>
<action-mappings>
<action path="/ch19/authenticate"
type="com.ora.jsp.servlets.AuthenticateAction" />
<action path="/ch19/logout"
type="com.ora.jsp.servlets.LogoutAction" />
<action path="/ch19/protected/storeMsg"
type="com.ora.jsp.servlets.StoreMsgAction" />
<action path="/ch19/protected/updateProfile"
type="com.ora.jsp.servlets.UpdateProfileAction" />
</action-mappings>
</struts-config>
It's an XML file, as are most configuration files nowadays. The first part of the file defines what Struts calls global forward mappings. I'll get back to them in the next section. The second part contains an <action-mapping> element with nested <action> elements for each action class in the application. For each action, the element attributes specify the context-relative request path for the action and the class name for the corresponding action class. If the Struts servlet is mapped to a path-prefix rule instead of an extension rule in the web.xml file, you must use different paths in the Struts configuration file as well: <action-mappings>
<action path="/authenticate"
type="com.ora.jsp.servlets.AuthenticateAction" />
<action path="/logout"
type="com.ora.jsp.servlets.LogoutAction" />
<action path="/storeMsg"
type="com.ora.jsp.servlets.StoreMsgAction" />
<action path="/updateProfile"
type="com.ora.jsp.servlets.UpdateProfileAction" />
</action-mappings>
Note that only the last part of the URI path identifies the action in this case. To see why it's so, let's look at the method Struts uses to process the request path and figure out which action is requested. A slightly simplified version of the method used by Struts 1.0.2 is shown in Example 19-9. Example 19-9. Extracting the action identifierprotected String processPath(HttpServletRequest request) {
String path = null;
path = request.getPathInfo( );
if ((path != null) && (path.length( ) > 0))
return (path);
path = request.getServletPath( );
int slash = path.lastIndexOf("/");
int period = path.lastIndexOf(".");
if ((period >= 0) && (period > slash))
path = path.substring(0, period);
return (path);
}
The processPath( ) method first calls getPathInfo( ) on the request object to get the part of the path that remains after removing the part the container uses to identify the servlet. For instance, with a path-prefix mapping such as /ch19/protected/do/* for the Struts servlet in the deployment descriptor and a request URI such as /ora/ch19/protected/do/storeMsg, the getPathInfo( ) method returns /storeMsg. If it returns null, it means that an extension mapping is used for the Struts servlet or that the URI is invalid. If so, the getServletPath( ) method is called to get the complete context-relative path for the request. With a mapping such as *.do and a request URI such as /ora/ch19/protected/storeMsg.do, it returns /ch19/protected/doStoreMsg.do. The processPath( ) method strips off the extension part and returns the rest of the path, i.e., /ch19/protected/doStoreMsg. Hence, when you use path-prefix mapping, only the part of the request URI path that comes after the part that identifies the Struts servlet is returned and subsequently finds a matching action, while with an extension mapping, the whole context-relative path is returned and identifies the action. This is what causes the security problem I mentioned earlier. With the access-control filter mapped to /ch19/protected/*, and the Struts servlet mapped to /ch19/do/* and /ch19/protected/do/*, an adventurous user can access a protected action with a URI like /ch19/do/storeMsg instead of /ch19/protected/do/storeMsg, completely bypassing the access-control filter. This means the only secure way to provide access control for Struts actions when you use path-prefix mapping is to do the access control within the actions instead of with a filter. It's easier to just stick to extension mapping, as I recommended earlier. 19.5.4 Implementing the Action ClassesThe servlet mapping rule in the deployment descriptor ensures that all requests reach the Struts servlet, and the action mappings in the struts-config.xml provides the information needed to distinguish different requests from each other. It's finally time to do some good old coding and implement the action classes. Struts creates only a single instance of each action class and uses it for all requests, so you have to ensure that the class is thread-safe in the same way as for a servlet class. Thus, you should avoid using instance variables for anything except read-only access, and synchronize the access to shared data that must be modified. Example 19-10 shows the main part of the action class that handles authentication requests in the Project Billboard application. Example 19-10. Authenticate action classpackage com.ora.jsp.servlets;
import java.io.*;
import java.net.*;
import java.sql.*;
import javax.servlet.*;
import javax.servlet.http.*;
import com.ora.jsp.beans.emp.*;
import org.apache.struts.action.*;
public class AuthenticateAction extends Action {
public ActionForward perform(ActionMapping mapping,
ActionForm form, HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
String userName = request.getParameter("userName");
String password = request.getParameter("password");
ActionForward nextPage = mapping.findForward("main");
EmployeeBean emp = null;
try {
EmployeeRegistryBean empReg = (EmployeeRegistryBean)
getServlet().getServletContext( ).getAttribute("empReg");
emp = empReg.authenticate(userName, password);
}
catch (SQLException e) {
throw new ServletException("Database error", e);
}
if (emp != null) {
// Valid login
HttpSession session = request.getSession( );
session.setAttribute("validUser", emp);
setLoginCookies(request, response, userName, password);
// Next page is the originally requested URL or main
String next = request.getParameter("origURL");
if (next != null && next.length( ) != 0) {
nextPage = new ActionForward(next, true);
}
}
else {
// Invalid login. Redirect to the login page
String loginPage = mapping.findForward("login").getPath( );
String loginURL = loginPage +
"?errorMsg=Invalid+User+Name+or+Password";
nextPage = new ActionForward(loginURL, false);
}
return nextPage;
}
...
}
The class extends the Struts Action class and overrides one method named perform( ). As the name implies, this is the method that performs the processing of the request. It returns an instance of another Struts class, named ActionForward. An ActionForward instance holds three pieces of information: a name, a path to a page (or a servlet), and information about how the specified path should be invoked (through a redirect or a forward). When the perform( ) method returns, the Struts servlet invokes the specified resource, typically a JSP page that renders the response for the request. The perform( ) method has four arguments. The request and response arguments are the same as in a servlet, but the form and mapping arguments contain references to instances of Struts classes. The form argument is a reference to an ActionForm, a class that collects and validates form data. I don't use this feature, because it's tightly coupled to the Struts tag libraries. You can read about it in the Struts documentation to see if it makes sense for your application. The mapping argument holds a reference to an ActionMapping instance. The ActionMapping class encapsulates all mapping information that can be defined in the Struts configuration file. I use only one of its features in this example, namely mappings between logical page names and the actual paths for the pages. This lets me change the page flow for the application without touching the action code. You set these mappings using the <forward> elements, as shown in Example 19-8. A mapping defined by a <forward> element nested within the body of a <global-forwards> element is available to all actions, while a <forward> element nested within an <action> element is available only to that action. All mappings used in the Project Billboard application are global, but local mappings can be handy for an action that uses different, action-specific JSP pages to render the response depending on the outcome of the request processing. Let's look at how all these Struts classes are used in the AuthenticateAction class in Example 19-10. The perform( ) method first retrieves the values of the userName and password request parameters with the getParameter( ) method. It then gets a reference to the ActionForward instance representing the application main page, using the findForward( ) method on the ActionMapping instance. Within a try block, a reference to the EmployeeRegistryBean is retrieved from the servlet context attribute (where the initialization listener placed it when the application was started) and is then used to authenticate the user based on the username and password. If the authentication is successful, the EmployeeBean returned by the authenticate( ) method is saved as a session attribute to serve as an authentication token. The call to setLoginCookies( ) adds the username and password cookies to the response. If the request includes a parameter named origURL, it means that the authentication was triggered by an attempt to load a protected page without being logged in. If so, a new ActionForward( ) instance is created for this page and eventually returned to the Struts servlet to send the user directly to the protected page she tried to load. If the authentication fails, the findForward( ) method gets a reference to the ActionForward instance that represents the login page. But you can't use this instance as is, because you need to add a query string with an error message. The getPath( ) method extracts the page path, and then a new ActionForward instance is created from the combination of the path and the query string. This way, the global forward mapping serves its purpose of removing hardcoded paths in the action code even for a dynamically created URI. Also note that the second argument to the ActionForward constructor is set to false. This tells the Struts servlet it should use the forward method instead of the redirect method to invoke the page, giving the page access to both the original parameters (userName and password) and the new errorMsg parameter. Example 19-11 shows the code for the setLoginCookies( ) method. Example 19-11. Adding cookies to the responseprivate void setLoginCookies(HttpServletRequest request,
HttpServletResponse response, String userName, String password) {
Cookie userNameCookie = new Cookie("userName", userName);
Cookie passwordCookie = new Cookie("password", password);
// Cookie age in seconds: 30 days * 24 hours * 60 min * 60 seconds
int maxAge = 30 * 24 * 60 * 60;
if (request.getParameter("remember") == null) {
// maxAge = 0 to delete the cookie
maxAge = 0;
}
userNameCookie.setMaxAge(maxAge);
passwordCookie.setMaxAge(maxAge);
userNameCookie.setPath(request.getContextPath( ));
passwordCookie.setPath(request.getContextPath( ));
response.addCookie(userNameCookie);
response.addCookie(passwordCookie);
}
The javax.servlet.http.Cookie class is defined by the servlet specification. The setLoginCookies( ) method creates two instances, one each for the username and the password. How long a cookie should be kept by the browser is specified in seconds. In this example I calculate the number of seconds corresponding to 30 days and use it to set the age with the setMaxAge( ) method, unless the user has requested no cookies (the remember parameter is not sent). In this case the maximum age is set to 0, which tells the browser to remove the cookie. The setPath( ) method sets the path attribute for both cookies to the context path for the application. This tells the browser to send only cookies with requests targeted for this application, instead of with all requests for this web server. All the other action classes used by the Project Billboard are similar to the AuthenticateAction described in this section. They all override the perform( ) method, do what they are supposed to do, and use the findForward( ) method to get hold of the correct ActionForward instance to return. The source code for all classes is included in the book examples download. Instead of describing each class here, and boring you with a lot of tedious repetition, I suggest that you instead look at them at your leisure. When you compile your action classes, you must ensure you have both the servlet classes and the Struts classes included in the classpath. You'll find the Struts classes in a JAR file named struts.jar in the Struts installation's lib directory. A copy of this JAR file for Struts 1.0.2 is bundled with the book examples in the WEB-INF/lib directory, but I suggest that you get the latest version directly from the Struts project web site (http://jakarta.apache.org/struts/) instead. 19.5.5 Processing RequestsWhen the Struts servlet receives a request, it first uses the processPath( ) method (Example 19-9) to extract the path part that is mapped to an action class. It then locates, or creates, the instance of the matching action class and calls its perform( ) method. The ActionForward instance returned by the perform( ) method is processed by the Struts servlet's processActionForward( ) method shown in Example 19-12. Example 19-12. Forward processingprotected void processActionForward(ActionForward forward,
ActionMapping mapping, ActionForm formInstance,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
if (forward != null) {
String path = forward.getPath( );
if (forward.getRedirect( )) {
if (path.startsWith("/"))
path = request.getContextPath( ) + path;
response.sendRedirect(response.encodeRedirectURL(path));
} else {
RequestDispatcher rd =
getServletContext( ).getRequestDispatcher(path);
if (rd == null) {
response.sendError(response.SC_INTERNAL_SERVER_ERROR,
internal.getMessage("requestDispatcher", path));
return;
}
rd.forward(request, response);
}
}
}
This method illustrates a number of interesting things about how to pass control to another part of the application—a servlet or a JSP page—that you need to be aware of if you decide to implement your own Controller servlet. The ActionForward argument contains all the information Struts needs to pass control to the next component. Again, this is typically a JSP page that renders the response. The getRedirect( ) method returns true if a redirect response should be returned, ending this request and telling the browser to make a new request for the page that describes the result of the action. In versions of the Servlet API prior to 2.3, the sendRedirect( ) method officially accepted only an absolute URI (e.g., http://localhost:8080/ora/mypage.jsp). But in reality, a server-relative path (a URI without the scheme and server-name parts, e.g., /ora/mypage.jsp) worked fine because all browsers handle such a path correctly in a redirect response, despite the fact that the HTTP specification doesn't allow it. In Version 2.3 of the specification, the absolute URI requirement was relaxed to also allow absolute and relative paths (e.g., /mypage.jsp or mypage.jsp), relying on the container to convert the path to the absolute URI demanded by the HTTP specification. But there's a twist: an absolute path (starting with a slash) is interpreted as a server-relative path by the container instead of as a context-relative path, as is the case for all other methods in the API that use path arguments. This behavior was defined for backward-compatibility reasons since so many existing applications take advantage of the loophole in previous versions of the servlet specification. To shield developers from the path-interpretation issue, the processActionForward( ) method is designed to expect a context-relative path for an ActionForward instance even when the redirect method is used. If the path starts with a slash, the context path is added automatically, resulting in the server-relative absolute path the sendRedirect( ) method can handle. The path passed to sendRedirect( ) method is also processed by the encodeRedirectURL( ). This method inserts the session ID in the URL if the browser doesn't support cookies, as described in Chapter 10. If getRedirect( ) returns false, it means that the forward method should be used to continue the request processing using the resource represented by the specified path. A RequestDispatcher for the path is retrieved from the ServletContext. A RequestDispatcher is a Servlet API class that programmatically invokes another servlet or a JSP page. It has two methods. The include( ) method temporarily passes control to the target, letting it generate a part of the response body but not set any response headers. It corresponds to the <jsp:include> action in a JSP page. The forward( ) method, used here, permanently passes control to the target in the same way as the <jsp:forward> action element in a JSP page. When a request is forwarded, the originating servlet delegates all processing to the target resource. The originating servlet is not allowed to modify the response in any way, neither before calling forward( ) nor when the method returns. In most cases, it should simply return after calling forward( ), possibly after doing some clean up that doesn't involve modifying the response. There are two ways to obtain a RequestDispatcher for a resource identified by a path. In Example 19-12, it is retrieved from the ServletContext. The path argument to its getRequestDispatcher( ) method must be a context-relative path, because the context has no knowledge about the path for the current request. If you want to use a path that's relative to the URI path for the current request, you can instead use the getRequestDispatcher( ) method on the request object. This method, defined in the javax.servlet.ServletRequest interface, accepts both types of paths. 19.5.6 Calling the Controller Servlet from JSP PagesAll that remains to complete the conversion of the Project Billboard application from a pure JSP application to an application that uses a mix of filters, listeners, servlets, and JSP pages is to modify the JSP pages to invoke the Controller servlet. To make the application a little bit more interesting, let's add information about the number of active sessions (loosely, the number of logged in users) to the main page. By moving all request processing to other components, there are only three JSP pages left: login.jsp, main.jsp, and entermsg.jsp. The single change needed in the login.jsp page is the form element's action attribute: <form action="<c:url value="/ch19/authenticate.do" />" method="post"> This application uses resources on different levels in the URI structure, and the login page can be invoked directly by the user as well as by a forwarded request for a resource in the protected directory if the user isn't logged in. The base URI differs depending on how it was invoked—/ora/ch19/login.jsp if it's invoked directly or /ora/ch19/protected/main.jsp if it's invoked through a forward caused by an unauthenticated request for the main page. Thus, a relative path as the action value doesn't work; the browser converts a relative path in a page to an absolute path based in the URI that generated the response, as described in Chapter 17. The solution is to use an absolute path instead. To avoid hardcoding the context path in the page, I use the <c:url> action to convert the context-relative path to a server-relative path. The .do extension tells the container to invoke the Struts servlet. When the Struts servlet processes the request, the processPath( ) method (see Example 19-9) returns /ch19/authenticate, which matches the path mapped to the AuthenticateAction class (see Example 19-10) in the struts-config.xml file (see Example 19-8). Everything is in order and works exactly as intended. The main.jsp page is invoked by the Struts servlet if the authentication succeeds, as commanded by the AuthenticateAction class through the ActionForward instance its perform( ) method returns. The context-relative path for the page is /ch19/protected/main.jsp. The AccessControlFilter (Example 19-7) is mapped to this path, ensuring that only an authenticated user can access the page. The first change in the main.jsp page is therefore to remove the access-control code; it's not needed anymore. To display the number of active sessions, add an EL expression that displays the current value maintained by the SessionCounterListener (Example 19-4) at the beginning of the page: <h1>Welcome ${fn:escapeXml(validUser.firstName)}</h1>
<h2>Number of active sessions: ${session_counter[0]}</h2>
The form and link elements also need attention: 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.do" method="post"> ... </form> <hr> When you're done reading the news, please <a href="http://logout.do">log out</a>. <hr> <a href="entermsg.jsp">Post a new message</a> ... The main.jsp page is always invoked with the /ch19/protected/main.jsp context-relative path, so here a relative URI for the form element's action attribute works fine. Compared to Chapter 13, the only difference is that it refers to the Struts action instead of a JSP page. The link element for the logout action must use a relative reference that moves up one level in the URI namespace: http://logout.do. Remember, the main page was invoked with the /ch19/protected/main.jsp path, but the logout action is unprotected, because it's mapped to the /ch19/logout.do path in the struts-config .xml file. Finally, the entermsg.jsp page; besides removing the access control code, the only change needed is the form element's action attribute: <form action="storeMsg.do" method="post"> It follows the same pattern as the form-element changes in the main.jsp page. |
| [ Team LiB ] |
|