Configuring the Spring Security JSP Taglib with FreeMarker for Spring Boot 2

by Paul Vorbach, 2018-09-01

TL;DR: Have a look at my sample project for setting up a Spring Boot project with FreeMarker and the JSP taglib from Spring Security.

Currently, I’m working on a project that involves classic Spring WebMVC development using Spring Boot 2, because I want it to work without any JavaScript. For that project, I chose to use FreeMarker as a template library, as I prefer its syntax over Thymeleaf. Also one of the things I quite like is that you can, in some way, inherit templates. In my opinion this is more useful in practice than including the same header and footer snippets for every page individually.

Today I needed my template to render a username, if there was a successful authentication. Since my project is using Spring Security via the spring-boot-starter-security artifact, I looked for a solution that would make using authentication context information in a FreeMarker template easy. Of course this would be something many people did before me with that exact combination of tools.

As it turned out, I was wrong and this problem was harder than expected. It took me a while to search through the docs for various projects and also many questions on Stack Overflow, so I thought I’d share the path to my solution here and I hope it’ll save you some time.

Spring Security doesn’t come with a macro library for FreeMarker, but only a taglib for JSP. But one of the great things about FreeMarker is that it supports using JSP taglibs. This answer on Stack Overflow led me to an easy-looking solution: To simply include the following line into your template

<#assign security=JspTaglibs["http://www.springframework.org/security/tags"]/>

and then using the

<@security.authorize access="isAuthenticated()">

macro.

This left me with the following exception:

freemarker.ext.jsp.TaglibFactory$TaglibGettingException: No TLD was found for the "http://www.springframework.org/security/tags" JSP taglib URI. (TLD-s are searched according the JSP 2.2 specification. In development- and embedded-servlet-container setups you may also need the "MetaInfTldSources" and "ClasspathTlds" freemarker.ext.servlet.FreemarkerServlet init-params or the similar system properites.)

So I thought the taglib was simply missing from the classpath and I began looking for a dependency that contained the correct *.tld file. And I was right! The following dependency contained the missing file.

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
</dependency>

After including the dependency I got the following exception:

freemarker.template.TemplateModelException: Error while looking for TLD file for "http://www.springframework.org/security/tags"; see cause exception.
[…]
Caused by: freemarker.ext.jsp.TaglibFactory$TaglibGettingException: No TLD was found for the "http://www.springframework.org/security/tags" JSP taglib URI. (TLD-s are searched according the JSP 2.2 specification. In development- and embedded-servlet-container setups you may also need the "MetaInfTldSources" and "ClasspathTlds" freemarker.ext.servlet.FreemarkerServlet init-params or the similar system properites.)

Meh. That “caused by” exception was the exact same one as before. So FreeMarker still couldn’t find the taglib? What next?

I had a look in the classpath, where the file was located. It was located in the spring-security-taglibs-5.0.7.RELEASE.jar file under /META-INF/security.tld. But unfortunately, simply changing the previous assignment to

<#assign security=JspTaglibs["/META-INF/security.tld"]/>

or

<#assign security=JspTaglibs["classpath:/META-INF/security.tld"]/>

also didn’t work out.

As this didn’t lead anywhere, I looked for alternatives solutions. There had been a request for a native FreeMarker macro library, several years ago. Unfortunately this didn’t get picked up by the Spring Security team and in the end there were only some suggestions on how to use the SPRING_SECURITY_CONTEXT in your own macros.

This was when I thought about giving up and writing my own macros for Spring Security, but eventually I came across an answer on Stack Overflow by Andy Wilkinson:

[There’s a] possible workaround where you configure FreemarkerConfigurer’s tag lib factory with some additional TLDs to be loaded from the classpath:

freeMarkerConfigurer.getTaglibFactory().setClasspathTlds(…);

So I only had to define a list of *.tld files on the classpath? That was worth giving a try. I added the following code to the constructor of my @SpringBootApplication class.

freeMarkerConfigurer.getTaglibFactory().setClasspathTlds(singletonList("/META-INF/security.tld"));

My template still threw an exception. But wait, the exception had changed!

freemarker.template.TemplateModelException: Error while loading tag library for URI "http://www.springframework.org/security/tags" from TLD location "classpath:/META-INF/security.tld"; see cause exception.
[…]
Caused by: freemarker.ext.jsp.TaglibFactory$TldParsingSAXException: Can't load class "org.springframework.security.taglibs.authz.JspAuthorizeTag" for custom tag "authorize".
[…]
Caused by: java.lang.Exception: Unchecked exception; see cause
[…]
Caused by: java.lang.NoClassDefFoundError: javax/servlet/jsp/tagext/Tag
[…]
Caused by: java.lang.ClassNotFoundException: javax.servlet.jsp.tagext.Tag

That was a huge step forward! FreeMarker finally found the security.tld file, but now couldn’t use it to render the template because of a missing class.

The only thing I had to do now was including the following dependency in my POM file. (Users of other servlet containers will have to use an equivalent library for their container.)

<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-jsp-api</artifactId>
</dependency>

After that change, the taglib finally worked and I could use it in my templates. The way to use it in your templates is by including the variant with the full TLD URI:

<#assign security=JspTaglibs["http://www.springframework.org/security/tags"]/>

<@security.authorize access="isAuthenticated()">
    <@security.authentication property="principal.username"/>
</@security.authorize>

Since the information is a bit scattered across this article, I compiled a minimal sample project where you can look up the details on how to get everything up and running.