Friday, March 25, 2011

Hibernate Audit Logs with Spring Security

I have recently had the worst time finding a solution to a seemingly simple problem - I had created a Hibernate interceptor to automate audit logs in the database, but I couldn't figure out how to obtain the currently logged in user.

If you've never created a Hibernate interceptor, there is really only one critical piece of information you need to know related to that - there is no way that I know of to obtain the current HttpRequest object inside of the interceptor. This was my biggest problem, as this system stored the currently authenticated user object in session, but I couldn't find a way to get to the session.

In researching how to do this, I read in a lot of places that you should implement your login solution using Spring Security. That way, from literally anywhere in the system, you can use the following code to retrieve the authenticated user:
SecurityContext secureContext = SecurityContextHolder.getContext();
Authentication auth = secureContext.getAuthentication();
Object principal = auth.getPrincipal();

String userName = null;
if (principal instanceof UserDetails) {
   UserDetails userDetails = (UserDetails) principal;
   userName = userDetails.getUsername();
} else {
   userName = principal.toString();
}

I was glad that there was such an easy solution to my problem, except for one minor hiccup - the project I was working on didn't use Spring Security for authentication. I also did not have the time or the authority to change that.

However, with a little more digging, I found that you can add in Spring Security to other authentication systems, so you can make use of some of these other features. So I set out with a new goal - find the minimum amount of Spring Security configuration necessary in order to execute the above snippet.

The first step was relatively easy. It turns out that you just add the following code to your Login page, after the user successfully authenticates:
Authentication auth = new UsernamePasswordAuthenticationToken(
        user.getUsername(), 
        user.getPassword()
    );
SecurityContextHolder.getContext().setAuthentication(auth);

With just that code snippet alone, the audit logs seemed to work great. I tested it, everything was working great, so I pushed out my changes to the production system. Unfortunately, I then bumped into one of the pitfalls of only having one person testing - the above solution alone is not thread safe.

When first exposed to that statement, many Spring Security familiar developers will scoff at my inexperience, and let me know that SecurityContextHolder attaches to a ThreadLocal, so we should be good to go. However, a little bit of additional digging reveals the following weakness:
In an application which receives concurrent requests in a single session, the same SecurityContext instance will be shared between threads. Even though a ThreadLocal is being used, it is the same instance that is retrieved from the HttpSession for each thread. This has implications if you wish to temporarily change the context under which a thread is running. If you just use SecurityContextHolder.getContext().setAuthentication(anAuthentication), then the Authentication object will change in all concurrent threads which share the same SecurityContext instance.

As it turns out, there is one more piece of critical configuration you need in order to enable all of this to work successfully. You need to create a Spring bean for the class SecurityContextPersistenceFilter. From the same page as above:
In Spring Security, the responsibility for storing the SecurityContext between requests falls to the SecurityContextPersistenceFilter, which by default stores the context as an HttpSession attribute between HTTP requests. It restores the context to the SecurityContextHolder for each request and, crucially, clears the SecurityContextHolder when the request completes.

So the final piece of the puzzle is to add the following line to your applicationContext:
<bean id="securityContextPersistenceFilter" class="org.springframework.security.web.context.SecurityContextPersistenceFilter"/>

So, long story short, the above three code snippets are all that you need in order to implement Spring Security's ability to retrieve the current user from anywhere in your system.

1 comment: