Spring Security is a powerful and highly customizable authentication and access-control framework for enterprise Java applications. The Spring Security framework is mainly used to ensure web application security such as authentication and authorization on application-level operations.
For web-layer security, Spring Security heavily leverages the existing Servlet filter architecture; it does not depend on any particular web technology. Spring Security mainly concerns HttpRequest
and HttpResponse
objects; it doesn't care about the source of the request and the response target. A request may originate from a web browser, web service, HTTP client, or JavaScript-based Ajax request. The only critical requirement for Spring Security is that it must be an HttpRequest
, so that it can apply standard Servlet filters.
Spring Security provides various pre-built Servlet filters as part of the framework; the only requirement for us is to configure the appropriate filters in our web application in order to intercept the request and perform security checks. Spring Security is a vast topic, so we are not going to see all the capabilities of Spring Security; instead we are only going to see how to add basic authentication to our web pages.
In Chapter 4, Working with Spring Tag Libraries, we saw how to serve and process web forms; in that exercise we created a web page to add products. Anyone with access to the add products page could add new products to our web store. But in a typical web store, only administrators can add products. So how can we prevent other users from accessing the add products page? Spring Security comes to the rescue.
We are going to restrict access to all our web pages using Spring Security. Only an authorised user or administrator with a valid username and password can access our web pages from a browser:
pom.xml
; you can find pom.xml
under the project root folder itself.pom.xml
; select the Dependencies tab and click the add button in the Dependencies section.org.springframework.security
, Artifact Id as spring-security-config
, Version as 4.1.1.RELEASE
, select Scope as compile, and click the OK button.org.springframework.security
, Artifact Id as spring-security-web
, Version as 4.1.1.RELEASE
, select Scope as compile, and click the OK button. And most importantly, save pom.xml
.LoginController
under the com.packt.webstore.controller
package in the src/main/java
source folder. Add the following code to it:package com.packt.webstore.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller public class LoginController { @RequestMapping(value = "/login", method = RequestMethod.GET) public String login() { return "login"; } }
login.jsp
under the src/main/webapp/WEB-INF/views/
directory, add the following code snippets into it, and save it:<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css /bootstrap.min.css"> <title>Products</title> </head> <body> <section> <div class="jumbotron"> <div class="container"> <h1>Welcome to Web Store!</h1> <p>The one and only amazing web store</p> </div> </div> </section> <div class="container"> <div class="row"> <div class="col-md-4 col-md-offset-4"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">Please sign in</h3> </div> <div class="panel-body"> <c:url var="loginUrl" value="/login" /> <form action="${loginUrl}" method="post" class="form-horizontal"> <c:if test="${param.error != null}"> <div class="alert alert-danger"> <p>Invalid username and password. </p> </div> </c:if> <c:if test="${param.logout != null}"> <div class="alert alert-success"> <p>You have been logged out successfully.</p> </div> </c:if> <c:if test="${param.accessDenied != null}"> <div class="alert alert-danger"> <p>Access Denied: You are not authorised! </p> </div> </c:if> <div class="input-group input-sm"> <label class="input-group-addon" for="username"><i class="fa fa-user"></i></label> <input type="text" class="form-control" id="userId" name="userId" placeholder="Enter Username" required> </div> <div class="input-group input-sm"> <label class="input-group-addon" for="password"><i class="fa fa-lock"></i></label> <input type="password" class="form-control" id="password" name="password" placeholder="Enter Password" required> </div> <div class="form-actions"> <input type="submit" class="btn btn-block btn-primary btn-default" value="Log in"> </div> </form> </div> </div> </div> </div> </div> </body>
SecurityConfig.java
under the com.packt.webstore.config
package in the src/main/java
source folder, add the following content into it, and save it:package com.packt.webstore.config; import org.springframework.beans.factory .annotation.Autowired; import org.springframework.context .annotation.Configuration; import org.springframework.security.config.annotation .authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config .annotation.web.builders.HttpSecurity; import org.springframework.security.config .annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config .annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser("john").password("pa55word ").roles("USER"); auth.inMemoryAuthentication().withUser("admin").password("root123 ").roles("USER","ADMIN"); } @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.formLogin().loginPage("/login") .usernameParameter("userId") .passwordParameter("password"); httpSecurity.formLogin().defaultSuccessUrl ("/market/products/add") .failureUrl("/login?error"); httpSecurity.logout().logoutSuccessUrl("/login? logout"); httpSecurity.exceptionHandling().accessDeniedPage ("/login?accessDenied"); httpSecurity.authorizeRequests() .antMatchers("/").permitAll() .antMatchers("/**/add").access("hasRole('ADMIN')") .antMatchers("/**/market/**").access ("hasRole('USER')"); httpSecurity.csrf().disable(); } }
SecurityWebApplicationInitializer
under the com.packt.webstore.config
package in the src/main/java
source folder, add the following content into it, and save it:package com.packt.webstore.config; import org.springframework.security.web.context .AbstractSecurityWebApplicationInitializer; public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer { }
addProduct.jsp
file and add the following code after the <a href="?language=en" >English</a>|<a href="?language=nl" >Dutch</a>
anchor tag:<a href="<c:url value="/logout" />">Logout</a>
http://localhost:8080/webstore/
; you will see the welcome screen.products
page by entering the URL, http://localhost:8080/webstore/market/products
; a login page will be shown. Enter an arbitrary username and password; you will see an Invalid username and password error:john
and the password pa55word
, and press the Log in button; you should be able to see the regular products
page.http://localhost:8080/webstore/market/products/add
; again the login page will be shown with the error message Access Denied: You are not authorised!admin
and the password root123
, and press the Log in button; you should be able to see the regular add products page with a logout button in the top-right corner.As usual, in order to use Spring Security in our project, we need some Spring Security-related JARs; from steps 1 to 4 we just added those JARs as Maven dependencies.
In step 5, we created one more controller called LoginController
to handle all login-related web requests. It simply contains a single request mapping method to handle login-login failure and log out requests. Since the request mapping method returns a view named login
, we need to create a view file called login.jsp
, which is what we did in step 6.
login.jsp
contains many tags with the bootstrap
style class applied to enhance the look and feel of the login form; we don't need to concentrate on those tags. But some important tags are used to understand the flow; the first one is the <c:if>
tag:
<c:if test="${param.error != null}"> <div class="alert alert-danger"> <p>Invalid username and password.</p> </div> </c:if>
<c:if >
is a special JSTL tag to check a condition; it is more like an if...else
condition that we use in our programming language. Using this <c:if>
tag we are simply checking whether the page request parameter contains a variable called error
; if the request parameter contains a variable called error
we simply show an error message, Invalid username and password, within the <p>
tag using the <spring:message>
tag.
Similarly, we are also checking whether the request parameter contains variables called logout
and accessDenied
; if so we show the corresponding message, also within the <P>
tag:
<c:if test="${param.logout != null}"> <div class="alert alert-success"> <p>You have been logged out successfully.</p> </div> </c:if> <c:if test="${param.accessDenied != null}"> <div class="alert alert-danger"> <p>Access Denied: You are not authorised! </p> </div> </c:if>
So now we facilitated all possible login-related messages being shown in the login page. The other important tag in login.jsp
is the form
tag, which represents the login form. Notice the action attribute of the form
tag:
<c:url var="loginUrl" value="/login" /> <form action="${loginUrl}" method="post" class="form-horizontal">
We are simply posting our login form values, such as username and password, to the Spring Security authentication handler URL, which is stored in the variable called ${loginUrl}
. Here the special JSTL tag <c:url>
is used to encode the URL.
Okay, now we have created a controller (LoginController
) to dispatch the login page. But we need to tell Spring to present this login page to users if they try to access a page without logging in. How can we enforce that? This is where the WebSecurityConfigurerAdapter
class comes in; by extending WebSecurityConfigurerAdapter
, we can configure the HttpSecurity
object for various security-related settings in our web application. So in step 7, we are simply creating a class called SecurityConfig
to configure the security-related aspects of our web application.
One of the important methods in the SecurityConfig
class is configureGlobalSecurity
; under this method we are simply configuring AuthenticationManagerBuilder
to create two users, john
and admin
, with a specified password and roles:
@Autowired public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser("john") .password("pa55word") .roles("USER"); auth.inMemoryAuthentication().withUser("admin") .password("root123") .roles("USER","ADMIN"); }
The next important method is configure
; within this method we are doing some authentication-and authorization-related configuration and we will see these one by one. The first configuration tells Spring MVC that it should redirect the users to the login page if authentication is required; here the loginpage
attribute denotes to which URL it should forward the request to get the login form.
Remember this request path should be the same as the request mapping of the login
()
method of LoginController.
We are also setting the user name parameter and password parameter name in this configuration:
httpSecurity.formLogin().loginPage("/login") .usernameParameter("userId") .passwordParameter("password");
With this configuration, while posting the username and password to the Spring Security authentication handler through the login page, Spring expects those values to be bound under the variable name userId
and password
respectively; that's why, if you notice, the input tags for username and password carry the name attributes userId
and password
in step 6:
<input type="text" class="form-control" id="userId" name="userId" placeholder="Enter Username" required> <input type="password" class="form-control" id="password" name="password" placeholder="Enter Password" required>
Similarly, Spring handles the log out operation under the /logout
URL; that's why in step 9 we formed the logout link on the add products page, as follows:
<a href="<c:url value="/logout" />">Logout</a>
Next, we are just configuring the default success URL, which denotes the default landing page after a successful login; similarly the authentication failure URL indicates to which URL the request needs to be forwarded in the case of login failure:
httpSecurity.formLogin().defaultSuccessUrl("/market/products/add") .failureUrl("/login?error");
Notice we are setting the request parameter to error
in the failure URL; thus when the login page is rendered, it will show the error message Invalid username and password in the case of login failure. Similarly, we can also configure the logout success URL, which denotes where the request needs to be forwarded to after a logout:
httpSecurity.logout().logoutSuccessUrl("/login?logout");
You can also see that we are setting the request parameter as logout
for the logout success URL to match the condition checking that we performed in login.jsp
:
<c:if test="${param.logout != null}"> <div class="alert alert-success"> <p>You have been logged out successfully.</p> </div> </c:if>
And similarly, we also configured the redirection URL for the access denied page in the case of authorization failure as follows:
httpSecurity.exceptionHandling().accessDeniedPage("/login?accessDenied");
The request parameter we are setting here for the access denied page URL should match the parameter name that we are checking in the login.jsp
page:
<c:if test="${param.accessDenied != null}"> <div class="alert alert-danger"> <p>Access Denied: You are not authorised! </p> </div> </c:if>
So now we have configured almost all redirection URLs that specify to which page the user should get redirected in the case of login success, login failure, logout success, and access denial. The next configuration is the more important as it defines which user should get access to which page:
httpSecurity.authorizeRequests() .antMatchers("/").permitAll() .antMatchers("/**/add").access("hasRole('ADMIN')") .antMatchers("/**/market/**").access("hasRole('USER')");
The preceding configuration defines three important authorization rules for our web application, in terms of Ant pattern matchers. The first one allows the request URLs that end with /
, even if the request doesn't carry any roles. The next rule allows all request URLs that end with /add
, if the request has the role ADMIN
. The third rule allows all request URLs that have the path /market/
if they have the role USER
.
Okay we defined all security-related configurations in the security context file, but Spring should know about this configuration file and will have to read this configuration file before booting the application. Only then can it create and manage those security-related configurations. How can we instruct Spring to pick up this file? The answer is AbstractSecurityWebApplicationInitializer
; by extending the AbstractSecurityWebApplicationInitializer
class, we can instruct Spring MVC to pick up our SecurityConfig
class during bootup. So in step 8, that's what we are doing.
After finishing all the steps, if you run your application and enter the http://localhost:8080/webstore/
URL, you are able to see the welcome screen. Now try to access the products
page by entering the http://localhost:8080/webstore/market/products
URL; a login page will be shown. Enter an arbitrary username and password; you will see an Invalid username and password error.
Now enter the username john
and the password pa55word
, and press the Log in button; you should be able to see the regular products
page. Now try to access the add products page by entering the http://localhost:8080/webstore/market/products/add
URL; again a login page will be shown with an error message, Access Denied: You are not authorised! Now enter the username admin
and the password root123
, and press the Log in button; you should be able to see the regular add products page with a logout button in the top-right corner.
Which URL is the Spring Security default authentication handler listening on for the username and password?
/login
/login?userName&password
/handler/login
What is the default logout handler URL for Spring Security?
/logout
/login?logout
/login?logout=true