Ditch Container-Managed Security To Create Portable Web Apps
Do your web apps need to run in the servlet containers from different vendors? How do you manage their vendor specific security settings? I recently ran into this exact problem while developing StackHunter. Like me, you probably started with container-managed security as you have many times before. The pain probably started after you tried deploying to your second or third container and got worse from there. This is one of the problems with Java web apps — they aren’t portable between containers out-of-the-box. You can’t just take a war file from one vendor’s container and deploy it to another without configuring the security handlers in that vendor’s unique way. This article will show you how to replace the “standard” Java container-managed security with Spring Security to create a single, secure application that can be deployed to any servlet container.
The Container-Managed, Non-Portable Approach
The traditional container-managed approach uses the web.xml
file to identify protected resources. The file has one or more security-constraint
sections that links resource paths with their required user roles. It also has a login-config
section that defines the type of authentication in use (BASIC, FORM, etc.), along with some of their settings.
My original web.xml
looked something like this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<security-constraint> <web-resource-collection> <web-resource-name>ADMIN Resources</web-resource-name> <url-pattern>/setup/*</url-pattern> <url-pattern>/users/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>ADMIN</role-name> </auth-constraint> </security-constraint> <security-constraint> <web-resource-collection> <web-resource-name>VIEWER Resources</web-resource-name> <url-pattern>/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>ADMIN</role-name> <role-name>VIEWER</role-name> </auth-constraint> </security-constraint> <security-constraint> <web-resource-collection> <web-resource-name>Unprotected Resources</web-resource-name> <url-pattern>/images/*</url-pattern> <url-pattern>/public/*</url-pattern> </web-resource-collection> </security-constraint> <login-config> <auth-method>FORM</auth-method> <realm-name>Protected Resources</realm-name> <form-login-config> <form-login-page>/public/login/</form-login-page> <form-error-page>/public/login-failed/</form-error-page> </form-login-config> </login-config> |
Vendor-Specific Settings
The above web.xml
file contains all the cross-vendor parts of the standard approach. However, it doesn’t say anything about “how” authentication actually takes place. Will users to be authenticated against LDAP, a database, or web service? Will it use a custom class or one provided by the container?
For this info, we have to turn to each vendor’s unique security setup. Since StackHunter started on Tomcat, it required the following steps:
- Modify Tomcat’s
server.xml
file to a) define the security realm and b) tell Tomcat about the custom user and role classes. - Create a JAAS login configuration file to specify the custom authentication provider class.
- Add a JVM param to tell Tomcat where to find the above JAAS login configuration file.
(You can read more about how to create a custom login module here: JAAS authentication in Tomcat example.)
Now multiply those steps by the 10+ servlet containers on the market. Keeping up-to-date with all the servers’ security configs is not where we want to be spending our time.
The Spring Security, Portable Approach
The good news is that there are alternatives to the Java web security standard. A few of the options are:
- SecurityFilter – http://securityfilter.sourceforge.net/
- Apache Shiro – http://shiro.apache.org/
- Spring Security – http://projects.spring.io/spring-security/
I went with Spring Security since it seems to have a lot of developer support and isn’t any more difficult than the others to set up. Plus I was already using Spring for data access and other things.
Download Spring Jars
On a side note: if you’re not using Maven to build your project, you can download the latest Spring Framework and Spring Security jars at:
- http://repo.spring.io/release/org/springframework/spring/
- http://repo.spring.io/release/org/springframework/security/spring-security/
Programming in XML
While the Spring Framework is a great tool for building Java software, its philosophy of configuring beans outside of the code sometimes makes it too XML heavy my taste. Let’s face it, configuration in large quantities is just another form of programming. I also like being able to reason about the classes I’m using (and the flow of control) without having to look outside the class files.
With all that in mind, Spring Security still comes through with flying colors. Even the configuration is just the right amount XML and indirection.
Here are the three steps you’ll need to replace container-managed security with Spring Security.
Step 1 – Create Your Custom AuthenticationProvider
Create a subclass of org.springframework.security.authentication.AuthenticationProvider
. The authenticate
method should either:
- Return
null
if it doesn’t support authentication on the supplied object. - Return an instance of
org.springframework.security.core.Authentication
if authentication is successful. - Throw
org.springframework.security.authentication.BadCredentialsException
,DisabledException
, orLockedException
if authentication is unsuccessful.
If you’re currently using a JAAS login module, it should be pretty easy to migrate it to spring way.
In my case, the user service handled all the authentication details — like account locking, password hashing, etc. — which made it pretty easy to plug into the new Spring class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class DashboardAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication auth) throws AuthenticationException { String username = (String) auth.getPrincipal(); String password = (String) auth.getCredentials(); UserLoginResponse loginResponse = Services.getUserService().login(username, password); if (!loginResponse.isSuccess()) { throw new BadCredentialsException("invalid user or password"); } String role = "ROLE_" + loginResponse.getUser().getRole().toString(); List<SimpleGrantedAuthority> authorities = Arrays.asList(new SimpleGrantedAuthority(role)); return new UsernamePasswordAuthenticationToken(username, password, authorities); } @Override public boolean supports(Class<?> type) { return Authentication.class.isAssignableFrom(type); } } |
Step 2 – Create Your Security Configuration
Create a new file named /WEB-INF/spring-security.xml
similar to the one below.
The first set of tags (with the security="none"
attribute) identify all the public, unsecured resource paths. The intercept-url
element inside http
, associates resource paths to their required user roles. And the form-login
element sets the authentication type to form and identifies the page with the login form along with other paths.
All the specified paths are relative to the context root and the pattern attribute use ANT notation — where double asterisks (/**) means “include all sub-folders”.
The last few lines of this file sets the authentication provider to the class defined above.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
<beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd"> <http pattern="/css/**" security="none"/> <http pattern="/js/**" security="none"/> <http pattern="/images/**" security="none"/> <http pattern="/public/**" security="none"/> <http pattern="/api/**" security="none"/> <http pattern="/error/**" security="none"/> <http pattern="/favicon.ico" security="none"/> <http auto-config="true"> <!-- Admin Resources --> <intercept-url pattern="/setup/**" access="ROLE_ADMIN" /> <intercept-url pattern="/users/**" access="ROLE_ADMIN" /> <intercept-url pattern="/events/**" access="ROLE_ADMIN" /> <!-- Developer Resources --> <intercept-url pattern="/apps/**" access="ROLE_ADMIN, ROLE_DEVELOPER" /> <!-- Viewer Resources --> <intercept-url pattern="/**" access="ROLE_ADMIN, ROLE_DEVELOPER, ROLE_VIEWER" /> <form-login login-page="/public/login/" default-target-url="/" authentication-failure-url="/public/login-failed/" /> <logout logout-success-url="/" /> </http> <authentication-manager> <authentication-provider ref='dashboardAuthenticationProvider' /> </authentication-manager> <beans:bean id="dashboardAuthenticationProvider" class="com.stackhunter.dashboard.security.DashboardAuthenticationProvider" /> </beans:beans> |
Step 3 – Add The web.xml Hooks
The final step is to update the web.xml to:
- Remove all the old security-constraint and login-config tags.
- Load the security settings from the file above.
- Use the Spring servlet filter to intercept requests and handle authentication.
Make sure this servlet filter is placed above any other filters in your web.xml
that expects authentication to have already occurred.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<!-- Loads Spring Security config file --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/spring-security.xml</param-value> </context-param> <!-- Spring Security --> <filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> |
Just Package and Deploy, and Deploy
That’s all there is to it. Just package the spring-security.xml
file, your custom authenticator, and the Spring jars into your war and deploy.
Spring Security personally saved me lots of time documenting and maintaining the installation steps for each servlet container. More importantly, it reduced my web app’s installation steps and removed a potential source of support issues. I hope it does as well for you.
Excellent post! But why stop at security? Why not ditch even the container?? This seems to be the direction that Spring is heading and I’m loving it! 🙂 http://projects.spring.io/spring-boot/
Get rid of the container??? Now that’s just crazy talk 😉
Your blog post hits the right nerve, but it’s not entirely correct.
You ask about the how, and then go on to say you need server specifics. But Java EE has a standard method for the how. It’s called JASPIC and has been in Java EE since two major versions.
See http://arjan-tijms.blogspot.co.uk/2012/11/implementing-container-authentication.html for more details.
With JASPIC your login modules are fully portable between all full Java EE servers.
Thanks for the info on JASPIC/JASPI, it looks very promising.
Unfortunately, it doesn’t seem to be supported by Tomcat, which is understandable, since it’s not a full Java EE server.
There also doesn’t seem to be a standard way to configure it declaratively.
I get why the “how” was excluded from web.xml, but having that option would have saved us a lot of headaches over the years.