I have been working on a project recently which required the migration of a non-trivial application from a, circa 2000, web application built on Struts and JDBC, to a more modern layered architecture using Java Persistence API (JPA), the Spring framework and Wicket for the front-end. Rather than rewrite everything at once the client opted for a phased approach.
Using Form-Based Container Security in Tomcat to Integrate a Struts and Wicket Application
Initially it wasn't too much of an issue when we had the Struts pages simply link to the Wicket pages. The Wicket application is using its own custom authentication mechanism, basically storing user information in a session object with a base SecurePage, extending WebPage, doing authentication and authorisation checks. However, once the weight of Wicket to Struts code became such that Wicket needed to do the authentication and authorisation checks for the whole application, we faced an integration issue.
Tomcat Form-Based Security
The struts application made extensive use of Tomcat's built in authentication mechanism with role based security being used in
- web.xml, via security-constraints,
- in struts menu.xml configuration file,
- and in code with calls to getRemoteUser.
Given that all the Struts codes is eventually going to be replaced it did not make sense to have to go and rip out all the security checks on the struts side and have wicket control the security. Even if this was done, Wicket would only be able to control the visibility of menu items, but would not be able to protect Struts pages from direct access.
The challenges was how to integrate the session based authentication mechanism used in the Wicket Application with the Form Based Authentication of the Struts application. Another objective was to ensure that users had to only log in once for both applications. The struts application was using a JDBCRealm mechanism to access user password and roles but this will work for any supported user database on Tomcat.
Tomcat SingleSignOn for Container Security
Tomcat supports a "single sign on" (SSO) directive in its server.xml file. Enabling shared authentication between web applications that run on the same server or virtual host and that share the same security realm. To enable this simply uncomment the line below in your server.xml file.
<Valve className="org.apache.catalina.authenticator.SingleSignOn" />
Wicket Configuration for Tomcat Form-Based Security
Next you ned to setup the web.xml in the Wicket applications to use form based security.
<security-constraint>
<display-name>Secure Pages</display-name>
<web-resource-collection>
<web-resource-name>ABC App</web-resource-name>
<url-pattern>/*</url-pattern>
<http-method>GET</http-method>
<http-method>POST</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>authenticated</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>FORM</auth-method>
<realm-name>ABC Realm</realm-name>
<form-login-config>
<form-login-page>/login.html</form-login-page>
<form-error-page>/login.html</form-error-page>
</form-login-config>
</login-config>
Since we still want to use the Wicket custom-authorisation, when the user is accessing the Wicket pages, we only need Tomcat to authorise the user, and make the authenticated user available to the Struts application via the servlet getRemoteUser method, we simply require the user to have the "authenticated" role to gain access to the Wicket application. To get the above configuration to work you will need to ensure that every user has the "authenticated" role, this role is user defined so could be anything you want as long as authenticated users have this role. How you do this will depend on how you have setup the user database for tomcat.
Prevent Wicket Filter from Intercepting Request for form-login-page
Next you need to make sure that the login.html page, or the form-login-page which you define in your web.xml deployment descriptor, by-passes the Wicket filter. If it does not Wicket will continually intercept the request and it will not work. We ended up writing a small customer web filter to bypass the Wicket filter if it was a request for the login.html page.
public class CustomFilter implements Filter {
Logger logger = Logger.getLogger(CustomFilter.class);
private FilterConfig filterConfig = null;
public void init(FilterConfig filterConfig)
throws ServletException {
this.filterConfig = filterConfig;
}
public void destroy() {
this.filterConfig = null;
}
public void doFilter(ServletRequest request,ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (filterConfig == null) return;
String path = ((HttpServletRequest)request).getPathInfo();
if (path!=null && path.equalsIgnoreCase("login.html")) return;
// Log the resulting string
chain.doFilter(request, response);
}
}
The filter needs to be configured in your web.xml file before the Wicket filter.
<filter>
<filter-name>Custom</filter-name>
<filter-class>abc.webui.support.CustomFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>Custom</filter-name>
<url-pattern>/login.html</url-pattern>
</filter-mapping>
Note: There may be way to do this using Wicket itself but there is very poor documentation about Wicket's processing cycle making it difficult to do without having to delve into Wicket code itself, which the budget did not allow for. The login.html page itself is simply a plain HTML page with the relevant tag attributes to access Tomcat's container security mechanism.
<html>
<head>
<title>Login Page</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<div style="width: 400px;margin-left:auto; margin-right:auto;margin-top:200px;">
<form action="j_security_check">
<table>
<tr><td>Username</td><td><input type="text" size="25" name="j_username" /></td></tr>
<tr><td>Password</td><td><input type="password" size="25" name="j_password" /></td></tr>
<tr><td><input type="Submit" value="Login" /></td><td><input type="reset" /></td></tr>
</table>
</form>
</div>
</body>
</html>
One should make the getHomePage method of the WicketApplication object point to the landing page the user will see when logged in. This way a reqest to http://abc will be redirected, by Tomcat, to the login page if the user is not authenticated and the user will be directed to the landing page on successful login. If you point the getHomePage method to the login page you will end up with an error that the j_security_check action does not exist.
Wicket Custom Session Based Security
So we could now get Tomcat to authenticate the user and we simply created the user object by getting the username from getRemoteUser() and place the object in the session where our custom security mechanism expected to find it.
Struts and Wicket SingleSignOn
With the above setup we could then have external links in the Wicket application point to the Struts application and the user would already be authenticated to tomcat upon application transversal. So we manged to keep the old security mechanism for the Struts pages and the new session based security for the Wicket application.