Spring Security — Tutorial
What You Will Learn
In this tutorial we will look in more depth at the most basic sample application that comes with Spring Security. It is most instructive to add security to an existing web application, so we will first remove the existing security configuration from the application, run it without it, and then incrementally add the necessary configuration options to satisfy our requirements. You can do most of this just by modifying the WAR file that comes with the Spring Security distribution, but for best results, we'd recommend you build the sample code yourself, by checking out Spring Security code.
- How to enable web security and protect different URL patterns within an application.
- How to use a simple form-based login to authenticated against an in-memory repository of test users.
- How to use hashed passwords to reduce the risks of a data compromise.
Preparation
You will need a servlet container (such as Apache Tomcat) to deploy the application to, and you really need a general understanding of using Spring without Spring Security if you are to understand how things work.
Running the Tutorial application without Spring Security
Download the latest Spring Security distribution and unzip the file. Find the file spring-security-samples-tutorial-3.1.x.war (where "x" is the minor version number, plus an additional suffix, such as "RC2" or "RELEASE") and unzip this file also. After unzipping the war file, you will see a folder called spring-security-samples-tutorial-3.1.x (where "x" is the minor version number). Rename this folder to "spring-security-tutorial" and copy it to the webapps directory of your tomcat installation. From now on we will modify the contents of this directory in place.
The application is already secured, and first we want to run it as a basic Spring MVC application. To this end, replace the contents of the web.xml file with the following:
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<display-name>Spring Security Tutorial Application</display-name>
<!--
- Location of the XML file that defines the root application context
- Applied by ContextLoaderListener.
-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:applicationContext-business.xml
</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--
- Provides core MVC application controller. See bank-servlet.xml.
-->
<servlet>
<servlet-name>bank</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>bank</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
Start Tomcat, and then browse to http://localhost:8080/spring-security-tutorial/listAccounts.html. You should see the simple interface shown below, which lists a set of accounts and allows you to modify the amounts in them by clicking on the links.
There are no controls in place to prevent you changing the values or exceeding the listed overdraft limits.
Note that if you go to the home page (index.jsp) or try to logout at this stage you will get an error, as there is no security infrastructure in place yet.
Adding a Spring Security configuration
Most applications keep their security configuration separate from the rest of their Spring configuration, so it is usually defined in its own application context file. Create a new file, spring-security-tutorial/WEB-INF/security-app-context.xml with the following contents:
<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-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.1.xsd">
<http use-expressions="true">
<intercept-url pattern="/**" access="permitAll" />
<form-login />
</http>
<authentication-manager>
<authentication-provider>
<user-service>
<user name="rod" password="koala" authorities="supervisor, teller, user" />
<user name="dianne" password="emu" authorities="teller, user" />
<user name="scott" password="wombat" authorities="user" />
<user name="peter" password="opal" authorities="user" />
</user-service>
</authentication-provider>
</authentication-manager>
</beans:beans>
Here we are using the Spring Security namespace to create a simple configuration. The <http> block states that we want to use web security (which is applied by Spring Security's filters), with form-based login and access control expressions enabled. The <intercept-url> element says that the whole site ("/**" means any request path) is accessible to anyone (the "permitAll" expression). The <authentication-manager> element is being used here to define a list of in-memory users and their passwords and role information, which is convenient for samples and demos. A real world application would more likely use a database, LDAP server or some single sign-on integration. Spring Security supports many other options out of the box.
Modify the <context-param> section of your web.xml, to add a reference to this file, causing it to be loaded as part of Spring's application context:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:applicationContext-business.xml
/WEB-INF/security-app-context.xml
</param-value>
</context-param>
Immediately under this, add the following XML snippet:
<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>
This links Spring Security's internal servlet filters into the servlet container's infrastructure. You can find more information in the Reference Manual.
If Tomcat doesn't reload the application when you save the web.xml file, restart it. You should now be able to browse to the home page at http://localhost:8080/spring-security-tutorial/index.jsp and also the "Secure page" and "Extremely secure page" links. This is because we haven't yet applied any security constraints to the application (other than the "permitAll" defined above), so the whole site is currently accessible to anonymous users.
Securing the Application
We're now ready to add some security. We won't replace the original configuration exactly, but will build up a new configuration in stages to satisfy a particular set of requirements.
Security Requirements
From the application's perspective, there are three user roles, "supervisors", "tellers" and plain "users". We want to apply the following access controls:
- You must be authenticated in order to access the account list
- Pages in the "/secure" directory should only be visible to authenticated users
- Pages in the "/secure/extreme" directory should only be visible to supervisors
- Only tellers and supervisors make withdrawals and deposits
- Only supervisors can exceed the overdraft limit for an account
Requiring authentication to access a URL
It is generally good practice to deny access by default, rather than only securing the resources we need. With that in mind, let's change the <http> block to add some web access controls
<http use-expressions="true">
<intercept-url pattern="/index.jsp" access="permitAll" />
<intercept-url pattern="/secure/extreme/**" access="hasRole('supervisor')" />
<intercept-url pattern="/secure/**" access="isAuthenticated()" />
<intercept-url pattern="/listAccounts.html" access="isAuthenticated()" />
<intercept-url pattern="/post.html" access="hasAnyRole('supervisor','teller')" />
<intercept-url pattern="/**" access="denyAll" />
<form-login />
</http>
The most specific patterns should come first, as they are tried in order. You should now find that if you try to access anything other than the index page, you will be asked to log in. We have also added the "/post.html" link, which is used when you make a withdrawal or deposit. If you look in the debug log at this point, you will see an exception:
17:44:00.301 DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Secure object: FilterInvocation: URL: /listAccounts.html; Attributes: [isAuthenticated()]
...
17:44:00.303 DEBUG o.s.s.w.a.ExceptionTranslationFilter - Access is denied (user is anonymous); redirecting to authentication entry point
org.springframework.security.access.AccessDeniedException: Access is denied
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:71)
...
This is normal. You should be redirected to a simple username and password login page. Where did this come from? It is generated internally by Spring Security, since we haven't specified a custom login page in our configuration. Verify that you can log in and use the application, using one of the username/password combinations defined above.
Bypassing security for static resources
There's one thing we've forgotten, though you may not have noticed it. If you look more closely at the debug output, you may see a similar exception
17:44:05.904 DEBUG o.s.security.web.FilterChainProxy - /static/css/tutorial.css at position 9 of 9 in additional filter chain; firing Filter: 'FilterSecurityInterceptor'
17:44:05.904 DEBUG o.s.s.web.util.AntPathRequestMatcher - Checking match of request : '/static/css/tutorial.css'; against '/index.jsp'
17:44:05.904 DEBUG o.s.s.web.util.AntPathRequestMatcher - Checking match of request : '/static/css/tutorial.css'; against '/secure/**'
17:44:05.904 DEBUG o.s.s.web.util.AntPathRequestMatcher - Checking match of request : '/static/css/tutorial.css'; against '/secure/extreme/**'
17:44:05.904 DEBUG o.s.s.web.util.AntPathRequestMatcher - Checking match of request : '/static/css/tutorial.css'; against '/listaccounts.html'
17:44:05.904 DEBUG o.s.s.web.util.AntPathRequestMatcher - Checking match of request : '/static/css/tutorial.css'; against '/post.html'
17:44:05.904 DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Secure object: FilterInvocation: URL: /static/css/tutorial.css; Attributes: [denyAll]
17:44:05.905 DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@a158adf1: Principal: org.springframework.security.core.userdetails.User@1b9c7: Username: rod; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_SUPERVISOR,ROLE_TELLER,ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@ffff10d0: RemoteIpAddress: 0:0:0:0:0:0:0:1%0; SessionId: C4914627A5A3B3B3EBCFA1832326E4A8; Granted Authorities: ROLE_SUPERVISOR, ROLE_TELLER, ROLE_USER
17:44:05.905 DEBUG o.s.s.access.vote.AffirmativeBased - Voter: org.springframework.security.web.access.expression.WebExpressionVoter@771931f8, returned: -1
17:44:05.906 DEBUG o.s.s.w.a.ExceptionTranslationFilter - Access is denied (user is not anonymous); delegating to AccessDeniedHandler
org.springframework.security.access.AccessDeniedException: Access is denied
We have forgotten to allow access to the CSS file which is used in rendering our site, and the URL has been caught by the "denyAll" access pattern. If you don't see this, try refreshing the page, as the file could be cached in your browser (a common source of confusion). You should also notice that the layout is no longer centered, since the style information is missing.
We'd prefer not to have static resources processed by Spring Security's filters at all. To achieve this, you can add an additional <http> block which only applies to a specific pattern. This must come before the existing block, as it applies to a specific pattern. If no pattern attribute is supplied, the block applies to any request.
<http pattern="/static/**" security="none" />
You should now be able to access the CSS file again.
Adding support for the logout link
We mentioned earlier that you would get an error if you tried to access the logout link (at http://localhost:8080/spring-security-tutorial/j_spring_security_logout). Now that you can login, it would also be useful to be able to log back out. The /j_spring_security_logout URL is supported by adding a <logout /> element to the <http> configuration.
<http use-expressions="true">
<intercept-url pattern="/index.jsp" access="permitAll" />
<intercept-url pattern="/secure/extreme/**" access="hasRole('supervisor')" />
<intercept-url pattern="/secure/**" access="isAuthenticated()" />
<intercept-url pattern="/listAccounts.html" access="isAuthenticated()" />
<intercept-url pattern="/post.html" access="isAuthenticated()" />
<intercept-url pattern="/**" access="denyAll" />
<form-login />
<logout />
</http>
You should now be able to log out.
Using encoded passwords
Despite many embarrassing high-profile compromises of online user databases, many sites continue to store passwords in plain text. There is really no excuse for this. All passwords should be stored as salted one-way hashes. Spring Security's StandardPasswordEncoder provides a best-practice implementation which you can use without any additional configuration. You can call the encode method on this class to obtain an encoded value, suitable for storing in your user database. For example (using the scala interpreter, with the spring-security-core jar):
luke$ scala -cp ./core/build/libs/spring-security-core-3.1.1.RELEASE.jar
Welcome to Scala version 2.8.1.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_24).
scala> val encoder = new org.springframework.security.crypto.password.StandardPasswordEncoder
encoder: org.springframework.security.crypto.password.StandardPasswordEncoder
scala> encoder.encode("koala")
res0: java.lang.String = 864acff7515e4e419d4266e474ea14a889dce340784038b704a30453e01245eed374f881f3df8e1e
Note that you will get a different value each time, due to the use of random salt bytes. Encoding the other passwords in the same way, we would modify the <authentication-manager> configuration like so:
<beans:bean id="encoder"
class="org.springframework.security.crypto.password.StandardPasswordEncoder"/>
<authentication-manager>
<authentication-provider>
<password-encoder ref="encoder" />
<user-service>
<user name="rod"
password="864acff7515e4e419d4266e474ea14a889dce340784038b704a30453e01245eed374f881f3df8e1e"
authorities="supervisor, teller, user" />
<user name="dianne"
password="9992e040d32b6a688ff45b6e53fd0f5f1689c754ecf638cce5f09aa57a68be3c6dae699091e58324"
authorities="teller, user" />
<user name="scott"
password="ab8d9744fa4dd5cee6eb692406fd29564267bad7c606837a70c44583b72e5684ec5f55c9dea869a5"
authorities="user" />
<user name="peter"
password="e446d30fcb00dc48d7e9fac49c2fec6a945171702e6822e1ec48f1ac1407902759fe30ed66a068df"
authorities="user" />
</user-service>
</authentication-provider>
</authentication-manager>
Further things to try
We now have a simple secured application running but we've only scratched the surface of what you can achieve using Spring Security. If you have a look at the namespace chapter in the manual, you will find other things you can do to experiment with the configuration we already have. For example, you could
- Create a custom login page.
- Authenticate against a user database or an LDAP server.
- Switch from using a login page to using Basic Authentication (RFC 2617).
- Experiment with different authorization expressions.
Note also that everything we've seen so far has been done without the need to modify any code. All the security configuration is completely separate from the application's business logic. However, there are limitations to what can be achieve using web security constraints alone and maintaining a large set of URL rules can be cumbersome. The last of our requirements (that only supervisors can exceed the overdraft for an account) is not something that can cleanly be achieved with web security. Instead we'll use Spring AOP to apply security to the post method of the Bankservice bean.
Securing the Service Layer
Spring Security also allows you to add security to method calls made on Spring beans in the application context. This is a more powerful and robust approach than securing URL patterns. To be able to make changes, you really need to be able to build the sample code, so ideally you should follow the instructions on getting and building the source.
To enable method security you just have to add the following element to the application context file (above the <http> elements will do):
<global-method-security pre-post-annotations="enabled" />
This enables the use of Security annotations on our Spring interfaces. The main interface for the sample application is called BankService and it already has the necessary annotation
public interface BankService {
public Account readAccount(Long id);
public Account[] findAccounts();
@PreAuthorize(
"hasRole('supervisor') or " +
"hasRole('teller') and (#account.balance + #amount >= -#account.overdraft)" )
public Account post(Account account, double amount);
}
The PreAuthorize annotation contains a Spring EL expression which encapsulates the logic of our final requirement. If you run the application now, you will get an exception if you try to exceed the overdraft amount without supervisor access. Expressions are a powerful tool but you should be wary of building excessively complicated logic into them. There are other ways of implementing access control in Spring Security. You can call other beans from expressions, for example, or implement your own custom access-decision components within the voter system which the framework uses.