AppFuse is in an interesting state right now. In order for me to easily switch to using iBatis for the DAO layer, I left all the Hibernate stuff intact and just added iBatis classes and JARs. The next step is to extract the iBatis stuff into a separate CVS module and write a build.xml file to replace the Hibernate implementation with iBatis.
The one thing that will suck, in the iBatis version, is that the database can't be dynamically created from POJOs. However, if you're going to use the iBatis implementation, its likely that the database (or SQL) already exists. The reason I'm writing this post is because right now, in CVS, you can change the dao.type property to "ibatis" or "hibernate" when building and Voila! - that's the implementation you'll get. I see no reason why any project would ever want to have both implementations (maintenance would be a nightmare), so that's why I'm extracting the iBatis stuff in the next few days.
This is a continuing series on what I'm doing to make AppFuse a better application in Winter/Spring 2004. Previous titles include: Changing the Directory Structure and Spring Integration.
- - - -
AppFuse includes a Remember Me feature that works with Container-Managed Authentication. In version 1.3 it works by setting a few cookies: username, password and rememberMe. The last one being a simple flag that the user wants to be remembered. Then a LoginFilter checks for the rememberMe cookie, and if present, logs in the user using the other cookie values. The obvious issue with this is that the password being sent and stored on the user's browser.
This was easily solved in Tomcat 4 by placing the form-login-page and form-error-page under a "security"
directory and then setting cookies on the /appfuse/security path. This way, since no other part of
the app can access /appfuse/security, these cookies can never be retrieved in any part of the application.
The problem is that this didn't work in Tomcat 5 since it forwards to the login page (rather than redirecting).
Since forwarding is obviously a better solution (user's can't bookmark the login page), I needed a new way to
implement the Remember Me feature.
To my knowledge, cookies can only be stolen if someone is able to login to your AppFuse app and insert JavaScript
to send the "document.cookie" value to an external URL. So for AppFuse, it's likely that stealing cookies is not
much of an issue. However, for applications like Roller, it is an issue - since other bloggers on the same server
(i.e. JRoller) could put JavaScript on their blog to grab cookies from other users.
Just as I was about to give up searching for solutions, along came
Charle's persistent cookie strategy. Here's
how I implemented it in AppFuse. Hopefully it follows all the rules and is a good solution. Here's what I did make it happen.
- - - -
Step 1: Setting the cookie.
Scenario: A user logs in and selects the "Remember Me" checkbox.
What Happens: When a user clicks the Login button, they submit to a LoginServlet that redirects them to "j_security_check" to take
advantage of Container-Managed Authentication. This servlet is responsible for ensuring an SSL Login (if enabled), encrypting the
user's password (if enabled) and also sets a session variable to indicate the user wants to be remembered. After authenticating, the user
will hit the ActionFilter, where the following code sits:
// if user wants to be remembered, create a remember me cookie
if (session.getAttribute(Constants.LOGIN_COOKIE) != null) {
session.removeAttribute(Constants.LOGIN_COOKIE);
String loginCookie = mgr.createLoginCookie(username);
RequestUtil.setCookie(response, Constants.LOGIN_COOKIE,
loginCookie, request.getContextPath());
}
|
In the above code snippet, the UserManager.createLoginCookie(username) method is responsible for creating a new cookie string and storing this information in the database.
public String createLoginCookie(String username) throws Exception {
UserCookie cookie = new UserCookie();
cookie.setUsername(username);
return saveLoginCookie(cookie);
}
/**
* Convenience method to set a unique cookie id and save to database
* @param cookie
* @return
* @throws Exception
*/
private String saveLoginCookie(UserCookie cookie) throws Exception {
cookie.setCookieId(new RandomGUID().toString());
dao.saveUserCookie(cookie);
return cookie.getUsername() + "|" + cookie.getCookieId();
}
|
The RandomGUID is a class I found on Java Exchange. Once the rememberMe cookie was set, I had to
configure LoginFilter.java (mapped to form-login-page and form-error-page) to look for this cookie. This brings us to Step 2.
- - - -
Step 2: Using the cookie to login the user.
Scenario: A User has already logged in successfully with "Remember Me" enabled.
What Happens: When the login page is served up to the user, the LoginFilter is invoked and it checks the validity of the "Remember Me" cookie.
Cookie c = RequestUtil.getCookie(request, Constants.LOGIN_COOKIE);
WebApplicationContext context =
(WebApplicationContext) config.getServletContext().getAttribute
(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
UserManager mgr = (UserManager) context.getBean("userManager");
if (c != null) {
try {
String loginCookie = mgr.checkLoginCookie(c.getValue());
|
The UserManager.checkLoginCookie(value) method looks up a record based on the random GUID, and if it finds a match, it creates a new GUID and saves it in the database. If null is returned, it means the cookieId doesn't exist, and the login proceeds as it normally would. Below is the guts of the checkLoginCookie() method.
public String checkLoginCookie(String value) throws Exception {
String[] values = StringUtils.split(value, "|");
if (log.isDebugEnabled()) {
log.debug("looking up cookieId: " + values[1]);
}
UserCookie cookie = dao.getUserCookie(values[1]);
if (cookie != null) {
if (log.isDebugEnabled()) {
log.debug("cookieId lookup succeeded, generating new cookieId");
}
return saveLoginCookie(cookie);
} else {
if (log.isDebugEnabled()) {
log.debug("cookieId lookup failed, returning null");
}
return null;
}
}
|
You can see from this, that if the lookup succeeds - a new cookieId is saved and returned. If a not-null cookieId is returned, the remember me cookie is updated, the user is looked up and the Filter forwards an authentication request (with username/password) to the LoginServlet. The Filter also sets an attribute to let the application know that this user authenticated via cookies. This is important so that cookie-authenticated users cannot change passwords. When using cookie-authentication, the password field is hidden and a message warns the user that they must logout/login to change passwords.
Lastly, I had to come up with a solution to remove these login cookies.
- - - -
Step 3: Allow the user to clear their login cookies.
Scenario: A User has already logged in successfully with "Remember Me" enabled.
What Happens: For this, I implemented a simple solution. When a user logs out, all persistent login cookies are removed.
I don't know if it's best to divulge the details of AppFuse's cookie login strategy. However - it *is* open source - so folks can find figure it out if they really want to. By exposing it to the world, I hope to get the most robust solution possible.
Next up, how I replaced Hibernate with iBatis. Using Spring, it only took me a few hours! Pretty slick, eh? ;-)