Java Web Application Security - Part II: Spring Security Login Demo
Last week, I wrote a tutorial on how to implement Security in Java EE 6. This week, I'd like to show you how to implement the same features using Spring Security. Before I begin, I'd like to explain my reason for writing this article.
Last month, I presented a talk on Java Web Application Security at the Utah JUG (UJUG). As part of that presentation, I did a number of demos about how to implement security with Java EE 6, Spring Security and Apache Shiro. I told the audience that I would post the presentation and was planning on recording screencasts of the various demos so the online version of the presentation would make more sense.
Today, I've finished the second screencast showing how to implement security with Spring Security. Below is the presentation (with the screencast embedded on slide 16) as well as a step-by-step tutorial.
Spring Security Login Tutorial
- Download and Run the Application
- Implement Basic Authentication
- Force SSL
- Implement Form-based Authentication
- Add Remember Me
- Store Users in a Database
- Summary
Download and Run the Application
To begin, download the application you'll be implementing security in. This app is a stripped-down version of the Ajax Login application I wrote for my article on Implementing Ajax Authentication using jQuery, Spring Security and HTTPS. You'll need Java 6 and Maven installed to run the app. Run it using mvn jetty:run and open http://localhost:8080 in your browser. You'll see it's a simple CRUD application for users and there's no login required to add or delete users.
Implement Basic Authentication
The first step is to protect the list screen so people have to login to view users. To do this, you'll need to create a Spring context file that contains Spring Security's configuration. Create src/main/webapp/WEB-INF/security.xml and populate it with the contents below:
<?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd"> <!-- New in Spring Security 3.1 --> <!-- <http pattern="/css/**" security="none"/> --> <http auto-config="true"> <intercept-url pattern="/app/users" access="ROLE_USER,ROLE_ADMIN"/> <http-basic/> </http> <authentication-manager alias="authenticationManager"> <authentication-provider> <password-encoder hash="sha"/> <user-service> <user name="user" password="12dea96fec20593566ab75692c9949596833adc9" authorities="ROLE_USER"/> <user name="admin" password="d033e22ae348aeb5660fc2140aec35850c4da997" authorities="ROLE_ADMIN"/> </user-service> </authentication-provider> </authentication-manager> <!-- Override userSecurityAdvice bean in appfuse-service to allow any role to update a user. --> <beans:bean id="userSecurityAdvice" class="org.appfuse.examples.webapp.security.UserSecurityAdvice"/> </beans:beans>
The last bean, userSecurityAdvice, is an aspect that's needed to override some behavior in AppFuse. You won't need this normally when implementing Spring Security.
Next, open src/main/webapp/WEB-INF/web.xml and add Spring's DelegatingFilterProxy:
<filter> <filter-name>securityFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetBeanName</param-name> <param-value>springSecurityFilterChain</param-value> </init-param> </filter>
And add its filter-mapping just after the rewriteFilter in the filter-mappings section (order is important!):
<filter-mapping> <filter-name>rewriteFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>securityFilter</filter-name> <url-pattern>/*</url-pattern> <dispatcher>REQUEST</dispatcher> <dispatcher>FORWARD</dispatcher> <dispatcher>INCLUDE</dispatcher> </filter-mapping>
You don't need to add any dependencies in your pom.xml is because this project depends on AppFuse, which already contains these dependencies.
At this point, if you restart Jetty (Ctrl+C and jetty:run again), you should be prompted to login when you click on the "Users" tab. Enter admin/admin to login. Spring Security is a bit easier to configure than Java EE 6 out-of-the-box, mostly because it doesn't require you to configure your container.
After logging in, you can try to logout by clicking the "Logout" link in the top-right corner. This calls a LogoutController with the following code that logs the user out.
public void logout(HttpServletResponse response) throws ServletException, IOException { request.getSession().invalidate(); response.sendRedirect(request.getContextPath()); }
NOTE: Spring Security has a way to configure "logout" to match a URL and get rid of a class like LogoutController. Since it was already in the project, I don't cover that in this tutorial.
You'll notice that clicking this link doesn't log you out, even though the session is invalidated. The only way to logout with basic authentication is to close the browser. In order to get the ability to logout, as well as to have more control over the look-and-feel of the login, you can implement form-based authentication. Before you implement form-based authentication, I'd like to show you how easy it is to force SSL with Spring Security.
Force SSL
Spring Security allows you to switch between secure (https) and non-secure (http) protocols using a simple requires-channel attribute on the <intercept-url> element. Possible values are "http", "https" and "any". Add requires-channel="https" to your security.xml file:
<intercept-url pattern="/app/users" access="ROLE_USER,ROLE_ADMIN" requires-channel="https"/>
In order for this to work, you have to configure Jetty to listen on an SSL port. Add the following just after the jetty-maven-plugin's </webAppConfig> element in your pom.xml:
<connectors> <connector implementation="org.eclipse.jetty.server.nio.SelectChannelConnector"> <forwarded>true</forwarded> <port>8080</port> </connector> <connector implementation="org.eclipse.jetty.server.ssl.SslSelectChannelConnector"> <forwarded>true</forwarded> <port>8443</port> <maxIdleTime>60000</maxIdleTime> <keystore>${project.build.directory}/ssl.keystore</keystore> <password>appfuse</password> <keyPassword>appfuse</keyPassword> </connector> </connectors>
The keystore must be generated for Jetty to start successfully, so add the keytool-maven-plugin just above the jetty-maven-plugin in pom.xml.
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>keytool-maven-plugin</artifactId> <version>1.0</version> <executions> <execution> <phase>generate-resources</phase> <id>clean</id> <goals> <goal>clean</goal> </goals> </execution> <execution> <phase>generate-resources</phase> <id>genkey</id> <goals> <goal>genkey</goal> </goals> </execution> </executions> <configuration> <keystore>${project.build.directory}/ssl.keystore</keystore> <dname>cn=localhost</dname> <keypass>appfuse</keypass> <storepass>appfuse</storepass> <alias>appfuse</alias> <keyalg>RSA</keyalg> </configuration> </plugin>
Now if you restart Jetty, go to http://localhost:8080 and click on the "Users" tab, you'll be prompted to accept the Untrusted Certificate and then redirected to https://localhost:8443/users after logging in. This is an improvement on Java EE's user-data-constraint for two reasons:
- You can switch between http and https protocols. With Java EE, you can only force https. You have to write a custom filter to switch back to http.
- Redirecting to https actually works. With Java EE (on Jetty at least), a 403 is returned instead of redirecting the request.
Now let's look at how to have more control over the look-and-feel of the login screen, as well as how to make logout work with form-based authentication.
Implement Form-based Authentication
To change from basic to form-based authentication, you simply have to add a <form-login> element in security.xml's <http> element:
<http auto-config="true"> <intercept-url pattern="/app/users" access="ROLE_USER,ROLE_ADMIN" requires-channel="https"/> <form-login login-page="/login" authentication-failure-url="/login?error=true" login-processing-url="/j_security_check"/> <http-basic/> </http>
You can leave the <http-basic> element since Spring Security is smart enough to serve up the form for browsers and use Basic Authentication for clients such as web services. The login.jsp page (that /login forwards to) already exists in the project, in the src/main/webapp directory. The forwarding is done by the UrlRewriteFilter with the following configuration in src/main/webapp/WEB-INF/urlrewrite.xml.
<rule> <from>/login</from> <to>/login.jsp</to> </rule>
This JSP has 3 important elements: 1) a form that submits to "/j_security_check", 2) an input element named "j_username" and 3) an input element named "j_password". If you restart Jetty, you'll now be prompted to login with this JSP instead of the basic authentication dialog.
Add Remember Me
Remember Me is a feature you see in many web applications today. It's usually a checkbox on the login form that allows you to auto-login the next time you visit a site. This feature doesn't exist in Java EE security, but it does exist in Spring Security. To enable it, add the following just below <form-login> in security.xml:
<remember-me user-service-ref="userDao" key="e37f4b31-0c45-11dd-bd0b-0800200c9a66"/>
Next, open src/main/webapp/login.jsp and change the name of the "remember me" checkbox to be _spring_security_remember_me:
<input type="checkbox" name="_spring_security_remember_me" id="rememberMe"/>
After making these changes, you should be able to restart Jetty, go to http://localhost:8080/users, enter admin/adminjdbc, check the Remember Me checkbox and login. Then close your browser, and repeat the process. This time, you won't be prompted to login. For more information on this feature, see Spring Security's Remember Me documentation.
While storing usernames and passwords in a file is convenient for demos, it's not very real-world-ish. The next section shows you how to configure Spring Security to use a database for its user store.
Store Users in a Database
To store your users in a database instead of file, you'll need to add a user-service-ref attribute to the <authentication-provider> element. You can also delete the <user-service> element.
<authentication-manager alias="authenticationManager"> <authentication-provider user-service-ref="userDao"> <password-encoder hash="sha"/> </authentication-provider> </authentication-manager>
The "userDao" bean is provided by AppFuse and its UserDaoHibernate.java class. This class implements Spring Security's UserDetailsService interface. With Java EE, I had to configure a database connection and make sure the JDBC Driver was in my container's classpath. With Spring Security, you can talk to the database you already have configured in your application.
2011-05-13 08:47:29.081:WARN::UserRealm Java EE Login could not connect to database; will try later org.h2.jdbc.JdbcSQLException: Database may be already in use: "Locked by another process". Possible solutions: close all other connection(s); use the server mode [90020-154] at org.h2.message.DbException.getJdbcSQLException(DbException.java:327) at org.h2.message.DbException.get(DbException.java:167) at org.h2.message.DbException.get(DbException.java:144) at org.h2.store.FileLock.getExceptionAlreadyInUse(FileLock.java:443) at org.h2.store.FileLock.lockFile(FileLock.java:338) at org.h2.store.FileLock.lock(FileLock.java:134) at org.h2.engine.Database.open(Database.java:535) at org.h2.engine.Database.openDatabase(Database.java:218)
The password for the "admin" user is configured in src/test/resources/sample-data.xml and it's loaded by DbUnit before the application starts. You can view your pom.xml and the dbunit-maven-plugin's configuration if you're interested in learning how this is done. The password is currently configured to "adminjdbc", but you can reset it by generating a new password and modifying sample-data.xml.
Now if you restart Jetty, you should be able to login with admin/adminjdbc and view the list of users.
Summary
In this tutorial, you learned how to implement authentication using Spring Security 3.0.5. In addition to the basic XML configuration, Spring Security also provides a AOP support and annotations you can use to secure methods. It also has many more features than standard Java EE Security. In my opinion, it's the most mature security framework we have in Java today. Currently, I think its reference documentation is the best place to learn more.
- The authentication mechanism (file, database, ldap, etc.) is contained in the WAR
- Securing methods only works on Spring beans
- Remember Me doesn't work in my screencast (because I forgot to rename the checkbox in login.jsp)
Of course, you can configure Spring to load its configuration from outside the WAR (e.g. a file or JNDI), but it's not as easy as including the configuration in your app.
In the next couple weeks, I'll post Part III of this series, where I'll show you how to implement this same set of features using Apache Shiro. In the meantime, please let me know if you have any questions.
I created the screencasts with Camtasia. For small screens, and embedding in the presentation, I created it at 50% and used the SmartFocus feature to zoom in and out during the demo. For larger screens, I published another screencast at 100%, in HD. If you have a preference for which screencast is better, I'd love to hear about it.
Posted by jonathan on May 17, 2011 at 01:29 PM MDT #
Posted by Matt Raible on May 17, 2011 at 01:33 PM MDT #
Posted by aniello on May 18, 2011 at 08:53 AM MDT #
Posted by Raible Designs on May 26, 2011 at 11:12 PM MDT #
Posted by nvolcz on November 19, 2011 at 09:44 AM MST #
Posted by pgarner on November 21, 2011 at 04:57 PM MST #
I'm just now learning Spring and AOP, and I'm not sure I understand the Spring Security limitation "Securing methods only works on Spring beans." Because the Spring Framework is compliant with JSR-250 can't JSR-250 annotations be used on any bean by simply adding the following rule to the Spring Security XML configuration file:
Posted by pgarner on November 21, 2011 at 04:57 PM MST #
Posted by Will on July 19, 2012 at 06:25 PM MDT #
Posted by Tragi Mansour on September 24, 2013 at 07:12 AM MDT #
Posted by Khushboo on April 24, 2014 at 09:43 PM MDT #