Wednesday, February 17, 2010

Extending UsernamePasswordAuthenticationFilter in Spring Security

So, you are using Spring security, and you want to process some information after a user has logged in (say, load something in session) or after failed login attempts (such as lock an account after a set number of failed logins.. ), here's how it can be done.. Hope this helps.

Before I go on to describe what goes into it, thanks to the following. These links are a bit dated, but they did help me along.

Custom AuthenticationProcessingFilter for spring security to perform actions on login
An example of the security config for a custom auth filter (see last post)
Spring's documentation on adding own filters

Now, here we go. What is described below works for Spring Security 3.0.0.RELEASE.

1. We need to extend UsernamePasswordAuthenticationFilter

It used to be AuthenticationProcessingFilter, which is depricated.

In this example, we just write to the console. You would do something more important.

public class CustomUsernamePasswordAuthenticationFilter extends
UsernamePasswordAuthenticationFilter {

@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, Authentication authResult)
throws IOException, ServletException {
super.successfulAuthentication(request, response, authResult);

System.out.println("==successful login==");
}

@Override
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
super.unsuccessfulAuthentication(request, response, failed);

System.out.println("==failed login==");
}

}

2. In spring security context, we will be overriding the form login filter FORM_LOGIN_FILTER.

This used to be AUTHENTICATION_PROCESSING_FILTER, which is not used now.

<http ...
<custom-filter position="FORM_LOGIN_FILTER" ref="customUsernamePasswordAuthenticationFilter">

Note: Since we are replacing the default FORM_LOGIN_FILTER, we should not use <form-login login-page... , as that will try to create the default filter and you would get an exception (that there are two filters defined at the same position).

3. Since we are using the custom FORM_LOGIN_FILTER, we need to set the following property on <http ..

<http auto-config="false"

4. Another thing to set at <http is entry-point-ref

Again, since we are altering the default behavior, we need to tell what is the entry point for the form login.

<http entry-point-ref="loginUrlAuthenticationEntryPoint"

And, correspondingly, define the loginUrlAuthenticationEntryPoint bean. Note that LoginUrlAuthenticationEntryPoint used to be AuthenticationProcessingFilterEntry which is depricated.

<beans:bean id="loginUrlAuthenticationEntryPoint"
class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<beans:property name="loginFormUrl" value="/login.html">
</beans:bean>

5. Remember the filter 'customUsernamePasswordAuthenticationFilter ' we used in in step 2 and extended in step 1, we have to define it.
<beans:bean id="customUsernamePasswordAuthenticationFilter"
class="com.yourapp.web.security.CustomUsernamePasswordAuthenticationFilter" >
<beans:property name="authenticationManager" ref="authenticationManager">
<beans:property name="authenticationFailureHandler" ref="failureHandler">
<beans:property name="authenticationSuccessHandler" ref="successHandler">
</beans:bean>

<beans:bean id="successHandler" class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
<beans:property name="defaultTargetUrl" value="/login.html">
</beans:bean>
<beans:bean id="failureHandler" class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
<beans:property name="defaultFailureUrl" value="/login.html?login_error=true">
</beans:bean>

6. In the definition of 'customUsernamePasswordAuthenticationFilter' we are identifying the 'authenticationManager', so when you define your authentication manager provide an 'alias' for it:

<authentication-manager alias="authenticationManager">
...
</authentication-manager>

There you have it. Share and enjoy.

Saturday, February 13, 2010

Flash Scope alternative in Spring MVC

Flash scope in Spring MVC has been a topic of discussion, and even though Spring provides infrastructure to create custom scopes, I went with another solution. Hey.. as long as it works.. (for your requirements and within your constraints)...

The goal:
Set a flash error or flash message before redirecting to another view. The next view consumes the flash, displays it and clears it (so that on further redirects or refreshes the message does not appear).

Here we go:

1. Create a SessionHandler (or any other dependency you can inject into your Controller).

public class SessionHandler {

private transient final Log log = LogFactory.getLog(SessionHandler.class);

public static final String FLASH_MESSAGE = "FLASH_MESSAGE";
public static final String FLASH_ERROR = "FLASH_ERROR";
public static final String FLASH_TYPE = "flashType";

public void flashError(HttpServletRequest request, String message){
flash(request, FLASH_ERROR, message);
}

public void flashMessage(HttpServletRequest request, String message){
flash(request, FLASH_MESSAGE, message);
}

public String consumeFlash(HttpServletRequest request, String flashType){
String flash = null;
try {
flash = (String) request.getSession().getAttribute(flashType);
request.getSession().removeAttribute(flashType);
} catch (Exception e){
log.warn("Error retrieving flash value of type:" + flashType);
log.error(e.getMessage());
}
return flash;
}


/**
* helper method that sets the flash message of the type provided in session scope
*/
private void flash(HttpServletRequest request, String flashType, String message){
request.getSession().setAttribute(flashType, message);
}

This handler accepts the flash error/message and sets in session. When requested for it (using consumeFlash), it gets it from the session and clears it. It returns null if it does not find it in session.Next,

2. Use it in your controller.

if(<error condition>){
sessionHandler.flashError(request, "user.error.user.id.invalid");
return new ModelAndView(cancelView);
}
or on a successful operation,
userManager.save(user);
sessionHandler.flashMessage(request, "user.message.saved.successfully");
return new ModelAndView(getSuccessView());
As of this point we can flash an error or a message. Next step is to show it. Here is where my solution gets a bit different. I created another controller (FlashController) that creates a view of type MappingJacksonJsonView and returns that along with flash messages as redereredAttributes.

3. Create FlashController (which is ResourceLoaderAware) and map it to a url (say /flash.html)

public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {

MappingJacksonJsonView view = new MappingJacksonJsonView();
Map model = new HashMap();
Resource resource = null;
PropertyResourceBundle propertyResourceBundle = null;

try{
resource = resourceLoader.getResource(resourceLocation);
propertyResourceBundle = new PropertyResourceBundle(resource.getInputStream());
}catch (Exception e){
log.warn("Error loading resource bundle");
log.error(e.getMessage());
}

String flashError = sessionHandler.consumeFlash(request, SessionHandler.FLASH_ERROR);
if(!(flashError==null)){
String resolvedFlashError = flashError;
if (!(propertyResourceBundle==null)){
try{
resolvedFlashError = propertyResourceBundle.getString(flashError);
}catch(Exception e){}
}
model.put(SessionHandler.FLASH_ERROR, resolvedFlashError);
}


String flashMessage = sessionHandler.consumeFlash(request, SessionHandler.FLASH_MESSAGE);
if(!(flashMessage==null)){
String resolvedFlashMessage = flashMessage;
if (!(propertyResourceBundle==null)){
try{
resolvedFlashMessage = propertyResourceBundle.getString(flashMessage);
}catch(Exception e){}
}
model.put(SessionHandler.FLASH_MESSAGE, resolvedFlashMessage);
}

Set renderedAttributes = new HashSet();
renderedAttributes.add(SessionHandler.FLASH_ERROR);
renderedAttributes.add(SessionHandler.FLASH_MESSAGE);

return(new ModelAndView(view, model));

}
Couple of things to note here. Since I've added support for key-value from a resource properties file, if the flash message sent is found in the resource file (classpath:ApplicationResources.properties by default), it uses that, or else it just sends the message sent.

Now the question is, we have a way and place to set the flash error/message and now a way to retrieve the message. How do we show it on a view. That comes next.

4. Invoke FlashController from your view using ajax call.

<script>
$.getJSON("${ctx}/flash.html",
function(data){
$.each(data, function(i,item){
var curDiv = (i=='FLASH_ERROR')?"#flashError":"#flashMessage";
$(curDiv).html(item).fadeIn('slow');
});
});
</script>
<div id="flashError" style="display:none;"></div&gt;
<div id="flashMessage" style="display:none;"></div>

That's it. using jQuery getJSON method, we get the messages, and if received show them.

I'm sure there are limitations and constraints in this method, such as using javascript, ajax may be a constraint. I'm sure workarounds can be found. And if not, then using Spring's custom flash scope is always an option. I would like to hear from you if you find any security issue in this solution. n'joy.

Additional reading:
Custom Scope for Flash Scope discussion
Spring by example Custom ThreadScope

Friday, February 5, 2010

JasperReports in Spring with runtime report and format

This post is after a while. I recently finished a phpCake project, which I want to write about a bit, but in an another post. This post is about something I recently was challenged with and came up with a pretty decent solution. (I think..) hope it helps.

My current project is a Spring based solution, built on Spring MVC, spring Security, Sitemesh, JPA with Hibernate and all other bells and whistles. It also integrates JasperReports. Spring has built in support for JasperReports. You can see it documented (that can be debated) here. It has built in View implementations for different renderings of the JasperReports. (csv, html, pdf, xls).

It works, with a little bit of digging.

But it was not sufficient for my requirement, which was:
Allow for rendering of a jasper report picked at runtime in a format chosen at runtime.

The View resolvers pretty much expect that the report url be defined at configuration time. Spring has another View implementation (JasperReportsMultiFormatView) that allows run time format choice, but the JasperReport url still needs to be defined at configuration time.

I understand where this behavior comes from, as majority of the time the reports are built and packaged with source code and the rendering is wired. But it fell short of allowing picking JasperReport at runtime. So, determined to get it to work within the framework of Spring Jasper View implementations (with its nifty support for both .jrxml and compiled .jasper) , I came up with this solution. Of course, this is one implementation, and can be optimized.. If you do, please drop in a note in comments.

1. Create a JasperReportsViewFactory

protected static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";

public AbstractJasperReportsSingleFormatView getJasperReportsView(HttpServletRequest httpServletRequest,
DataSource dataSource, String url, String format, String fileName){
String viewFormat = format==null?"pdf":format;

// set possible content headers
Properties availableHeaders = new Properties();
availableHeaders.put("html", "inline; filename="+fileName+".html");
availableHeaders.put("csv", "inline; filename="+fileName+".csv");
availableHeaders.put("pdf", "inline; filename="+fileName+".pdf");
availableHeaders.put("xls", "inline; filename="+fileName+".xls");

// get jasperView class based on the format supplied
// defaults to pdf
AbstractJasperReportsSingleFormatView jasperView = null;
if(viewFormat.equals("csv")) {
jasperView = new JasperReportsCsvView();
}else if(viewFormat.equals("html")){
jasperView = new JasperReportsHtmlView();
}else if(viewFormat.equals("xls")){
jasperView = new JasperReportsXlsView();
}else{
jasperView = new JasperReportsPdfView();
}

// get appContext. required by the view
WebApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(
httpServletRequest.getSession().getServletContext());

// set the appropriate content disposition header.
Properties headers = new Properties();
headers.put(HEADER_CONTENT_DISPOSITION, availableHeaders.get(viewFormat));

// set the relevant jasperView properties
jasperView.setJdbcDataSource(dataSource);
jasperView.setUrl(url);
jasperView.setApplicationContext(ctx);
jasperView.setHeaders(headers);

// return view
return jasperView;
}

As you can see, the return type is a super class of all the Spring's Jasper View implementations. Another feature in this method is to set content disposition and supply an alternate file name for the downloaded file.

2. Use It
Once you have this , make it a dependency in your controller responsible for launching the report, and provide the url, format and other required elements for the view resolver, and execute your report.
public ModelAndView handleRequest(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse) throws Exception {

Map model = new HashMap();

String format = httpServletRequest.getParameter("format");
//Default format to pdf
if (StringUtils.hasText(format)){
if (!(format.equalsIgnoreCase("pdf") || format.equalsIgnoreCase("html")
|| format.equalsIgnoreCase("csv") || format.equalsIgnoreCase("xls"))){
format = "pdf";
}
}else{
format = "pdf";
}

// get the View that will render the report.
// in actual controller, based on report id rquestd, get the report name and build URL and use
AbstractJasperReportsSingleFormatView jasperView = jasperReportsViewFactory.getJasperReportsView(
httpServletRequest, dataSource, "/WEB-INF/jasper/sample.jrxml",format,"DownloadFileName");

// add parameters used by the report
model.put("Company","Sample Company");
// more here...

return new ModelAndView(jasperView, model);
}


Limitations:
I'm sure there are some.. One that I know of is Jasper's sub-reports. For me it is not an issue right now as we are not supporting Jasper sub-Reports in our application.

There you have it.. short and sweet. Here are some other links if this is not what you're looking for.

Matt Raible's blog on JasperReports with Appfuse and Spring (dated)
AppFuseJasperReports
A springOne presentation of Spring and JasperReports
Filename discussion using JasperReportsMultiFormatView using configuration