Security is one of those aspects of development that often gets relegated to the end of the programming process. It seems like customers expect security features can be bolted on to an application. What makes things worse is you can implement security in many ways. When it comes to building security into an application, determine the requirements before you start coding. Additionally, it can be tempting to let technology drive requirements. Some technologies are flexible and let you get away with this, but when it comes to security, this is not the case. Choosing one security approach or technology over another without understanding what the user wants and needs can leave you scrambling at the end of the development cycle. You don’t want to be recoding your entire security layer before your application goes into production.
Security features center around two basic concepts: authentication and authorization. Users authenticate to the system to prove they are who they say they are. Authorization allows or disallows access to certain application features. Authentication requirements are usually specific and concrete:
Anybody can access the welcome page of the application. But if a users attempt to access any other page they will have to log in with a username and password.
Authorization needs tend to be broad and vague. In many cases, authorization rules can be complex, particularly if the authorization requirements are based on hierarchical or matrix-style organizations:
Users can only look at the data they own. The user’s manager can look at any information for users that work for him. Administrators can look at anybody’s data but can only modify the data if approved by a manager.
Before you start coding to meet these requirements, ensure you understand what the various approaches to security can and cannot do. Security mechanisms for J2EE and Java-based applications fall into two broad categories: container-managed security and application-managed security. Container-managed security is specified as part of the J2EE and Servlet specifications. Any spec-compliant application server—such as Tomcat, JBoss, Weblogic, or Websphere—supports container-managed security. Since Tomcat isn’t an EJB container, it supports container-managed security as specified in the Servlet specification. Full-blown J2EE containers will support container-managed security for EJB applications and web applications.
Many think container-managed security is too restrictive. It’s somewhat ironic that using container-managed security can lead to portability issues. The reason is that each container implements container-managed security in different ways. For example, each container has its own way of configuring security realms. A security realm provides data to the container used to authenticate and authorize users. This data could come from a database, an LDAP server, a flat file, or some other storage repository. If you use container-managed security, you’ll have to make changes to your application’s configuration if you want to deploy the application on a different application server. Several recipes related to setting up container-managed security are presented in this chapter.
With application-managed security, you’re
responsible for developing the security features of your
specifications. You’ll have to write more code;
however, you won’t have to compromise on the
requirements. Many applications manage their own security
requirements for this reason alone. Servlet filters, available on
containers that support the Servlet 2.3 specification, provide a
great vehicle for implementing application-managed security. This
chapter provides many recipes focused on application-managed
security. These recipes range from the use of base
Action
s to full-blown custom filters for
authentication and authorization.
SecurityFilter is an open source servlet filter that provides the convenience and features of container-managed security yet allows the flexibility of application-managed security. This chapter includes a recipe that shows you how to use this powerful tool.
The Solutions and code samples in this chapter provide examples that demonstrate various approaches to security. As you look at the Recipes in this chapter, keep in mind that Security is rarely something a single solution can cover. Instead, you may want to employ a combination of solutions to fulfill your requirements.
You need to ensure that users are logged in before they can access certain actions.
Create a base
Action
, like the one
shown in Example 11-1, which implements the security
policy.
package com.oreilly.strutsckbk.ch11; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.struts.action.Action; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import org.apache.struts.webapp.example.Constants; import org.apache.struts.webapp.example.User; public abstract class SecureAction extends Action { // final so cannot be overridden public final ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { HttpSession session = request.getSession( ); User user = (User) session.getAttribute(Constants.USER_KEY); // send back to the logon page if no user if (user == null) return (mapping.findForward("logon")); return doExecute(mapping, form, request, response, user); } public abstract ActionForward doExecute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response, User user) throws Exception; }
Concrete Action
s that require this policy extend
the base SecureAction
, shown in Example 11-2.
package com.oreilly.strutsckbk.ch11; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import org.apache.struts.webapp.example.User; public class TestSecureAction extends SecureAction { public ActionForward doExecute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response, User user) throws Exception { // do real work here return mapping.findForward("success"); } }
The base SecureAction
is an abstract class that
wraps the execute( )
method with the security
policy. In the Solution, the policy is simple. The
User
object is retrieved from the
HttpSession
; if this object is not found, then the
user must not be logged in; control is forwarded to
“logon.” Otherwise, the abstract
doExecute( )
method is called. In addition to the
standard execute( )
arguments, the
User
object is passed through. Your concrete
subclasses then implement doExecute( )
instead of
execute( )
.
Enforcing security through a base SecureAction
is
an understandable approach. However, this solution requires that
every concrete Action
must subclass
SecureAction
. If you are using the
DispatchAction
or other Struts built-in
Action
s, you’ll need to create
your own subclasses for these that perform the security checks.
In a team environment, you’ll need to ensure all
developers abide by these rules. More importantly, the base
Action
approach only provides security for HTTP
requests that go through your base Action
. It
doesn’t provide any security for directly accessed
JSPs, static HTML pages, and other resources accessed outside of
Struts or through a different Action
. If you
ensure all requests go through your base Action
,
then this may not be an issue; however, many applications are not
written in such a way.
The Base Action technique is discussed in Programming Jakarta Struts by Chuck Cavaness (O’Reilly).
The Struts JavaDoc for the Action class can be found at http://struts.apache.org/api/org/apache/struts/action/Action.html.
You want to be able to check if a user is logged in for any request
made to an action
in your
struts-config.xml file, but you
don’t want to have to subclass a custom base
Action
class.
Create a custom request processor, overriding the
processPreprocess( )
or the
processActionPerform( )
method. The custom request
processor shown in Example 11-3 retrieves the user
object from the HTTP session. If this object is null, an HTTP error
response of 403 (Forbidden) is returned.
package org.apache.struts.action; import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.struts.webapp.example.Constants; import org.apache.struts.webapp.example.User; public class CustomRequestProcessor1 extends RequestProcessor { protected boolean processPreprocess(HttpServletRequest request, HttpServletResponse response) { HttpSession session = request.getSession( ); User user = (User) session.getAttribute(Constants.USER_KEY); if (user == null) { try { response.sendError(403, "User not logged in"); } catch (IOException e) { log.error("Unable to send response"); } return false; } return true; } }
If you need to use the Struts objects passed to an
Action
’s execute()
method, such as the ActionForm
and
ActionMapping
, override the
processActionPerform( )
method as shown in Example 11-4. In this example, if the
user
object is null
, control is
forwarded to the logon
Struts forward; otherwise,
the base
RequestProcessor
.processActionPerform()
method is called to continue normal processing.
package org.apache.struts.action; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.struts.webapp.example.Constants; import org.apache.struts.webapp.example.User; public class CustomRequestProcessor2 extends RequestProcessor { protected ActionForward processActionPerform(HttpServletRequest request, HttpServletResponse response, Action action, ActionForm form, ActionMapping mapping) throws IOException, ServletException { HttpSession session = request.getSession( ); User user = (User) session.getAttribute(Constants.USER_KEY); if (user == null) { return mapping.findForward("logon"); } else { return super.processActionPerform(request, response, action, form, mapping); } } }
You deploy the custom request processor using the
controller
element in the
struts-config.xml file:
<controller type="com.oreilly.strutsckbk.ch11.CustomRequestProcessor1" />
The Solutions shown in this recipe allow you to enforce a
security policy across
any requests handled by Struts without having to create and extend a
special base Action
. The first approach (Example 11-3) overrides the processPreprocess(
)
method of the Struts RequestProcessor
.
This method is a general-purpose preprocessing method. The
implementation in the base RequestProcessor
is a
no-op. You return true
from this method to
continue normal processing. If you return false
,
the RequestProcessor
assumes that the response has
been written and it aborts normal processing. In Example 11-3, the method checks for a
user
object in the session. If one is found, the
method returns true
; otherwise, the method sends
the HTTP error code of 403 (Forbidden).
The second approach overrides the processActionPerform(
)
method. Like the first approach, the user object is
retrieved from the HTTP session. Unlike the first approach, however,
Struts API objects are available. If the user object
isn’t found, you call the findForward()
method on the ActionMapping
to forward
to “logon.” Otherwise, you call the
super.processActionPerform( )
method where the
base RequestProcessor
calls
action.execute( )
and handles any thrown
exception:
protected ActionForward processActionPerform(HttpServletRequest request, HttpServletResponse response, Action action, ActionForm form, ActionMapping mapping) throws IOException, ServletException { try { return (action.execute(mapping, form, request, response)); } catch (Exception e) { return (processException(request, response, e, form, mapping)); } }
The two approaches shown here apply the security policy to all
Action
s. If needed, you can customize the Solution
so the login check is selectively applied. In the first approach,
which overrides processPreprocess( )
, you could
look for a special request parameter or attribute that indicates if
the login check is required. In the second approach, you could employ
a custom ActionMapping
containing a property
indicating if the action was secured or not.
Like using a base Action
, directly accessed JSPs
and static HTML pages aren’t secured by the
Solution. These resources can be selectively secured using a custom
JSP tag as shown in Recipe 11.3.
The Struts RequestProcessor
also processes roles.
You can override the
processRoles()
method to achieve custom handling, as shown in Recipe 11-4. You can learn about additional
RequestProcessor
methods in the Struts
User’s Guide
“Controller” section at http://struts.apache.org/userGuide/building_controller.html.
Custom action mappings are discussed in Recipe 2.8.
You want to ensure users can access a JSP page only if they are logged in.
Use a custom JSP tag, like the checkLogon
tag from
the Struts Mail Reader example application, on pages that require
users to be logged in. The checkLogon
tag is shown
in Example 11-5.
package org.apache.struts.webapp.example; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpSession; import javax.servlet.jsp.JspException; import javax.servlet.jsp.tagext.TagSupport; import org.apache.struts.config.ModuleConfig; /** * Check for a valid User logged on in the current session. If there is no * such user, forward control to the logon page. * * @author Craig R. McClanahan * @author Marius Barduta * @version $Revision: 1.5 $ $Date: 2005/03/21 18:08:09 $ */ public final class CheckLogonTag extends TagSupport { // --------------------------------------------------- Instance Variables /** * The key of the session-scope bean we look for. */ private String name = Constants.USER_KEY; /** * The page to which we should forward for the user to log on. */ private String page = "/logon.jsp"; // ----------------------------------------------------------- Properties /** * Return the bean name. */ public String getName( ) { return (this.name); } /** * Set the bean name. * * @param name The new bean name */ public void setName(String name) { this.name = name; } /** * Return the forward page. */ public String getPage( ) { return (this.page); } /** * Set the forward page. * * @param page The new forward page */ public void setPage(String page) { this.page = page; } // ----------- Public Methods ----------------- /** * Defer our checking until the end of this tag is encountered. * * @exception JspException if a JSP exception has occurred */ public int doStartTag( ) throws JspException { return (SKIP_BODY); } /** * Perform our logged-in user check by looking for the existence of * a session scope bean under the specified name. If this bean is not * present, control is forwarded to the specified logon page. * * @exception JspException if a JSP exception has occurred */ public int doEndTag( ) throws JspException { // Is there a valid user logged on? boolean valid = false; HttpSession session = pageContext.getSession( ); if ((session != null) && (session.getAttribute(name) != null)) { valid = true; } // Forward control based on the results if (valid) { return (EVAL_PAGE); } else { ModuleConfig config = (ModuleConfig) pageContext.getServletContext( ).getAttribute( org.apache.struts.Globals.MODULE_KEY); try { pageContext.forward(config.getPrefix( ) + page); } catch (ServletException e) { throw new JspException(e.toString( )); } catch (IOException e) { throw new JspException(e.toString( )); } return (SKIP_PAGE); } } /** * Release any acquired resources. */ public void release( ) { super.release( ); this.name = Constants.USER_KEY; this.page = "/logon.jsp"; } }
Include the tag at the start of a page that requires users to be logged in. Example 11-6 lists the mainMenu.jsp taken from the Struts Mail Reader example application.
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib uri="/tags/app" prefix="app" %> <%@ taglib uri="/tags/struts-bean" prefix="bean" %> <%@ taglib uri="/tags/struts-html" prefix="html" %> <%-- Check if the user is logged in and redirect to logon if not --%> <app:checkLogon/> <html> <head> <title><bean:message key="mainMenu.title"/></title> <link rel="stylesheet" type="text/css" href="base.css" /> </head> <h3><bean:message key="mainMenu.heading"/> <bean:write name="user" property="fullName" /></h3> <ul> <li><html:link action="/EditRegistration?action=Edit"><bean:message key="mainMenu.registration"/></html:link></li> <li><html:link forward="logoff"><bean:message key="mainMenu.logoff"/> </html:link></li> </ul> </body> </html>
If you use directly accessed JSP pages,
you will need a mechanism to secure those pages. With a custom JSP
tag, you can create the logic in one place and reuse the
functionality throughout your application. The
checkLogon
tag, shown in Example 11-5 and applied in Example 11-6,
attempts to retrieve an object from the HTTP session stored under a
certain name. The name
property defaults to the
value defined by Constants.USER_KEY
. If the object
isn’t found, the tag forwards to a module-relative
page specified by the page
property. This value
defaults to /logon.jsp.
You can use this tag in your own applications even if you store the
user under a different name in the session and you want to forward to
a different page. In the following snippet, the user object is stored
under the name user
. If the object cannot be
found, the tag redirects to the Register
action:
... <%-- Check if there's a user and redirect to registration if not --%> <app:checkLogon name="user" page="/Register.do"/> ...
Like using a base Action
, this custom JSP tag only
protects JSP pages on which it is included. It does
not provide security for actions, static HTML
pages, or other web resources.
Recipe 11.1
Section 11.1 shows
you how to secure Action
s in the same way that the
Solution secures JSP pages.
Recipe 11.6 shows a more comprehensive mechanism, applicable to any web resource, for checking that a user is logged in.
Use the roles
attribute of the
action
element to specify the roles that are
permitted to use the action:
<!-- Display all users -->
<action path="/ViewUsers"
forward="/view_users.jsp"
roles="manager,sysadmin"
/>
Struts actions, configured via the action
element
in the struts-config.xml file, can be restricted
to certain roles using the roles
attribute. This
attribute accepts a comma-separated list of role names. When a
request is received for the action, the
RequestProcessor.processRoles( )
method checks
that the user has at least one of the roles specified. If the user
doesn’t have one of the roles, the HTTP 403 error
(Forbidden) is sent; otherwise, processing continues normally. Here
is the processRoles( )
method from the Struts
RequestProcessor
:
protected boolean processRoles( HttpServletRequest request,
HttpServletResponse response,
ActionMapping mapping )
throws IOException, ServletException {
// Is this action protected by role requirements?
String roles[] = mapping.getRoleNames( );
if ((roles == null) || (roles.length < 1)) {
return (true);
}
// Check the current user against the list of required roles
for (int i = 0; i < roles.length; i++) {
if ( request.isUserInRole(roles[i])
) {
if (log.isDebugEnabled( )) {
log.debug(" User '" + request.getRemoteUser( ) +
"' has role '" + roles[i] + "', granting access");
}
return (true);
}
}
// The current user is not authorized for this action
if (log.isDebugEnabled( )) {
log.debug(" User '" + request.getRemoteUser( ) +
"' does not have any required role, denying access");
}
response.sendError(
HttpServletResponse.SC_FORBIDDEN,
getInternal( ).getMessage("notAuthorized", mapping.getPath( )));
return (false);
}
The Struts RequestProcessor
determines if a user
has a role using the HttpServletRequest.isUserInRole(
)
method. This method can only be used if you are using
container-managed security (or if you use a Solution such as the one
in Recipe 11.10). For application-managed
security, you can create your own custom
RequestProcessor
that overrides the
processRoles
method.
In Example 11-7, the
processRoles(
)
method retrieves a User
object from
the HTTP session. Then, the hasRole( )
method is
called on this object to determine if the user has at least one of
the required roles.
package com.oreilly.strutsckbk.ch11; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.*; import org.apache.struts.action.*; import org.apache.struts.webapp.example.User; public class RoleRequestProcessor extends RequestProcessor { protected boolean processRoles( HttpServletRequest request, HttpServletResponse response, ActionMapping mapping ) throws IOException, ServletException { // Is this action protected by role requirements? If not, return true. String roles[] = mapping.getRoleNames( ); if ((roles == null) || (roles.length < 1)) { return true; } // Check the current user against the list of required roles HttpSession session = request.getSession( ); User user = (User) session.getAttribute("user"); if (user == null) { return false; } for (int i = 0; i < roles.length; i++) { if (user.hasRole(roles[i])) { return (true); } } // The user does not have one of the roles; send an error response.sendError( HttpServletResponse.SC_BAD_REQUEST, getInternal( ).getMessage("notAuthorized", mapping.getPath( ))); return (false); } }
This custom request processor is deployed using the
controller
element in the
struts-config.xml file:
<controller processorClass="com.oreilly.strutsckbk.ch11. RoleRequestProcessor"/>
The customization, shown in Example 11-7, used to
code the processRoles( )
method to perform checks
can be as complex as you want. You have complete access to the
servlet request and response, servlet context, HTTP session, and the
ActionMapping
.
Since container-managed security typically supports a simple flat role structure, using application-managed security along with a custom request processor allows you to handle more complex hierarchical schemes. Because you have access to the servlet response, you can redirect a user to a different URL if that user doesn’t pass the security test.
You can learn about additional RequestProcessor
methods in the Struts User’s Guide
“Controller” section at http://struts.apache.org/userGuide/building_controller.html.
If you want to implement container-managed security for your application, see Recipe 11.9.
You want to provide a “remember me” feature so a user’s username and password are prefilled on the logon form if that user has logged on before.
In your Action
that logs a user in, create
persistent cookies containing the user’s base-64
encoded username and password. The private
saveCookies( )
and removeCookies( )
methods shown in Example 11-8 manipulate the cookies as needed.
package com.oreilly.strutsckbk.ch11; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.commons.beanutils.PropertyUtils; import org.apache.struts.action.Action; import org.apache.struts.action.ActionErrors; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import com.oreilly.servlet.Base64Encoder; public final class MyLogonAction extends Action { public ActionForward execute( ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { HttpSession session = request.getSession( ); ActionErrors errors = new ActionErrors( ); String username = (String) PropertyUtils.getSimpleProperty(form, "username"); String password = (String) PropertyUtils.getSimpleProperty(form, "password"); boolean rememberMe = ((Boolean) PropertyUtils.getSimpleProperty( form, "rememberMe")).booleanValue( ); // Call your security service here //SecurityService.authenticate(username, password); if (rememberMe) { saveCookies(response, username, password); } else { removeCookies(response); } session.setAttribute("username", username); return mapping.findForward("success"); } private void saveCookies(HttpServletResponse response, String username, String password) { Cookie usernameCookie = new Cookie("StrutsCookbookUsername", Base64Encoder.encode(username)); usernameCookie.setMaxAge(60 * 60 * 24 * 30); // 30 day expiration response.addCookie(usernameCookie); Cookie passwordCookie = new Cookie("StrutsCookbookPassword", Base64Encoder.encode(password)); passwordCookie.setMaxAge(60 * 60 * 24 * 30); // 30 day expiration response.addCookie(passwordCookie); } private void removeCookies(HttpServletResponse response) { // expire the username cookie by setting maxAge to 0 // (actual cookie value is irrelevant) Cookie unameCookie = new Cookie("StrutsCookbookUsername", "expired"); unameCookie.setMaxAge(0); response.addCookie(unameCookie); // expire the password cookie by setting maxAge to 0 // (actual cookie value is irrelevant) Cookie pwdCookie = new Cookie("StrutsCookbookPassword", "expired"); pwdCookie.setMaxAge(0); response.addCookie(pwdCookie); } }
When a user goes to the logon page, fill the username and password
fields with values from the cookie, decoded from base-64, using the
Struts bean:cookie
tag, as shown in Example 11-9.
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="com.oreilly.servlet.*" %> <%@ taglib uri="/tags/struts-bean" prefix="bean" %> <%@ taglib uri="/tags/struts-html" prefix="html" %> <html> <head> <title>Struts Cookbook - Cookie Logon</title> </head> <body> <html:errors/> <html:form action="/SubmitCookieLogon" focus="username"> <bean:cookie id="uname" name="StrutsCookbookUsername" value=""/> <bean:cookie id="pword" name="StrutsCookbookPassword" value=""/> <table border="0" width="100%"> <tr> <th align="right"> <bean:message key="prompt.username"/>: </th> <td align="left"> <html:text property="username" size="16" maxlength="18" value="<%=Base64Decoder.decode(uname.getValue( ))%>"/> </td> </tr> <tr> <th align="right"> <bean:message key="prompt.password" bundle="alternate"/>: </th> <td align="left"> <html:password property="password" size="16" maxlength="18" redisplay="false" </td> </tr> <tr> <th align="right"> <bean:message key="prompt.rememberMe"/>: </th> <td align="left"> <html:checkbox property="rememberMe"/> </td> </tr> <tr> <td align="right"> <html:submit property="Submit" value="Submit"/> </td> <td align="left"> <html:reset/> </td> </tr> </table> </html:form> </body> </html>
A cookie consists of a name-value data pair that can be sent to a client’s browser and then read back again at a later time. Browsers provide security for cookies so a cookie can only be read by the server that originally created it. Cookies must have an expiration period.
Though cookies are in widespread use and are supported by modern browsers, they do pose a privacy risk. Most browsers allow the user to disable them. You can design your web application to use cookies to improve the user experience, but you shouldn’t require users to use cookies.
The logon Action
of Example 11-8
retrieves the username
and
password
from the logon form. This form includes
the true/false property rememberme
, which
indicates if the users want their login credentials remembered. If
users want to be remembered, they check the checkbox for the
rememberme
property. In the
MyLogonAction
—if
rememberme
is true—the cookies are created
and saved in the response. If rememberme
is false,
the cookies for username
and
password
have their maxAge
set
to 0
, effectively removing them from the response.
The
bean:cookie
tags used in Example 11-9 retrieve the
cookie values from the request and store them in scripting variables.
These tags specify the empty string
(“”) as the default value in case
cookies are disabled. The initial values for the login form fields
are set to the values from the scripting variables.
The Solution shown here does not address cookie security issues. For a production system, the data sent in the cookies should be encrypted. A simple encryption scheme, such as MD5 or a variant of the Secure Hash Algorithm (SHA), can be used to encrypt the cookie value when it is created. Since the server creates the cookie and is the only party that can legitimately use the data, it can encrypt and decrypt the data using the algorithm of its own choosing. Alternatively, you can send the cookies only over HTTPS, thereby providing encryption/decryption at the transport level.
You can use cookies to log in a user automatically; in other words, if users have a cookie(s) with valid credentials for the web application they don’t have to submit the login form at all. The automatic login approach is shown in Recipe 11.7.
Recipe 11.10 shows how to use the open source SecurityFilter software to implement “remember me” functionality. Its implementation includes support for cookie encryption and other settings.
Java Servlet Programming
by Jason Hunter
(O’Reilly) covers servlet development from top to
bottom, including the Cookie APIs. The Base64 encoder and decoder
used in this recipe are part of the companion
com.oreilly.servlet
classes available from
http://www.servlets.com/cos/.
The foundation of Java server-side cookie handling is the Servlet Specification and API, available for download from http://java.sun.com/products/servlet/download.html.
The JavaBoutique has a nice tutorial on server-side cookie handling found at http://javaboutique.internet.com/tutorials/JSP/part09/. Alexander Prohorenko has written a good article on cookie security issues for O’Reilly’s ONLamp.com site at http://www.onlamp.com/pub/a/security/2004/04/01/cookie_vulnerabilities.html.
You need to verify the user is logged in and authenticated when a request is received for any URL path of your web application.
Use an
authentication servlet filter,
such as the one in Example 11-10, which checks for a
User
object in the session.
package com.oreilly.strutsckbk.ch11.ams; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; public class AuthenticationFilter implements Filter { private String onFailure = "logon.jsp"; private FilterConfig filterConfig; public void init(FilterConfig filterConfig) throws ServletException { this.filterConfig = filterConfig; onFailure = filterConfig.getInitParameter("onFailure"); } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; // if the requested page is the onFailure page continue // down the chain to avoid an infinite redirect loop if (req.getServletPath( ).equals(onFailure)) { chain.doFilter(request, response); return; } // get the session or create it HttpSession session = req.getSession( ); User user = (User) session.getAttribute("user"); if (user == null) { // redirect to the login page res.sendRedirect(req.getContextPath( )+onFailure); } else { chain.doFilter(request, response); } } public void destroy( ) { } }
Servlet filters provide a convenient way to apply across-the-board
processing of a servlet request. Servlet filters were introduced in
the Servlet 2.3 specification. Most all containers support Servlet
2.3, so you can probably use them in your application. The filter
looks for a User
object from the
HttpSession
. If the object is found, then
processing continues normally. If the object isn’t
found, then a redirect to the page specified by the
onFailure
initialization parameter is sent in the
response.
You create a filter by implementing the Filter
interface, declare the filter in your web.xml
file, and map URLs to it using URL patterns (see the
Sidebar 11-1). In
Example 11-11, the filter is declared by the
filter
element. The init-param
element specifies the context-relative path of the URL to redirect if
authentication fails.
<filter> <filter-name>AuthenticationFilter</filter-name> <filter-class> com.oreilly.strutsckbk.ch11.ams.AuthenticationFilter </filter-class> <init-param> <param-name>onFailure</param-name> <param-value>/logon.jsp</param-value> </init-param> </filter> <filter-mapping> <filter-name>AuthenticationFilter</filter-name> <url-pattern>/reg/*</url-pattern> </filter-mapping>
The value of the url-pattern
in the
filter-mapping
determines the URLs to be filtered.
In this case, the filter mapping indicates users must be logged in
before they can access a URL that matches the
url-pattern
/reg/*. URLs that
match this pattern include /reg/Main.do,
/reg/viewReg.jsp,
/reg/help.html, and
/reg/sub/viewSub.jsp.
Recipe 11.7 shows a servlet filter that authenticates using cookies. Recipe 11.8 presents a servlet filter that can be used for authorization.
Recipe 11.10 shows how to use the open source SecurityFilter software for providing functionality similar to the filter presented in this recipe.
Java Servlet Programming by Jason Hunter (O’Reilly) covers servlet filters in-depth. Sun’s Java site has a good article on the essentials of servlet filters. It can be found at http://java.sun.com/products/servlet/Filters.html.
You want to allow users to be logged in automatically if they have valid credentials stored in a cookie(s).
Use a servlet filter, such as the one shown in Example 11-12, that looks for cookies containing the user’s credentials. The credentials are used to authenticate the user. If the authentication succeeds, the user is automatically logged in; otherwise, the user will be prompted to login.
package com.oreilly.strutsckbk.ch11; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Filter which handles application authentication. The filter implements * the following policy: * <ol> * <li>If the username is in the session the filter exits; * <li>If not, the authentication cookies are looked for; * <li>If found, the authentication is attempted * <li>If authentication is successful, the username is stored in * the session * <li>Otherwise, the cookies are invalid and subsequently removed * from the response * </ol> * * @author Bill Siggelkow */ public class AutomaticLoginFilter implements Filter { private String onFailure = "logon.jsp"; public void init(FilterConfig filterConfig) throws ServletException { this.filterConfig = filterConfig; onFailure = filterConfig.getInitParameter("onFailure"); } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; String contextPath = req.getContextPath( ); // if the requested page is the onFailure page continue // down the chain to avoid an infinite redirect loop if (req.getServletPath( ).equals(onFailure)) { chain.doFilter(request, response); return; } // get the session or create it HttpSession session = req.getSession( ); String username = (String) session.getAttribute("username"); if (log.isDebugEnabled( )) log.debug("User in session:"+username); // if user is null get credentials from cookie; otherwise continue if (username == null) { boolean authentic = false; username = findCookie(req, "StrutsCookbookUsername"); String password = findCookie(req, "StrutsCookbookPassword"); if (username != null && password != null) { try { if (log.isDebugEnabled( )) log.debug("Checking authentication"); // Call your security service here //SecurityService.authenticate(username, password); session.setAttribute("username", username); authentic = true; } catch (Exception e) { log.error("Unexpected authentication failure.", e); clearCookie(res, "StrutsCookbookUsername"); clearCookie(res, "StrutsCookbookPassword"); } } // if not authentic redirect to the logon page if (!authentic) { //redirect to the onFailure page, alternatively we could send //an HTTP error code such as 403 (Forbidden) res.sendRedirect(contextPath+onFailure); //abort filter instead of chaining return; } } if (log.isDebugEnabled( )) log.debug("Continuing filter chain ..."); chain.doFilter(request, response); } public void destroy( ) { // Nothing necessary } private String findCookie(HttpServletRequest request, String cookieName) { Cookie[] cookies = request.getCookies( ); String value = null; if (cookies != null) { for (int i=0; i<cookies.length; i++) { if (cookies[i].getName( ).equals(cookieName)) { value = cookies[i].getValue( ); } } } return value; } private void clearCookie(HttpServletResponse response, String cookieName) { // the cookie value does not matter Cookie cookie = new Cookie(cookieName, "expired"); // setting maxAge to 0 effectively removes the cookie cookie.setMaxAge(0); response.addCookie(cookie); } private FilterConfig filterConfig; private static final Log log = LogFactory.getLog(AutomaticLoginFilter. class); }
This Solution assumes that cookies for the username and password have been stored in the request; this filter does not store the cookies.
For that functionality, you need to use an Action
such as the one shown in Recipe 11.5.
You map the servlet filter shown in the Solution to any application
URLs requiring user authentication. If users aren’t
authenticated, they should be redirected to the page specified by the
onFailure
initialization parameter (defaults to
logon.jsp
). You describe the
filter’s configuration using the
filter
and filter-mapping
elements in the web.xml file, as shown in Example 11-13.
<filter> <filter-name>AutomaticLoginFilter</filter-name> <filter-class> com.oreilly.strutsckbk.ch11.AutomaticLoginFilter </filter-class> <init-param> <param-name>onFailure</param-name> <param-value>/my_logon.jsp</param-value> </init-param> </filter> <filter-mapping> <filter-name>AutomaticLoginFilter</filter-name> <url-pattern>/reg/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>AutomaticLoginFilter</filter-name> <url-pattern>/admin/menu.do</url-pattern> </filter-mapping>
When a request is received, the servlet filter attempts to retrieve
specific cookie values for the username and password. If the values
aren’t present, control will be redirected to the
onFailure
page. If the cookies are present, the
username and password will be verified using a
SecurityService
. If authentic, the request is
passed to the next filter in the chain, effectively allowing the
request to proceed as normal. If not authentic, the cookie values
themselves are invalid and not legitimate. The cookies are removed
and control is redirected to the onFailure
page.
From a developer’s perspective, one of the more interesting aspects of this filter is the following bit of code:
if (req.getServletPath( ).equals(onFailure)) { chain.doFilter(request, response); return; }
When this filter was first written (by yours truly), this block was
omitted. The filter was mapped to all application URLs
(/
) and the application was deployed. When an
attempt was made to access to any part of the application, a browser
message was displayed indicating too many redirects had been
attempted. What happened was that when the filter redirected to the
logon page, the request was routed back through the filter,
essentially creating an HTTP infinite loop. To fix this problem, the
code block was added to skip the authentication check if the request
path is the same as the onFailure
path.
Recipe 11.10 shows how to use the open source SecurityFilter software for providing similar functionality as the filter presented in this recipe.
Java Servlet Programming by Jason Hunter (O’Reilly) covers Servlet development in-depth, including filters and the cookie-related APIs.
You need to verify the user is authorized to access selected URLs based on the user’s security role and profile.
Use a servlet filter such as the one shown in Example 11-14.
package com.oreilly.strutsckbk.ch11.ams; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.struts.Globals; import org.apache.struts.action.ActionErrors; import org.apache.struts.action.ActionMessage; public class AuthorizationFilter implements Filter { public void init(FilterConfig filterConfig) throws ServletException { String roles = filterConfig.getInitParameter("roles"); if (roles == null || "".equals(roles)) { roleNames = new String[0]; } else { roles.trim( ); // use the new split method of JDK 1.4 roleNames = roles.split("\s*,\s*"); } onFailure = filterConfig.getInitParameter("onFailure"); if (onFailure == null || "".equals(onFailure)) { onFailure = "/index.jsp"; } } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; HttpSession session = req.getSession( ); User user = (User) session.getAttribute("user"); ActionErrors errors = new ActionErrors( ); if (user != null) { boolean hasRole = false; for (int i = 0; i < roleNames.length; i++) { if (user.hasRole(roleNames[i])) { hasRole = true; break; } } if (!hasRole) { errors.add(ActionErrors.GLOBAL_MESSAGE, new ActionMessage( "error.authorization.required")); } } if (errors.isEmpty( )) { chain.doFilter(request, response); } else { req.setAttribute(Globals.ERROR_KEY, errors); req.getRequestDispatcher(onFailure).forward(req, res); } } public void destroy( ) { } private String[] roleNames; private String onFailure; }
Servlet filters, introduced as part of the Servlet 2.3 specification, provide for custom request and response processing that can be applied across any (and all) web resources. Filters can alter a request before it arrives at its destination and, likewise, can modify the response after it leaves a destination. Filters can be applied to static HTML pages, JSP pages, Struts actions, essentially any resource that you can specify with a URL.
You can use a filter to prohibit or allow access to resources based
on any user information. Container-managed security (Recipe 11.9) provides a limited form of this capability,
known as role-based access control. With a filter, you can implement
role-based access control or any other security policy desired. The
Solution shows an example usage that implements custom role-based
authorization. This filter performs a similar security check as the
custom RoleRequestProcessor
of Example 11-14.
Initialization parameters specify the required roles and the page to
forward to if the authorization check fails. For each combination of
roles and mapped URLs, you can deploy a separate instance of the
filter class. Each instance, specified by the
filter
element, can have its own set of
initialization parameters and filter mappings. Example 11-15, taken from the web.xml
file, shows a deployment that uses two instances of the same
authorization filter.
<filter> <filter-name>adminAuthFilter</filter-name> <filter-class> com.oreilly.strutsckbk.ch11.ams.AuthorizationFilter </filter-class> <init-param> <param-name>roles</param-name> <param-value>admin</param-value> </init-param> <init-param> <param-name>onFailure</param-name> <param-value>/index.jsp</param-value> </init-param> </filter> <filter> <filter-name>managerAuthFilter</filter-name> <filter-class> com.oreilly.strutsckbk.ch11.ams.AuthorizationFilter </filter-class> <init-param> <param-name>roles</param-name> <param-value>manager,asstManager</param-value> </init-param> <init-param> <param-name>onFailure</param-name> <param-value>/index.jsp</param-value> </init-param> </filter> <filter-mapping> <filter-name>adminAuthFilter</filter-name> <url-pattern>/admin/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name> managerAuthFilter </filter-name> <url-pattern>/mgr/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name> managerAuthFilter </filter-name> <url-pattern>/usr/*</url-pattern> </filter-mapping>
Unlike other filters in this chapter, the authorization filter of
Example 11-14 truly integrates with Struts. If a user
doesn’t have a required role, then a set of
ActionError
s are created and stored as a request
attribute. Though the filter only has access to the request and
response, it integrates with Struts based on knowing how Struts
stores the ActionErrors
in the request. Unlike
Action
s, filters don’t have
access to Struts helper methods for accessing objects, like
ActionErrors
, that may be stored in the servlet
request or session. To get around this, you can retrieve Struts
objects. By using the
constants
defined in the org.apache.struts.Globals
class,
you can protect your code from future changes to Struts internals.
For a complete application-managed security solution, you will want
to implement an authentication mechanism. Recipe 11.6 presents a servlet filter that can be used for
this purpose. To enforce authentication prior to authorization, list
the authentication filter before the authorization filter in the
filter-mapping
declarations.
Recipe 11.10 shows how to use the open source SecurityFilter software for providing similar functionality as the filter presented in this recipe.
Java Servlet Programming by Jason Hunter (O’Reilly) covers servlet filters in-depth. Sun’s Java site has a good article on the essentials of servlet filters. It can be found at http://java.sun.com/products/servlet/Filters.html.
You want to let the container manage security for your Struts application instead of you having to write all the Java code to support log in (authentication) and access checks (authorization).
Use container-managed security, as defined by the Java Servlet Specification.
A servlet container or J2EE application server can manage security for web applications. Container-managed security provides three main features:
You can specify to the container how users are to be authenticated using a login configuration. You indicate if you want the browser to prompt for the username and password, or if you want to use your own custom login page.
You can establish security constraints that allow users with certain roles access to specific URLs of the application. If users attempt to access a page to which they aren’t authorized, they will be prompted to login using the login configuration.
You can specify which URLs should be accessed using a secure protocol. In practical terms, you indicate which pages can be accessed with the HTTPS protocol (HTTP over Secure Socket Layer).
You configure container-managed security using special XML elements in your web.xml, as shown in Example 11-16.
<?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> <display-name>Struts Cookbook - Chapter 11 : CMS</display-name> <!-- Action Servlet Configuration --> <servlet> <servlet-name>action</servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> <init-param> <param-name>config</param-name> <param-value>/WEB-INF/struts-config.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <!-- Action Servlet Mapping --> <servlet-mapping> <servlet-name>action</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> <!-- The Welcome File List --> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <!-- Container-managed security configuration --> <security-constraint> <!-- At least one web-resource collection --> <web-resource-collection> <web-resource-name>RegPages</web-resource-name> <description>Registered user pages</description> <url-pattern>/reg/*</url-pattern> </web-resource-collection> <auth-constraint> <!-- Zero or more role-names --> <role-name>jscUser</role-name> </auth-constraint> </security-constraint> <security-constraint> <web-resource-collection> <web-resource-name>AdminPages</web-resource-name> <description>Administrative pages</description> <url-pattern>/admin/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>jscAdmin</role-name> </auth-constraint> <!-- Switch to HTTPS for the admin pages --> <user-data-constraint> <transport-guarantee>CONFIDENTIAL</transport-guarantee> </user-data-constraint> </security-constraint> <login-config> <auth-method>FORM</auth-method> <realm-name>StrutsCookbookCh11</realm-name> <form-login-config> <form-login-page>/cma_logon.jsp</form-login-page> <form-error-page>/cma_logon_error.jsp</form-error-page> </form-login-config> </login-config> <security-role> <description>Registered User</description> <role-name>jscUser</role-name> </security-role> <security-role> <description>Administrators</description> <role-name>jscAdmin</role-name> </security-role> </web-app>
You use the security-constraint
element to apply
constraints to one or more web resource collections—i.e., a set
of URLs. URL patterns (see the Sidebar 11-1) identify the URLs that
comprise each collection. The auth-constraint
element identifies the user roles, specified using
role-name
elements, which can access a constrained
URL. If users attempt to access a constrained URL, they must log in
based on settings in the login-config
element.
The login-config
element indicates the
authentication to be performed and where the user information can be
found. A web application can have one login configuration. The
auth-method
nested element
indicates the type of authentication and accepts the values detailed
in Table 11-1.
Authentication method |
Description |
BASIC |
The browser pops up a dialog allowing the user to enter a username and password. The username and password are Base-64 encoded and sent to the server. |
FORM |
Allows for a custom form to be specified. The form must contain a
|
DIGEST |
Just like BASIC authentication, except that the
|
CLIENT-CERT |
The client is required to provide a digital certificate for authentication. This is the most secure configuration and is the most costly; certificates for production use must be purchased from a Certificate Authority. |
The majority of applications employing container-managed security use
BASIC or FORM-based authentication. With
FORM-based
authentication, the form-login-page
element
specifies an HTML or JSP page that users use to submit their
authentication credentials. That page, like the one shown in Example 11-17, must submit to the form action named
j_security_check
and have form fields named
j_username
and j_password
.
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean" %> <html> <head> <title><bean:message key="logon.cma.title"/></title> </head> <body> <form action="j_security_check"> <table border="0" width="100%"> <tr> <th align="right"> <bean:message key="prompt.username"/>: </th> <td align="left"> <input type="text" name="j_username" size="16" maxlength="18"> </td> </tr> <tr> <th align="right"> <bean:message key="prompt.password"/>: </th> <td align="left"> <input type="password" name="j_password" size="16" maxlength="18"> </td> </tr> <tr> <td align="right"> <input type="submit" value="Submit"> </td> <td align="left"> <input type="reset"> </td> </tr> </table> </form> </body> </html>
In addition to specifying the login, the login configuration may also specify a security realm. A security realm is essentially the store from which a web application retrieves and verifies user credentials. In addition, a realm provides a mechanism for specifying the roles users may have.
The user data for authentication comes from the security realm. The security realm serves as a reference to container-specific security storage. Realms can be based, for example, on property files, XML files, a relational database, or an LDAP-server. Some containers, such as JBoss, provide a mapping between a realm and a Java Authentication and Authorization Service (JAAS) implementation. The mechanism for associating the logical realm named in the web.xml to the concrete realm varies by container.
If you are experimenting with container-managed security and using Tomcat, you can use Tomcat’s UserDatabase realm. In a default configuration, Tomcat supports this realm across all deployed applications. The realm uses usernames, passwords, roles, and role assignments specified in the conf/tomcat-users.xml file. A sample file is shown in Example 11-18.
<?xml version='1.0' encoding='utf-8'?> <tomcat-users> <role rolename="jscUser"/> <role rolename="tomcat"/> <role rolename="role1"/> <role rolename="manager"/> <role rolename="jscAdmin"/> <role rolename="admin"/> <user username="bsiggelkow" password="crazybill" roles="jscUser,jscAdmin"/> <user username="tomcat" password="tomcat" roles="tomcat"/> <user username="role1" password="tomcat" roles="role1"/> <user username="both" password="tomcat" roles="tomcat,role1"/> <user username="gpburdell" password="gotech" roles="jscUser"/> <user username="admin" password="admin" roles="admin,manager"/> </tomcat-users>
When users attempt to access an authorization-constrained URL, they
will be challenged to enter authentication credentials. If you are
using FORM-based authentication, the
form-login-page
will be displayed. Once the user
has been authenticated, your web application can glean useful user
data from the HTTP request. The HttpServletRequest
provides three particular methods enabled when using
container-managed security: getUserPrincipal( )
;
getRemoteUser( )
, which returns user identity
information, such as the username; and isUserInRole()
, which determines if a user has a specified role. These
methods can be used in your Action
classes to
perform such things as the following:
Loading the user’s profile and storing it in the session
Rendering a specific response or redirect to a certain URL based on the user’s role
Allowing role-based access to Action
s as
configured in the struts-config.xml file
Hiding or displaying presentation components (links, buttons, menus,
etc.) based on a user’s role (using the
logic:present
and
logic:notPresent
tags)
A drawback to the
challenge/response authentication
model of container-managed security is that users must attempt access
to a constrained URL to log in. This behavior can make it difficult
for a user to log in proactively. A common trick to permit proactive
logins is to create a link on an unsecured page to an
authorization-constrained JSP page. Because the JSP page is secured,
the user will be forced to log in. The secured JSP page then
redirects back to the original unsecured page using the
logic:redirect
Struts tag:
<%@ taglib uri="http://struts.apache.org/tags-logic.tld" prefix="logic" %> <logic:redirect page="/index.jsp"/>
Many web applications place the login form on every publicly
accessible page, usually near the top of the page on the left or
right. With container-managed security, however, this technique
can’t easily be employed. The container itself can
only reference the login form specified in the
login-config
. This limitation forces the chicanery
required to emulate a proactive login.
There is no easy workaround for this problem. If you need this capability, as many applications do, use application-managed security. To get the best of both worlds, consider using the Solution shown in Recipe 11.10.
Container-managed security allows you to force portions of your applications to run under the Secure Socket Layer (SSL) transport, using HTTP over SSL (HTTPS). The example web.xml file (Example 11-16) configures the AdminPages to effectively run under the HTTPS:
<security-constraint> <web-resource-collection> <web-resource-name>AdminPages</web-resource-name> <description>Administrative pages</description> <url-pattern>/admin/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>jscAdmin</role-name> </auth-constraint> <!-- Switch to HTTPS for the admin pages --> <user-data-constraint> <transport-guarantee>CONFIDENTIAL</transport-guarantee> </user-data-constraint> </security-constraint>
The transport-guarantee
element accepts values of
NONE
, INTEGRAL
, and
CONFIDENTIAL
. Specifying either of the latter two
values requires requests to the URLs to use the HTTPS protocol over a
secured port (typically port 443 or 8443). The value of
NONE
indicates no particular transport security is
required.
Specifying a transport-guarantee
of
NONE
won’t make the container
switch from the secured protocol (https
) to the
unsecured protocol (http
). Unless you specify
http
on a request, the application will continue
to use https
. If you need to switch protocols,
consider using the Solution shown in Recipe 11.11.
Most application servers and servlet containers accept the HTTPS protocol. However, you may need to configure the container.
Container-managed security is convenient and easy to configure, but it can make your application inflexible to changing security requirements and less portable between application servers. Recipe 11-10 provides a Solution that mitigates these problems.
Enabling a servlet container to support https
varies by application server. Tomcat provides a simple
how-to for this. For Tomcat 5.0, the relevant
documentation can be found at http://jakarta.apache.org/tomcat/tomcat-5.0-doc/ssl-howto.html.
If you need finer-grained control of transport security than can be provided by container-managed security, consider using the Solution shown in Recipe 11.11.
You want the convenience of container-managed security, yet need a custom mechanism for implementing your security policies.
Use the SecurityFilter (http://securityfilter.sourceforge.net) custom servlet filter and associated classes.
Container-managed security, as shown in Recipe 11.9, has some advantages:
When users attempt to access a protected URL, the container automatically prompts them to logon. Once authenticated, they are forwarded to the originally requested URL.
The user identity can be determined using the
getUserPrincipal( )
or getRemoteUser(
)
methods of the HttpServletRequest
.
These methods can determine if a user is logged in.
You can determine if a user has a specific role using the
isUserInRole(
roleName
)
method of the HttpServletRequest
. Struts leverages
this feature to provide role-constrained actions via the roles
attribute. Struts provides for role-specific page generation using
the logic:present
role=
"roleNames
"
custom JSP tag.
Container-managed security has drawbacks, such as portability. With container-managed security, the implementation is split between your web application and the application server. You usually must configure container-specific resources to specify the repository, known as a security realm, from which the container acquires the user’s credentials and roles. Container-managed security will only prompt users to login if they attempt access of a protected URL. Users cannot log in by going to a known page and entering their username and password. This restriction makes it difficult, for example, to include a login form on every page.
The SecurityFilter servlet filter and related classes provide a
hybrid of container-managed security and application-managed security
that solves most of these problems. SecurityFilter permits
implementation of a custom security policy yet allows programmatic
access to user identity and role information via the standard
HttpServletRequest
methods. You configure the
SecurityFilter through an XML file. The format of this file is near
identical to the security-constraint elements used for
container-managed security in the web.xml. Example 11-19 shows a sample
securityfilter-config.xml
file. This example is similar to the web.xml for
container-managed security shown in Recipe 11.9.
<?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE securityfilter-config PUBLIC "-//SecurityFilter.org//DTD Security Filter Configuration 2.0//EN" "http://www.securityfilter.org/dtd/securityfilter-config_2_0.dtd"> <securityfilter-config> <security-constraint> <web-resource-collection> <web-resource-name>RegPages</web-resource-name> <description>Registered user pages</description> <url-pattern>/reg/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>jscUser</role-name> </auth-constraint> </security-constraint> <security-constraint> <web-resource-collection> <web-resource-name>AdminPages</web-resource-name> <url-pattern>/admin/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>jscAdmin</role-name> </auth-constraint> </security-constraint> <!-- Use this login-config to test BASIC authentication --> <!-- <login-config> <auth-method>BASIC</auth-method> <realm-name>StrutsCookbookCh11</realm-name> </login-config> --> <login-config> <auth-method>FORM</auth-method> <realm-name>StrutsCookbookCh11</realm-name> <form-login-config> <form-login-page>/sf_logon.jsp</form-login-page> <form-error-page>/sf_logon_error.jsp</form-error-page> <form-default-page>/Welcome.do</form-default-page> </form-login-config> </login-config> <security-role> <description>Regular Users</description> <role-name>jscUser</role-name> </security-role> <security-role> <description>Administrators</description> <role-name>jscAdmin</role-name> </security-role> <realm className="com.oreilly.strutsckbk.ch11.sf.MemorySecurityRealm"/> </securityfilter-config>
Like container-managed security, with SecurityFilter you can specify
allowed roles for URLs using the
security-constraint
element. SecurityFilter
supports BASIC and FORM-based authentication. Unlike
container-managed security, SecurityFilter allows for a user to
perform an “unsolicited” login.
That is, the user can log in without having to attempt access to a
protected URL. In this scenario, once logged in, the user will be
forwarded to the page specified in the
form-default-page
element. The logon page that you
specify for the form-login-page
element follows
the same convention as container-managed security. The logon form,
sf_logon.jsp, shown in Example 11-20, submits the j_username
and
j_password
fields to
j_security_check
.
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean" %> <html> <head> <title>Security Filter : Logon Page</title> </head> <body> <form method="POST" action="j_security_check"> <table border="0" width="100%"> <tr> <th align="right"> <bean:message key="prompt.username"/>: </th> <td align="left"> <input type="text" name="j_username" size="16" maxlength="18"> </td> </tr> <tr> <th align="right"> <bean:message key="prompt.password"/>: </th> <td align="left"> <input type="password" name="j_password" size="16" maxlength="18"> </td> </tr> <tr> <td align="right"> <input type="submit" value="Submit"> </td> <td align="left"> <input type="reset"> </td> </tr> </table> </form> </body> </html>
With container-managed security, you have to separate the security
realm configuration and
code from the rest of the web application. The configuration is
usually part of a container-specific XML file, and the code usually
needs to be placed in a separate JAR file or in the
server’s classpath. With SecurityFilter, however,
you include the configuration and code for your realm with your web
application. It all gets bundled together in the same WAR file. Your
custom realm must implement the
SecurityRealmInterface
interface (shown in Example 11-21). SecurityFilter is licensed under the
SecurityFilter Software License, derived from and compatible with the
Apache Software License. (For brevity, the license has been excluded
in this example.)
package org.securityfilter.realm; import java.security.Principal; public interface SecurityRealmInterface { /** * Authenticate a user. * * @param username a username * @param password a plain text password, as entered by the user * * @return a Principal object representing the user if successful, * false otherwise */ public Principal authenticate(String username, String password); /** * Test for role membership. * * Use Principal.getName( ) to get the username from the principal object. * * @param principal Principal object representing a user * @param rolename name of a role to test for membership * * @return true if the user is in the role, false otherwise */ public boolean isUserInRole(Principal principal, String rolename); }
If you want to work with only usernames and passwords and
don’t need to create Principal
s,
you can extend the SimpleSecurityRealmBase
class.
The custom MemorySecurityRealm
, shown in Example 11-22, extends this class and implements the
booleanAuthenticate( )
and isUserInRole()
methods by delegating to a custom security service.
package com.oreilly.strutsckbk.ch11.sf; import org.securityfilter.realm.SimpleSecurityRealmBase; public class MemorySecurityRealm extends SimpleSecurityRealmBase { private SecurityService serviceImpl = new SecurityServiceImpl( ); public boolean booleanAuthenticate(String username, String password) { try { User user = serviceImpl.authenticate(username, password); if (user != null) return true; } catch (SecurityException e) { e.printStackTrace( ); } return false; } public boolean isUserInRole(String username, String role) { User user = serviceImpl.findUser(username); return user == null ? false : user.hasRole(role); } }
The realm is configured and deployed in the securityfilter-config.xml file:
<realm className="com.oreilly.strutsckbk.ch11.sf.MemorySecurityRealm"/>
Optionally, you can declaratively set properties on a custom realm
using the realm-param
element:
<realm className="fully.qualified.classname.of.SecurityRealm">
<realm-param name="propertyName
" value="propertyValue
" /> </realm>
You describe the deployment of the actual SecurityFilter servlet filter in the web.xml file as shown in Example 11-23.
<?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> <display-name>Struts Cookbook - Chapter 11 : SecurityFilter</display-name> <!-- Security Filter --> <filter> <filter-name>Security Filter</filter-name> <filter-class>org.securityfilter.filter.SecurityFilter</filter-class> <init-param> <param-name>config</param-name> <param-value>/WEB-INF/securityfilter-config.xml</param-value> <description> Configuration file location (this is the default value) </description> </init-param> <init-param> <param-name>validate</param-name> <param-value>true</param-value> <description>Validate config file if set to true</description> </init-param> </filter> <!-- map all requests to the SecurityFilter --> <filter-mapping> <filter-name>Security Filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- Action Servlet Configuration --> <servlet> <servlet-name>action</servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> <init-param> <param-name>config</param-name> <param-value>/WEB-INF/struts-config.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <!-- Action Servlet Mapping --> <servlet-mapping> <servlet-name>action</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> <!-- The Welcome File List --> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>
It’s best to map all requests to the filter. Let the securityfilter-config.xml file set the security constraints for specific URLs.
SecurityFilter provides support for automatic logins using cookies. This gives you a similar capability to the custom solution shown in Recipe 11.7. Furthermore, you can configure all the details about the cookies, such as expiration and encryption, in the securityfilter-config.xml file, as shown in Example 11-24.
<form-login-config> <!--Logon page must contain a checkbox for j_rememberme --> <form-login-page>/sf_logon.jsp</form-login-page> <form-error-page>/sf_logon_error.jsp</form-error-page> <form-default-page>/Welcome.do</form-default-page> <!-- remember-me config --> <remember-me className="org.securityfilter.authenticator.persistent. DefaultPersistentLoginManager"> <!-- optional settings for default persistent login manager --> <remember-me-param name="cookieLife" value="15"/> <remember-me-param name="protection" value="all"/> <remember-me-param name="useIP" value="true"/> <remember-me-param name="encryptionAlgorithm" value="DES"/> <remember-me-param name="encryptionMode" value="ECB"/> <remember-me-param name="encryptionPadding" value="PKCS5Padding"/> <!-- encryption keys; customize for each application --> <!-- NOTE: these kys must be speciied AFTER other encryption settings --> <remember-me-param name="validationKey" value="347382902489402489754895734890347"/> <remember-me-param name="encryptionKey" value="347892347028490237487846240673842"/> </remember-me> </form-login-config>
You enable the “remember me”
capability by adding a
checkbox to your logon form with the name
of j_rememberme
. Here’s what you
would add to the logon page shown in Example 11-20:
<tr> <th align="right">Remember me:</th> <td align="left"> <input type="checkbox" name="j_rememberme" value="true"> </td> </tr>
The behavior you get with the SecurityFilter will look and feel like
container-managed security. If users attempt to access a protected
page, they will be prompted to log in. Of course, if they use the
“remember me” feature, they can
automatically log in. They can log in without having to attempt
access to a protected page. Once authenticated, control is forwarded
to the form-default-page
.
Using SecurityFilter, you can use the getUserPrincipal(
)
, getRemoteUser( )
, and
isUserInRole( )
methods of the
HttpServletRequest
as if you were using full-blown
container-managed security. Struts support for roles—the
roles
attribute of the action
element and the roles
attribute of the
logic:present
tag—will work as intended.
The SecurityFilter project’s home page can be found at http://securityfilter.sourceforge.net. SecurityFilter provides a framework for authentication and authorization using a servlet filter. To understand how filters work for these purposes, take a look at Recipes Section 11.6 and Section 11.8.
Recipe 11.9 discusses the use of container-managed security.
You want to control if HTTPS is required on a page-by-page basis.
The Struts SSL Extension (SSLEXT), an open source Struts plug-in,
enables you to indicate if an action requires the secure
(https
) protocol. Steve Ditlinger created and
maintains this project (with others), hosted at
http://sslext.sourceforge.net.
SSLEXT enables fine-grained secure protocol control by providing:
The SSLEXT distribution consists of a plug-in class for
initialization (SecurePlugIn
), a custom request
processor (SecureRequestProcessor
), and a custom
action mapping class (SecureActionMapping
).
If you have been using custom RequestProcessor
or
ActionMapping
classes and you want to use SSLEXT,
you will need to change these classes to extend the corresponding
classes provided by SSLEXT.
For JSP pages, SSLEXT provides custom extensions of Struts tags for
generating protocol-specific URLs. A custom JSP allows you to
indicate if a JSP page requires https
. SSLEXT
depends on the Java Secure Socket Extension (JSSE). JSSE is included
with JDK 1.4 or later. If you’re using an older JDK,
you can download JSSE from Sun’s Java site. Finally,
you’ll need to enable SSL for your application
server. For Tomcat, this can be found in the Tomcat SSL
How-To documentation.
SSLEXT works by intercepting the request in its
SecureRequestProcessor
. If the request is directed
toward an action that is marked as secure
, the
SecureRequestProcessor
will generate a redirect.
The redirect will change the protocol to https
and
the port to a secure port (e.g., 443 or 8443). Switching protocols
sounds simple; however, a request in a Struts application usually
contains request attributes, and these attributes are lost on a
redirect. SSLEXT solves this problem by temporarily storing the
request attributes in the session.
You can download the SSLEXT distribution from the project web site. SSLEXT doesn’t include a lot of documentation, but it comes with sample applications that demonstrate its use and features. If all your requests go through Struts actions, you can apply SSLEXT without modifying any Java code or JSP pages. Here’s how you would apply SSLEXT to a Struts application:
Copy the sslext.jar file into your application’s WEB-INF/lib folder.
If you need to use the custom JSP tags, copy the sslext.tld file into the WEB-INF/lib folder.
Make the following changes to the struts-config.xml file:
Add the type
attribute to the
action-mappings
element to specify the custom
secure action mapping class:
<action-mappings type="org.apache.struts.config.SecureActionConfig">
Add the controller
element for the secure request
processor:
<controller processorClass="org.apache.struts.action.SecureRequestProcessor" />
Add the plug-in
declaration to load the SSLEXT
code:
<plug-in className="org.apache.struts.action.SecurePlugIn"> <set-property property="httpPort" value="80"/> <set-property property="httpsPort" value="443"/> <set-property property="enable" value="true"/> <set-property property="addSession" value="true"/> </plug-in>
Set the secure
property to true
for any action
you want to be accessed using
https
:
<action path="/reg/Main" type="com.oreilly.strutsckbk.ch11.ssl.MainMenuAction"> <!-- Force this action to run secured --> <set-property property="secure" value="true"/> <forward name="success" path="/reg/main.jsp"/> </action>
Set the secure
property to
false
for any action
that you
only want to run under an unsecured protocol
(http
):
<action path="/Welcome" type="com.oreilly.strutsckbk.ch11.ssl.WelcomeAction"> <!-- Force this action to run unsecured --> <set-property property="secure" value="false"/> <forward name="success" path="/welcome.jsp"/> </action>
If you have accessible JSP pages you want to specify as secured (or
unsecured), use the SSLEXT pageScheme
custom JSP
tag:
<%@ taglib uri="http://www.ebuilt.com/taglib" prefix="sslext"%> <sslext:pageScheme secure="true"/>
Now rebuild and deploy the application. When you click on a link to a
secured action, the protocol will switch to
https
and the port to the
secure port (e.g., 8443
or
443
). If you go to an action
marked as unsecured, the protocol and port should switch back to
http
and the port to the standard port (e.g.,
8080
or
80
). If you access an action without a specified
value for the secure property or the value is set to
any
, then the protocol won’t
switch when you access the action. If you’re under
http
, the protocol will remain
http
; if you’re under
https
, the protocol will remain
https
.
Be careful if you switch from a secured to unsecured protocol (https to http). Critical user-specific data, such as the current session ID, can be snooped by a hacker. The hacker could use this data to hijack the session and imposter the user. Here is a good rule to follow: Once you switch to https, stay in https.
You can use SSLEXT alongside container-managed security mechanisms for specifying secure transport. The container-managed security approach works well when you want to secure entire portions of your application:
<security-constraint> <web-resource-collection> <web-resource-name>AdminPages</web-resource-name> <description>Administrative pages</description> <url-pattern>/admin/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>jscAdmin</role-name> </auth-constraint> <!-- Switch to HTTPS for the admin pages --> <user-data-constraint> <transport-guarantee>CONFIDENTIAL</transport-guarantee> </user-data-constraint> </security-constraint>
You can then use SSLEXT for fine-grained control of the protocol at the action level.
Enabling an application server to support https
varies. Tomcat provides a how-to for this. For Tomcat 5.0, the
relevant documentation can be found at http://jakarta.apache.org/tomcat/tomcat-5.0-doc/ssl-howto.html.
SSLEXT is hosted on SourceForge at http://sslext.sourceforge.net.
Craig McClanahan presents a good argument against switching back to
http
from https
. His comments
can be found in a struts-user mailing list
thread archived at http://www.mail-archive.com/[email protected]/msg81889.html.
Recipe 11.9 shows how you can specify the protocol in the web.xml file. This approach, presented as part of the J2EE tutorial, can be found at http://java.sun.com/j2ee/1.4/docs/tutorial/doc/Security4.html.
You want to limit the size of a file to be uploaded to your application.
In the struts-config.xml file, set the
maxFileSize
attribute on the
controller
element to the maximum accepted size
(in bytes) for an uploaded file. In this example, a single uploaded
file must be smaller than 700 KB:
<controller maxFileSize="700K"/>
Whether intended or not, users may attempt to upload excessively
large files to your web application. In most cases, the user
accidentally picked the wrong file; however, a malicious user could
be attempting to bring down your application. You can restrict
uploads to a maximum file size using the
maxFileSize
attribute on the
controller
element in your
struts-config.xml file. The value for this
attribute is expressed as an integer value optionally followed by a
“K,”
“M,” or
“G,” interpreted as kilobytes,
megabytes or gigabytes, respectively. If you specify the integer
valuewith no units indicated, the value will be interpreted as bytes.
If you attempt to upload a file larger than the acceptable maximum,
the FormFile
property of the
ActionForm
will be null
. You
can handle this condition in your Action
that
processes the upload, as shown in Example 11-25.
public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { // Get the form file property from the form UploadForm uploadForm = (UploadForm) form; FormFile content = uploadForm.getContent( ); if (content == null) { ActionMessage msg = new ActionMessage("error.maxFileSize.exceeded"); ActionMessages errors = new ActionMessages( ); errors.add(ActionMessages.GLOBAL_MESSAGE, msg); saveErrors(request, errors); return mapping.getInputForward( ); } // continue processing upload ...
If you don’t specify the
maxFileSize
attribute, the
default maximum will be 250 MB
(250M
). If you want a size different size than
this, you must specify it (as shown in the Solution).
The controller
element supports a related
attribute with the name of memFileSize
. This
attribute specifies the maximum amount of memory that will be used to
hold an uploaded file. If a file is larger than this amount, it will
be written to some external storage, typically the filesystem. The
value for the attribute is specified using the same notation as the
maxFileSize
attribute. The default memory file
size is 256 KB. Here, the maximum file size is set to 5 MB, and the
maximum amount held in memory is set to 500 KB:
<controller maxFileSize="5M" memFileSize="500K"/>
The memFileSize
property sets the size threshold
which determines at what point an uploaded file will be written to
disk or cached in memory. The default value for this setting is
10,240 bytes (10K
). Some containers are configured
to allow limited or no ability for writing to disk from within a web
application. If your container has this restriction, you may need to
adjust this setting to a value greater than the largest expected file
size.
Recipe 7.10 shows you how to allow users to upload files to your application.
The Struts User’s Guide discusses controller configuration. The relevant section can be found at http://struts.apache.org/userGuide/configuration.html#controller_config.
Beneath the covers, Struts use the Jakarta Commons FileUpload package. Complete documentation and source for this package can be found at http://jakarta.apache.org/commons/fileupload.
18.222.93.132