Click here to Skip to main content
15,879,474 members
Articles / Web Development / Spring

RESTFul Service with Spring Boot and Filter Based Security

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
20 Nov 2018MIT15 min read 26.9K   143   2   1
Set up a RESTFul service using Spring Boot and use Spring Security to secure the RESTFul API
In this tutorial, I will show how to set up a RESTFul service using Spring Boot, and how to use Spring Security to secure the RESTFul API. And I will use a very simple token based security to secure this service.

Introduction

Just recently, I submitted two tutorials to CodeProject regarding Spring Boot. One for setting up a simple MVC application using Spring Boot and Spring MVC with no security. Another goes one step further and integrates with Spring Security to lock down the MVC application. From these tutorials, you can see Spring Boot handles the MVC web application development really well. And it also supports development of RESTFul web services, which I am about to discuss in this tutorial. I will first explain how to create a RESTFul service using Spring Boot and Spring Web. In the second half of the tutorial, I will show a simple way to secure the RESTFul service, a simple token based security for the RESTFul service.

Application designed with MVC architecture, every page interaction would have a round trip from the browser to back end server, then from back end server back to the browser. RESTFul service is different from a typical MVC application. The interaction with RESTFul service is usually exchange of data by browser exchange data and the service. The RESTFul service has no responsibility of constructing the UI display. And this tutorial will show how to design such a web service using Spring Boot.

The Project File Structure

For this project, I have setup the following project structure:

<Base dir>/pom.xml
<Base dir>/src/main/java/org/hanbo/boot/rest/App.java
<Base dir>/src/main/java/org/hanbo/boot/rest/config/RestAppSecurityConfig.java
<Base dir>/src/main/java/org/hanbo/boot/rest/config/security/AuthTokenSecurityProvider.java
<Base dir>/src/main/java/org/hanbo/boot/rest/config/security/RestAuthSecurityFilter.java
<Base dir>/src/main/java/org/hanbo/boot/rest/controllers/SampleController.java
<Base dir>/src/main/java/org/hanbo/boot/rest/controllers/SampleSecureController.java
<Base dir>/src/main/java/org/hanbo/boot/rest/models/CarModel.java
<Base dir>/src/main/java/org/hanbo/boot/rest/models/CarRequestModel.java
<Base dir>/src/main/java/org/hanbo/boot/rest/models/PurchaseTaxModel.java
<Base dir>/src/main/java/org/hanbo/boot/rest/models/UserCredential.java
<Base dir>/src/main/resources/application.properties

These are basic parts that can help create the most basic RESTFul service:

  • App.java: It is the main entry of a Java application. Spring framework uses this to bootstrap and start the application.
  • The controller classes which contain the action methods. These will handle the incoming requests.
  • The classes in the models directory which are the requests and responses.

In addition, I added some configurations to this application. And these files can be found under the folder "configuration" and its sub folder "security".

The POM File

Just like the previous two tutorials, I will start with the Maven POM file. Here it is:

XML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>hanbo-boot-rest</artifactId>
    <version>1.0.1</version>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.5.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-taglibs</artifactId>
        </dependency>
        <dependency>
           <groupId>commons-codec</groupId>
           <artifactId>commons-codec</artifactId>
           <version>1.11</version>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

This Maven POM file is nothing special. In it, there is the Spring Boot Starter Web and Spring Boot Starter Security. One is used for implementing the RESTFul web service, and the other is to provide security for the application. The Spring Security taglib is used to provide the @PreAuthroize annotation to lock down the RESTFul controller action methods. I also added the Apache Commons codec jar to encode and decode Base64 string values, which is used in the HTTP security header and the authorization filter that decoding it.

Unlike the previous two tutorials, which package the project as war files, in this tutorial, the project is packaged as a regular jar file.

The Main Entry

If you have not read my previous two tutorials, you might not know that application written in Spring Boot does not necessarily need to be deployed into an application server to execute. It can be a standalone application. This is why there will be a main entry. The code looks like this:

Java
package org.hanbo.boot.rest;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App
{   
   public static void main(String[] args)
   {
      SpringApplication.run(App.class, args);
   }
}

This is a very simple program. All it does is take the definition of the current class, then do a scan and find out what are the injectable classes and how to setup the dependency tree so that the application can run. Again, I like to point out that there is no XML base configuration. Everything is done by convention or by class annotation.

The Controller Class

The main entry is almost the same for every Spring Boot application. Sometimes, it might be a bit different. Once you have it, you can add a controller and some action methods. Then a web application will be ready for use.

This is different from the MVC based application, which requires the action methods in a controller to return a view back to the user's browser. With RESTFul APIs, the action methods are returning Spring based HTTP responses, which is considerably simpler. Before I venture further, let me show you the code of this REST based controller:

Java
package org.hanbo.boot.rest.controllers;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.hanbo.boot.rest.models.CarModel;
import org.hanbo.boot.rest.models.CarRequestModel;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SampleController
{
   private List<CarModel> allCars;
   
   public SampleController()
   {
      allCars = createCarModelCollection();
   }
   
   @ResponseBody
   @RequestMapping(value="/public/allCars/{yearOfManufacture}", method = RequestMethod.GET)
   public ResponseEntity<List<CarModel>> allCars(
      @PathVariable("yearOfManufacture")  
      int year)
   {
      ResponseEntity<List<CarModel>> retVal = null;
      if (allCars == null || year < 1980)
      {
         List<CarModel> listOfCars = new ArrayList<CarModel>();
         retVal = ResponseEntity.ok(listOfCars);
         return retVal;
      }
      
      List<CarModel> foundCars = allCars
         .stream()
         .filter(x -> x.getYearOfManufacturing() == year)
         .collect(Collectors.toList());
      retVal = ResponseEntity.ok(foundCars);
      return retVal;
   }
   
   @RequestMapping(value="/public/findCar", method = RequestMethod.POST)
   public ResponseEntity<CarModel> findCar(
      @RequestBody
      CarRequestModel req)
   {
      ResponseEntity<CarModel> retVal = null;
      if (allCars == null || req == null)
      {
         retVal = ResponseEntity.ok((CarModel)null);
         return retVal;
      }
      
      Optional<CarModel> foundCar =  allCars
         .stream()
         .filter(x -> x.getYearOfManufacturing() == req.getYear()
            && x.getMaker().equalsIgnoreCase(req.getManufacturer())
            && x.getModel().equalsIgnoreCase(req.getModel()))
         .findFirst();
      if (foundCar.isPresent())
      {
         retVal = ResponseEntity.ok(foundCar.get());
      }
      else
      {
         retVal = ResponseEntity.ok((CarModel)null);         
      }
      
      return retVal;
   }
   
   private List<CarModel> createCarModelCollection()
   {
      List<CarModel> retVal = new ArrayList<CarModel>();
      
      CarModel car = new CarModel();
      car.setFullPrice(20000);
      car.setMaker("Nessan");
      car.setModel("Altima");
      car.setRebateAmount(600);
      car.setSuggestedRetailPrice(19250);
      car.setYearOfManufacturing(2005);
      
      retVal.add(car);

      car = new CarModel();
      car.setFullPrice(20096);
      car.setMaker("Subaru");
      car.setModel("Legacy");
      car.setRebateAmount(487);
      car.setSuggestedRetailPrice(20001);
      car.setYearOfManufacturing(2006);

      retVal.add(car);
      
      car = new CarModel();
      car.setFullPrice(20890);
      car.setMaker("Subaru");
      car.setModel("Outback");
      car.setRebateAmount(695);
      car.setSuggestedRetailPrice(19980);
      car.setYearOfManufacturing(2007);

      retVal.add(car);
      
      car = new CarModel();
      car.setFullPrice(21500);
      car.setMaker("Honda");
      car.setModel("Civic");
      car.setRebateAmount(750);
      car.setSuggestedRetailPrice(20100);
      car.setYearOfManufacturing(2008);

      retVal.add(car);
      
      car = new CarModel();
      car.setFullPrice(22600);
      car.setMaker("Toyota");
      car.setModel("Camery");
      car.setRebateAmount(708);
      car.setSuggestedRetailPrice(21100);
      car.setYearOfManufacturing(2008);

      retVal.add(car);
      
      return retVal;
   }
}

There are a couple noticeable things about this class. First is the annotation used on the class:

Java
@RestController
public class SampleController
{
...
}

If you have read through one of my two earlier tutorials, you will know that for MVC, the annotation used on the class should be @Controller. Defining RESTFul web service uses different annotation, which is @RestController. This is one noticeable difference between defining a RESTFul API controller and a MVC controller.

Now, take a look at one of the action methods:

Java
@RequestMapping(value="/public/allCars/{yearOfManufacture}", method = RequestMethod.GET)
public ResponseEntity<List<CarModel>> allCars(
   @PathVariable("yearOfManufacture")  
   int year)
{
   ResponseEntity<List<CarModel>> retVal = null;
   if (allCars == null || year < 1980)
   {
      List<CarModel> listOfCars = new ArrayList<CarModel>();
      retVal = ResponseEntity.ok(listOfCars);
      return retVal;
   }
   
   List<CarModel> foundCars = allCars
      .stream()
      .filter(x -> x.getYearOfManufacturing() == year)
      .collect(Collectors.toList());
   retVal = ResponseEntity.ok(foundCars);
   return retVal;
}

Another noticeable thing is that this method is the annotation @RequestMapping. This specifies the URL path which the action method can handle. It also specifies what the HTTP method this action method can handle as well. In this case, it handles only HTTP get requests.

The method returns an object of type ResponseEntity<List<CarModel>>. ResponseEntity wraps the actual object that is being returned back in HTTP response. It can also transform the Java object into JSON and prepare the proper HTTP response to be returned. I like to use this object type because it allows me to set the status code, and returned object. Here is how it was used:

Java
ResponseEntity<List<CarModel>> retVal = null;
...

retVal = ResponseEntity.ok(foundCars);
return retVal;

Next, the parameter of the method is annotated with @PathVariable. This is telling the Spring Web that in the URL path, a part of it can be used as a value. The annotation @RequestMapping specifies the URL path pattern: "/public/allCars/{yearOfManufacture}". "{yearOfManufacture}" will be used as parameter value into the method. In this case, the value represented by "{yearOfManufacture}" is passed into the parameter called year.

Lastly, the body of the method looks up all the cars in a list that has the year of manufacture of a specific year. And the found list would be returned back as a JSON list of objects. If no cars are found, an empty list will be returned.

At this point, that is all that is needed to create the RESTFul service. An main entry, a controller, and some DTO data models. I am not going to show you the code of the DTO data models. They are mock object types with mock data for tutorial purposes. To make this tutorial more interesting, I added a security filter, which is what we will discuss next.

Security Filter

It is little different when handling security in a RESTFul service than a MVC web application. When a web application that utilizes the RESTFul service, the service itself does not keep sessions, instead, every request that comes in, the service must check the HTTP header values for authentication data. Once found, it can transform the authenticatiom data into proper authorization. That is, it will create an authorization token with user name, credential, and user roles for the request. Then pass the request to the action methods.

Secured action methods will have annotations like @PreAuthorize(...). Unless the request has the security token with proper role(s), the annotation will force return 403 before the action methods can get the request. If you try hard enough, you can force session based security with authorization cookies. However, RESTFul service supposed to be stateless, using sessions to enforce security would limit the scalability of the service, thus defeat the purpose of being a RESTFul web service.

In this tutorial, I am assuming the request coming from the client end (usually from the browser) is already authenticated. That is, some other service has provided a security token to the request. The RESTFul service, once intercepted the request, the filter will use the token from the request to determine what the authorization is for the request before it is handed off to the actual action method.

The Spring Security Configuration

Here is the class that I have defined the Spring security configuration:

Java
package org.hanbo.boot.rest.config;

import org.hanbo.boot.rest.config.security.AuthTokenSecurityProvider;
import org.hanbo.boot.rest.config.security.RestAuthSecurityFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.
           authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.
           method.configuration.EnableGlobalMethodSecurity;
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;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class RestAppSecurityConfig  extends WebSecurityConfigurerAdapter
{
   @Autowired
   private AuthTokenSecurityProvider authProvider;
   
   @Override
   protected void configure(HttpSecurity http) throws Exception
   {
      System.out.println("    ++++++++++++ Called AuthTokenSecurityProvider.configure
                        ( Http Security )...");
      http.csrf().disable()
         .authorizeRequests()
         .antMatchers("/public/**").permitAll()
         .anyRequest().authenticated();
      http.addFilterAfter(new RestAuthSecurityFilter
           (super.authenticationManager()), BasicAuthenticationFilter.class);
   }
   
   @Override
   protected void configure(AuthenticationManagerBuilder authMgrBuilder)
      throws Exception
   {
      System.out.println("    ++++++++++++ Called AuthTokenSecurityProvider.configure
                        ( Auth Manager )...");
      authMgrBuilder.authenticationProvider(authProvider);
   }
   
   @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
   @Override
   public AuthenticationManager authenticationManagerBean() throws Exception {
       return super.authenticationManagerBean();
   }
}

The class is called "RestAppSecurityConfig".

Java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class RestAppSecurityConfig  extends WebSecurityConfigurerAdapter
{
...
}

It inherits from WebSecurityConfigurerAdapter. Any sub class derived from WebSecurityConfigurerAdapter would provide the security configurations for the web application. The problem is that Spring framework will not look for it unless you annotate the sub class with @Configuration. There are two more annotations for the class. One is called @EnableWebSecurity. This annotation will enable Spring security for the entire application. The other annotation, EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) enables the authorization annotations for the action methods, such as @PreAuthorize(...) and @PostAuthorize(...).

The method called configure(...) is used to define how a request should be handled:

Java
@Override
protected void configure(HttpSecurity http) throws Exception
{
   System.out.println("    ++++++++++++ Called AuthTokenSecurityProvider.configure
                     ( Http Security )...");
   http.csrf().disable()
      .authorizeRequests()
      .antMatchers("/public/**").permitAll()
      .anyRequest().authenticated();
   http.addFilterAfter(new RestAuthSecurityFilter
                      (super.authenticationManager()), BasicAuthenticationFilter.class);
}

In this method, what I have defined is as follows:

  • Any requests route into <appContext>/public/**, will be handled without any authentication. Although this has no effect because we added a customized filter.
  • Any requests route into other sub path that is not "/public/" must be explicitly authenticated and authorized.
  • Add a customized filter that will handle security token in the request. The customized filter is called RestAuthSecurityFilter, and it takes an AuthenticationManager object so that authenticationManager object can properly authorize the request before it is sent to the action method.

Before the AuthenticationManager object can be used, it has to be properly initialized. That is what the next method does:

Java
@Override
protected void configure(AuthenticationManagerBuilder authMgrBuilder)
   throws Exception
{
   System.out.println("    ++++++++++++ Called AuthTokenSecurityProvider.configure
                     ( Auth Manager )...");
   authMgrBuilder.authenticationProvider(authProvider);
}

AuthenticationManagerBuilder is the builder object that can create a AuthenticationManager object. And it needs a authentication provider (AuthenticationProvider). That is why I created my own authentication provider for this. We will cover it in the next section. There is one last method in this class, which is a getter that returns an AuthenticationManager object. This is necessary because my filter is defined as Spring service, and it needs a dependency injection of the AuthenticationManager object. So in order for the start-up to work properly, I needed this getter that returns the AuthenticationManager bean.

Authentication Provider

Customized authentication provider is also a necessity. The simplest way to see this is:

  • An authentication manager builder would create an authentication manager.
  • The authentication manager uses an authentication provider to authenticate the user's credential.

Create a customized authentication provider that allows a programmer to define how the user credential can be verified and how to set the user authorization. A common thing that all of us like to do is:

  • Get user name and password, hash the password.
  • Fetch the user credential from database.
  • Match the user name and password hash against the user credential in the database, if they are equal and user is actively. Then the associated roles from database will be added to the security token.
  • If the user credential does not match, then no security token will be created. When the request is passed down to the action method, the security annotation would fail the request with status code 403.

This is a typical scenario of web application authentication and authorization. For RESTFul service, user log in is handled differently, most of the times, there is a security token in the request. For this example, I am just going to assume the HTTP request has a header entry which can be transformed into a user name and password pair. My authentication provider would be able to check the user credential just like the scenario I have described. My customized authentication provider class looks like the following:

Java
package org.hanbo.boot.rest.config.security;

import java.util.ArrayList;
import java.util.List;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;

@Service
public class AuthTokenSecurityProvider implements AuthenticationProvider
{
   @Override
   public Authentication authenticate(Authentication auth) throws AuthenticationException
   {
      if (auth == null)
      {
         return null;
      }
      
      String name = auth.getName();
      String password = "";
      if (auth.getCredentials() != null) 
      {
         password = auth.getCredentials().toString();
      }
      
      if (name == null || name.length() == 0)
      {
         return null;
      }

      if (password == null || password.length() == 0)
      {
         return null;
      }
      
      Authentication retVal = null;
      List<GrantedAuthority> grantedAuths = new ArrayList<GrantedAuthority>();
      
      if (name.equalsIgnoreCase("anonymous-user") && 
          password.equalsIgnoreCase("anonymouspass"))
      {
         grantedAuths.clear();
         retVal = new UsernamePasswordAuthenticationToken(
            "anonymous", "not-authenticated", grantedAuths
         );
      }
      else if (name.equalsIgnoreCase("Elrick") && 
               password.equalsIgnoreCase("123tset321"))
      {
         grantedAuths.clear();
         grantedAuths.add(new SimpleGrantedAuthority("ROLE_USER"));
         retVal = new UsernamePasswordAuthenticationToken(
            name, "UserAuthenticated", grantedAuths
         );
      }

      System.out.println("Add Auth - User Name: " + retVal.getName());
      System.out.println("Add Auth - Roles Count: " + 
             (retVal.getAuthorities() != null? retVal.getAuthorities().size() : 0));

      return retVal;
   }

   @Override
   public boolean supports(Class<?> tokenClass)
   {
      return tokenClass.equals(UsernamePasswordAuthenticationToken.class);
   }
}

This class has a method called supports(...). This method basically checks whether the authentication token is the right type which this provider can handle. If it is, then it will use its other method authenticate(...) to attempt authenticate and authorize the user.

The method authenticate(...) will check the user name and password, if the user name is "Elrick" and password "123tset321", then authentication token will be set for the user and its role will be "ROLE_USER". If the user name is "anonymous-user" and password is "anonymouspass", then an anonymous user authentication token will be created, and it has no user role. And for all other cases, a null authentication token will be returned. In this case, the authentication process failed. It is handled in my security filter.

Security Filter

Finally, the security filter which I have created for the sake of this tutorial looks like this:

Java
package org.hanbo.boot.rest.config.security;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.apache.tomcat.util.codec.binary.Base64;
import org.hanbo.boot.rest.models.UserCredential;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

@Component
public class RestAuthSecurityFilter extends GenericFilterBean
{
   private AuthenticationManager authManager;
   
   public RestAuthSecurityFilter(AuthenticationManager authManager)
   {
      this.authManager = authManager;
   }
   
   @Override
   public void doFilter(
      ServletRequest request, 
      ServletResponse response,
      FilterChain chain) throws IOException, ServletException
   {
      try
      {
         if (request != null)
         {
            String auth = ((HttpServletRequest)request).getHeader("testAuth");
            System.out.println("Test Auth: " + auth);
            
            SecurityContext sc = SecurityContextHolder.getContext();
            UsernamePasswordAuthenticationToken authReq = null;
            
            authReq = getAuthCredentialFrom(auth);

            Authentication authCool = authManager.authenticate(authReq);
            sc.setAuthentication(authCool);
            chain.doFilter(request, response);
         }
         else
         {
            ((HttpServletResponse)response).sendError
            (HttpServletResponse.SC_BAD_REQUEST, "Invalid Request.");
         }
      }
      catch (Exception ex)
      {
         throw new ServletException("Unknown exception in RestAuthSecurityFilter", ex);
      }
   }
   
   private UsernamePasswordAuthenticationToken getAuthCredentialFrom(String authHdrValue) 
           throws JsonParseException, JsonMappingException, IOException
   {
      UsernamePasswordAuthenticationToken defUserToken =
         new UsernamePasswordAuthenticationToken("anonymous-user", "anonymouspass");
      if (authHdrValue == null || authHdrValue.length() == 0) 
      {
         return defUserToken;
      }
      else
      {
         byte[] decodedBytes = Base64.decodeBase64(authHdrValue);
         String authCred = new String(decodedBytes);
         System.out.println(authCred);
         if (authCred != null && authCred.length() > 0)
         {
            ObjectMapper objMapper = new ObjectMapper();
            UserCredential userCrede = objMapper.readValue(authCred, UserCredential.class);
            if (userCrede != null)
            {
               return new UsernamePasswordAuthenticationToken
                      (userCrede.getUserName(), userCrede.getUserPassword());
            }
            else
            {
               return defUserToken;
            }
         }
         else
         {
            return defUserToken;
         }
      }
   }
}

The class I have defined is called RestAuthSecurityFilter. It extends from GenericFilterBean, so that I can override the filter method called doFilter. The class is annotated with @Component so that if needed, it can be injected. This is the reason why I need a bean getter in the class RestAppSecurityConfig. Well, it is part of the reason. The other is that for this class I added a constructor that took an AuthenticationManager object as parameter. I really need the AuthenticationManager object. You will see why next.

Here is the code for the doFilter():

Java
@Override
public void doFilter(
   ServletRequest request,
   ServletResponse response,
   FilterChain chain) throws IOException, ServletException
{
   try
   {
      if (request != null)
      {
         String auth = ((HttpServletRequest)request).getHeader("testAuth");
         System.out.println("Test Auth: " + auth);

         SecurityContext sc = SecurityContextHolder.getContext();
         UsernamePasswordAuthenticationToken authReq = null;

         authReq = getAuthCredentialFrom(auth);

         Authentication authCool = authManager.authenticate(authReq);
         sc.setAuthentication(authCool);
         chain.doFilter(request, response);
      }
      else
      {
         ((HttpServletResponse)response).sendError
         (HttpServletResponse.SC_BAD_REQUEST, "Invalid Request.");
      }
   }
   catch (Exception ex)
   {
      throw new ServletException("Unknown exception in RestAuthSecurityFilter", ex);
   }
}

This method will first get the untransformed security token from the header of the request. And it is output to the commandline console just for a sanity check. It is done by the following code:

Java
String auth = ((HttpServletRequest)request).getHeader("testAuth");
System.out.println("Test Auth: " + auth);

Then, I use a helper method to authenticate the security token, which is done with the following code:

Java
authReq = getAuthCredentialFrom(auth);

And the code for this helper method is as follows:

Java
private UsernamePasswordAuthenticationToken getAuthCredentialFrom(String authHdrValue) 
        throws JsonParseException, JsonMappingException, IOException
{
   UsernamePasswordAuthenticationToken defUserToken =
      new UsernamePasswordAuthenticationToken("anonymous-user", "anonymouspass");
   if (authHdrValue == null || authHdrValue.length() == 0) 
   {
      return defUserToken;
   }
   else
   {
      byte[] decodedBytes = Base64.decodeBase64(authHdrValue);
      String authCred = new String(decodedBytes);
      System.out.println(authCred);
      if (authCred != null && authCred.length() > 0)
      {
         ObjectMapper objMapper = new ObjectMapper();
         UserCredential userCrede = objMapper.readValue(authCred, UserCredential.class);
         if (userCrede != null)
         {
            return new UsernamePasswordAuthenticationToken
                   (userCrede.getUserName(), userCrede.getUserPassword());
         }
         else
         {
            return defUserToken;
         }
      }
      else
      {
         return defUserToken;
      }
   }
}

The security token before the transformation is a JSON string encoded as a Base64 byte array. It has to be transformed from Base64 encoded byte array back to a string. Here is the code how it is done:

Java
byte[] decodedBytes = Base64.decodeBase64(authHdrValue);
String authCred = new String(decodedBytes);
System.out.println(authCred);

Once we have a readable string of the security token, it would be converted from JSON to an actual Java object. Since Spring starter wab library supports RESTFul web service, it has to include some kind of JSON serializer/deserializer. That library is Jackson. This means we don't have to add another JSON serializer/deserializer library. It is already available. And the way I deserialize the string is as follows:

Java
ObjectMapper objMapper = new ObjectMapper();
UserCredential userCrede = objMapper.readValue(authCred, UserCredential.class);

The UserCredential class is defined as the following:

Java
package org.hanbo.boot.rest.models;

public class UserCredential
{
   private String userName;
   
   private String userPassword;

   public String getUserName()
   {
      return userName;
   }

   public void setUserName(String userName)
   {
      this.userName = userName;
   }

   public String getUserPassword()
   {
      return userPassword;
   }

   public void setUserPassword(String userPassword)
   {
      this.userPassword = userPassword;
   }
}

The helper method will convert the UserCredential object to a UsernamePasswordAuthenticationToken object and return to the caller doFilter() method. Back in the doFilter() method, once it has a valid UsernamePasswordAuthenticationToken object, it is passed into the AuthenticationManager object for attempt authentication and authorization. Which object actual does this authentication and authorization? The right answer would be my authentication provider. The helper method will attempt to return the default anonymous user with no role associated. And in the filter if the request is bad, the response is status code 400. And if there is any exception, the response would be 500 internal server error.

Secured RESTFul API Controller

Now that the security filter and authentication provider is in place, it is time to secure the API controller. I have two controller classes, one is public accessible. That is, the action methods in this class are not annotated with @PreAuthorize, and they are accessible by un-authenticated users. The source code for this controller has been listed in previous sections. If you need a refresher, please go up and check it out. The class name is SampleController.

To test this controller and action methods, you can use the following two URLs:

  • http://localhost:8080/public/allCars/{year of manufacturing}: This can be done with a HTTP GET method.
  • http://localhost:8080/public/findCar: This can be done with a HTTP POST method. The request body is a JSON object. And response will be an JSON string representing the found car object.

The secured API controller uses annotation @PreAuthorize to filter out any request that does not have the correct authorization token. The source code for this controller looks like this:

Java
package org.hanbo.boot.rest.controllers;

import org.hanbo.boot.rest.models.CarRequestModel;
import org.hanbo.boot.rest.models.PurchaseTaxModel;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SampleSecureController
{
   @PreAuthorize("hasRole('ROLE_USER')")
   @ResponseBody
   @RequestMapping(value="/secure/calculateTax.", method = RequestMethod.POST)
   public ResponseEntity<PurchaseTaxModel> calculateTax(
      @RequestBody
      CarRequestModel req)
   {
      PurchaseTaxModel retVal = new PurchaseTaxModel();
      retVal.setYear(req.getYear());
      retVal.setModel(req.getModel());
      retVal.setManufacturer(req.getManufacturer());
      retVal.setTaxAmount(4000);
      retVal.setTaxRate(0.03f);
      
      ResponseEntity<PurchaseTaxModel> resp = ResponseEntity.ok(retVal);
      return resp;
   }
}

As you can see in the class, the action method has a @PreAuthorize annotation. This annotation ensures that only the authenticated users with the role of "ROLE_USER" can access this action method.

To test this controller and action methods, you can use the following URL: http://localhost:8080/secure/calculateTax.

This method only accepts HTTP POST request. More information about how to test this will be covered in the next section.

Test the RESTFul Web Service

Testing this web service is pretty easy. It is all about HTTP requests and response, there is no need to do any page navigation. I use a tool called "Postman". Maybe you have heard of it. Anyways, before we test it, we have to build the whole project.

To build the project, go to the base directory of the project and run the following command:

mvn clean install

To test the application in the base directory, run the following command:

java -jar target\hanbo-boot-rest-1.0.1.jar

The build and the execution will succeed. Let's start testing. Boot up the "Postman" application. When it starts, the screenshot looks like this:

Image 1

Start with the first test scenario, run a requests to get all the cars in a specific manufacturing year. The supported years are: 2005, 2006, 2007, and 2008. Here are the settings for the request:

  • Set the HTTP method to "GET".
  • URL is set to: http://localhost:8080/public/allCars/2008
  • Click the button "Send"

Here is a screenshot of the request:

Image 2

As the web service is running, it will respond with the following:

JSON
[
    {
        "yearOfManufacturing": 2008,
        "model": "Civic",
        "maker": "Honda",
        "suggestedRetailPrice": 20100,
        "fullPrice": 21500,
        "rebateAmount": 750
    },
    {
        "yearOfManufacturing": 2008,
        "model": "Camery",
        "maker": "Toyota",
        "suggestedRetailPrice": 21100,
        "fullPrice": 22600,
        "rebateAmount": 708
    }
]

The screenshot of the response looks like the following:

Image 3

The next test is the HTTP POST request of finding a car. The request URL is: http://localhost:8080/public/findCar

The Postman setting for the HTTP POST request:

  • The HTTP method should be "POST".
  • The URL is: http://localhost:8080/public/findCar
  • Select the tab "Body", and check the radio box "Raw".
  • In the drop down, select "Application/JSON".

Image 4

The request needs a JSON object, and it looks like this:

JSON
{
   "year": 2007,
   "manufacturer": "Subaru",
   "model": "Outback"
}

Here is the screenshot of the request:

Image 5

Click the button "Send". And the response returned will be:

JSON
{
    "yearOfManufacturing": 2007,
    "model": "Outback",
    "maker": "Subaru",
    "suggestedRetailPrice": 19980,
    "fullPrice": 20890,
    "rebateAmount": 695
}

Lastly, the test scenario of the secured RESTFul controller. For this scenario, we need a security header in the request. The full string of the request looks like this:

JSON
{ "userName": "Elrick", "userPassword": "123tset321" }

To add a small twsit, I encode this as a Base64 string, which looks like this:

eyAidXNlck5hbWUiOiAiRWxyaWNrIiwgInVzZXJQYXNzd29yZCI6ICIxMjN0c2V0MzIxIiB9

To setup Postman application for this scenario, here are the steps:

  • The HTTP method should be "POST".
  • The URL is: http://localhost:8080/secure/calculateTax
  • Frist select the "Header" tab
  • In the first available header entry, set the key as "testAuth", and the value to: eyAidXNlck5hbWUiOiAiRWxyaWNrIiwgInVzZXJQYXNzd29yZCI6ICIxMjN0c2V0MzIxIiB9.
  • Select the tab "Body", set the same as the previous request:
    • Check he radio box "Raw".
    • In the drop down, select "Application/JSON".
    • Use the same JSON for the request body.

Image 6

Click the button "Send" and you will get the following response:

JSON
{
    "year": 2007,
    "manufacturer": "Subaru",
    "model": "Outback",
    "taxRate": 0.03,
    "taxAmount": 4000
}

Just to make this a little more interesting, remove the header entry called "testAuth". And run the same request again, you will see error response instead of expected response:

JSON
{
    "timestamp": "2018-11-19T03:43:12.349+0000",
    "status": 403,
    "error": "Forbidden",
    "message": "Forbidden",
    "path": "/secure/calculateTax"
}

Here is the screenshot of the response of a request with the empty "testAuth" header entry:

Image 7

Points of Interest

That is all for this tutorial. A summary of what has been covered in this tutorial:

  • How to set up a RESTFul service using Spring Boot
  • How to add security configuration to the Spring Boot application and configure to use customized filter and authentication provider

Again, I had a blast writing this. I hope you enjoy this one.

History

  • 18th November, 2018 - Initial draft

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Team Leader The Judge Group
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
PraiseThank you Pin
Member 1459919120-Sep-19 8:02
Member 1459919120-Sep-19 8:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.