Distigme

A little learning is a dangerous thing.


4 Comments

Ajax and Spring Security form-based login

If your web application uses a form-based login through Spring security, and the same application uses ajax, you likely have a problem. Say your user opens two tabs for your application in the browser and logs out from one of them, while the other is periodically updating via ajax. Or, say your user comes back after lunch, to a page whose session has long timed out, and does something that attempts ajax. Or, say the user just pushes the Back button after logging out or timing out, and that pages uses some ajax. The default behavior is to treat the ajax request like any other web page and redirect to the login form. Not good.

Aside from the obvious absurdity, there are some real problems. First, it is not straightforward for your JavaScript code to determine what went wrong; it just sees a redirect followed by a successful GET of the login page, which looks fine and dandy apart from being HTML instead of JSON. Second, if the user, in another tab, tries to access a secured page, and an ajax call hits while they are typing in their password, the user will end up looking at a page full of JSON instead of their intended destination. That’s because Spring Security, by default, determines the destination after login from whatever was most recently stored in the session. (Really? The session??? I guess that’s because if you submit a form on an expired session, you don’t want to lose all your typing, but there’s no better place to keep the form data.)

Umar offers a solution based upon writing a custom ExceptionTranslationFilter, which ends up being really cumbersome because the <http> security namespace does not play nicely with it and has to be expanded into all its gory innards.

My solution is similar but overcomes this difficulty. The two steps are to configure the request cache to reject ajax requests and to replace the authentication entry point with one that rejects ajax requests.

<bean id="nonAjaxRequestMatcher" class="org.example.NonAjaxRequestMatcher" />

<bean id="loginUrlAuthenticationEntryPoint" 
	class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
	<constructor-arg value="/login" />
</bean>

<bean id="ajaxAuthenticationEntryPoint" 
	class="org.springframework.security.web.authentication.Http403ForbiddenEntryPoint" />

<bean id="authenticationRequestCache" 
	class="org.springframework.security.web.savedrequest.HttpSessionRequestCache">
	<property name="requestMatcher" ref="nonAjaxRequestMatcher" />
</bean>

<bean id="authenticationEntryPoint" 
	class="org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint">
	<constructor-arg>
		<map>
			<entry key-ref="nonAjaxRequestMatcher" value-ref="loginUrlAuthenticationEntryPoint" />
		</map>
	</constructor-arg>
	<property name="defaultEntryPoint" ref="ajaxAuthenticationEntryPoint" />
</bean>

<security:http entry-point-ref="authenticationEntryPoint" ...>
	<security:request-cache ref="authenticationRequestCache" />
	...
</security:http>

This works by defining a criterion nonAjaxRequestMatcher that will identify only non-ajax pages that should be redirected as needed to the login page. This can be plugged directly into the authentication request cache. The authentication entry point is a bit more complicated, but it can all be set up in Spring as shown. A DelegatingAuthenticationEntryPoint is used to ask which type of request we’re dealing with and then send interactive requests to a standard loginUrlAuthenticationEntryPoint and ajax requests to ajaxAuthenticationEntryPoint, which returns 403 Forbidden.

The NonAjaxRequestMatcher can be implemented simply:

public class NonAjaxRequestMatcher implements RequestMatcher {
	@Override
	public boolean matches(HttpServletRequest request) {
		return !"XmlHttpRequest".equalsIgnoreCase(request.getHeader("X-Requested-With"));
	}
}

This bit of magic relies on a convention built into most modern JavaScript frameworks (Dojo, JQuery, etc.) to identify ajax requests by a special header.

Lastly, you can also go one step further and write a custom replacement for Http403ForbiddenEntryPoint that will return JSON instead of HTML, as many JavaScript frameworks seem surprised when you say to expect JSON and the error page has HTML instead (!).

Advertisements


Leave a comment

Serving Dojo from Spring

If you have a project using Dojo (or any comparable framework), you have a few options for how to serve Dojo itself, including:

  1. Use the Google CDN. This is the most straightforward and a great place to start. You might even hope for caching efficiencies by using such a common CDN, but in practice this does not work out so well. And if you do development from somewhere with a flaky WiFi, every page reload is a spin of the revolver.
  2. Use Dojo’s sweet build system to package up all the JavaScript+CSS that your site needs. This approach can be a big win, but you have to figure out how to set everything up in your automated build system.
  3. Use Maven to pull in the Dojo WAR. You can then deploy this separately or integrate it via an overlay. Then you discover the joy of waiting for m2e to unpack 8,000 Dojo files over and over again.

There is a another very convenient method that I have never seen suggested but have found to work quite well.

There is a middle ground of serving the pre-packaged Dojo Toolkit yourself, without going through a WAR, and without ever unpacking everything on disk. First, download the ZIP, rename the extension to .jar, and put it in your WEB-INF/lib. Then, in Spring, you can map to this JAR via the classpath:

<mvc:resources 
    mapping="/dojo-1.8.0/**" 
    location="classpath:dojo-release-1.8.0/" 
    cache-period="31556926" />

When someone requests from your site, say, /dojo-1.8.0/dojo/dojo.js, Spring will use the first part of the path to map into your zip-renamed-as-jar, then find the folder and file within the ZIP itself, and serve it directly.


1 Comment

Hacking Thymeleaf to minimize white space

Thymeleaf has an open feature request to suppress superfluous white space (whitespace?) in its output.

Getting this right is good for efficiency but rife with pitfalls, which is why the safest thing is to just leave all the white space alone. For sure, you don’t want to mess with the white space inside a <pre> or <code>, or any element that might potentially be styled white-space: pre, which is nearly all of them. Even if you remove only text nodes that are only white space, you can run into things like this:

<p>I am <u>very</u> <a href="#2">interested</a>.</p>

Caveats aside, let’s see how to configure Thymeleaf now to strip whitespace-only nodes. This approach will use a custom template writer, as suggested here.

In Spring, start with the templateEngine:

<bean id="templateEngine" class="org.thymeleaf.spring3.SpringTemplateEngine">
	<property name="templateResolver" ref="templateResolver" />
	<property name="templateModeHandlers">
		<bean class="org.thymeleaf.templatemode.TemplateModeHandler">
			<constructor-arg index="0" value="HTML5" />
			<constructor-arg index="1" 
				value="#{T(org.thymeleaf.templatemode.StandardTemplateModeHandlers).HTML5.templateParser}" />
			<constructor-arg index="2">
				<bean class="org.example.WhiteSpaceNormalizedTemplateWriter" />
			</constructor-arg>
		</bean>
	</property>
</bean>

This is replacing the default list of template mode handlers (HTML5, XHTML, etc.) with a single custom one, which we are calling “HTML5” (or you could pick a distinctive name). A template mode handler really just ties together a parser and a writer, so there is no need to write a new class; instead, we construct an instance right here, passing in the name, the original parser for HTML5, and our custom template writer.

So, that’s how to wire the custom template writer into Thymeleaf via Spring. The class itself can look like this:

public final class WhiteSpaceNormalizedTemplateWriter extends AbstractGeneralTemplateWriter {
	
	@Override protected boolean shouldWriteXmlDeclaration() { return false; }
	@Override protected boolean useXhtmlTagMinimizationRules() { return true; }

	@Override
	protected void writeText(final Arguments arguments, final Writer writer, final Text text)
			throws IOException {
		final char[] textChars = text.unsafeGetContentCharArray();
		if ( StringUtils.hasText(CharBuffer.wrap(textChars)) ) {
			writer.write(textChars);
		}
	}
	
}