Click here to Skip to main content
15,867,704 members
Articles / Web Development / Spring

Spring MVC Application With Spring Security and Spring Rest

Rate me:
Please Sign up or sign in to vote.
4.89/5 (2 votes)
14 Mar 2018MIT19 min read 18.7K   420   11  
In this article, I will present a fully working Spring MVC application. It integrates with Spring Security, and Spring Rest. The sample application can be used as a template while the tutorial gives you a general idea of how each component work.

Introduction

This tutorial will discuss how to use annotations to setup a Spring MVC web application, integrating with Spring Security, and Spring Rest. I started learning this about a year ago. And there was little documentation. Now, there is a lot. What I am trying to do with this tutorial is to provide a more comprehensive example than the ones out there.

What is the significance of using annotation to do a Spring based web application? Before this, you had to use XML based configuration to setup the Spring IoC container, and to describe how components interact with each other. Fully annotated Spring application frees the developer from creating a lot of XML configuration files. And the developer can design his/her own version of configuration files to host necessary information or data for their application's needs. I believe this is the greatest strength that spring annotations for application development. As you are going through the code and the tutorial, you will see what I meant.

How the Example Works

This is a typical Spring MVC based web application. It has a default home page everyone can access. There are also three pages, each has a different access level. User can only log in with a specific role in order to access each of these pages. There is also a simple restful service that is also protected. User can only use this service when logged in with the appropriate role.

The user authentication and authorization information are kept in a plain text file. When a user attempts to access a page that the user does not have access to, it will show the log in page. Once the user can login with the right credential, the user will be assigned with the proper roles, then the user will be authorized to access the page.

To demonstrate how to create a RESTFul service, I added just one RestController class with one method. I just want to demonstrate one point, the RESTFul service can be protected by the Spring Security. Spring Security is configured as a conventional authentication and authorization component. That is, once a legit user logged in, a session will be used to track the user's authentication and authorization until the same user logs off or is kicked off due to inactivity for an extended period.

There are quite a lot of files. But I will not go over every single one, just the vital ones. Hopefully, it will paint a picture that clearly describes how Spring MVC, Spring Security and Spring Rest works, together. If not, download the source code and run it and see. Let's dig in.

The Start up Class

As described earlier, instead of using XML for specifying spring configuration, everything is done with Java annotations. You won't even see a web.xml. In order to get this working, there must be an entry point -- a class that server container can recognize and start up the web application. Here is the class:

Java
package org.hanbo.general.web.servlet;

import org.hanbo.general.web.config.RootConfig;
import org.hanbo.general.web.config.WebConfig;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class ServletStart extends
   AbstractAnnotationConfigDispatcherServletInitializer
{
   @Override
   protected Class<?>[] getRootConfigClasses()
   {
      return new Class[] { RootConfig.class, WebConfig.class };
   }
   
   @Override
   protected Class<?>[] getServletConfigClasses()
   {
      return null;
   }
   
   @Override
   protected String[] getServletMappings()
   {
      return new String[] { "/" };
   }
}

This class is pretty easy to understand. It is called ServletStart. It is inherited from a class called AbstractAnnotationConfigDispatcherServletInitializer. When this web application is deployed to the web server, the container will recognize this class as the start up class. AbstractAnnotationConfigDispatcherServletInitializer is a sub class of Spring's DispatcherServlet class, which makes my class ServletStart a subclass of Spring's DispatcherServlet class. And this makes my class a sub class. And hence it can be used for starting up the web application.

There are three methods, the middle one is not used. The first one returns an array of class type objects. The two that were specified were class RootConfig and WebConfig. The instances of these two classes are providing more configurations. The last one returns the servlet mapping path in a string array. The servlet mapping specified is "/". This is equivalent of the tag "<servlet-mapping>" in a web.xml file.

Configuration by Annotations

Two more classes were introduced in the previous section. One is called RootConfig, the other is called WebConfig. As mentioned, they are used for providing configurations of the web application. Let's start with RootConfig. It is pretty simple:

Java
package org.hanbo.general.web.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan({
   "org.hanbo.general.web"
})
public class RootConfig
{
}

RootConfig class itself does not do much, it has no properties and no methods. What is important are the two annotations to the class. The annotation @Configuration indicates the class provides configuration information. The other annotation @ComponentScan(...) tells Spring framework that for the packages specified in the parameters, do scans of injectable classes, and create the dependency injection graph of all objects. The trick here is to specify the root package, which in this case is "org.hanbo.general.web", and it will go through all the sub packages underneath, for all the injectable classes.

What makes a class an injectable class? If you annotate a class with @Component, @Service, @Controller, and @RestController, etc. Then these classes can be autowired via @Autowired into other classes.

There is another class, "WebConfig". Here is the code:

Java
package org.hanbo.general.web.config;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;

@EnableWebMvc
@Configuration
@ComponentScan({ 
   "org.hanbo.general.web",
   "org.hanbo.general.web.entities",
   "org.hanbo.general.web.repositories",
   "org.hanbo.general.web.services"
})
public class WebConfig extends WebMvcConfigurerAdapter {
 
   @Override
   public void addResourceHandlers(ResourceHandlerRegistry registry) {
      registry.addResourceHandler("/assets/**")
         .addResourceLocations("/assets/");
   }
   
   @Bean
   public InternalResourceViewResolver viewResolver() {
      InternalResourceViewResolver viewResolver 
                         = new InternalResourceViewResolver();
      viewResolver.setViewClass(JstlView.class);
      viewResolver.setPrefix("/WEB-INF/views/jsp/");
      viewResolver.setSuffix(".jsp");
      return viewResolver;
   }
}

This class "WebConfig" is slightly more complicated. It extends class called WebMvcConfigurerAdapter. This base class is a default implementation of interface WebMvcConfigurer. By extending it, I can override just a subset of all the methods in the interface. In this case, I only implement two methods. One is addResourceHandlers(). Overriding this method allows me to hard code the folder "/assets/**" (located in the folder webapps) as the base location of all static contents and web page templates. The other method is viewResolver(). This one specifies that I want to use JSTL for view construction; from location "/WEB-INF/views/jsp/", all the page templates can be used, and the files the web application looks for has extension ".jsp". More on this when we get to the MVC controller.

This class is also annotated with @Configuration and with @ComponentScan. I don't think the annotation @ComponentScan is necessary here because it was already specified on "RootConfig". I left this here just to show that, if you add a @ComponentScan to any class that was annotated with @Configuration and it will scan the packages for injectable classes. You can try this by removing one and leaving the other in and run the whole app and see if the whole thing still works.

The annotation @EnableWebMvc allows all the configuration defined in the class WebConfig to be added into the web application. That is all about this class.

Security Configurations

To add Spring security into this web application, took me a while to understand how. And it was super simple. The first thing is to add a sub class to AbstractSecurityWebApplicationInitializer. Here is the code:

Java
package org.hanbo.general.web.config;

import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;

public class SecurityConfigInitializer
   extends AbstractSecurityWebApplicationInitializer
{
}

When the web application starts up, the above class will register a springSecurityFilterChain object. All done through component scan, I guess. Then what is needed is a class that will extend WebSecurityConfigurerAdapter. Here is the code:

Java
package org.hanbo.general.web.config;

import org.hanbo.general.web.security.SampleAccessDeniedHandler;
import org.hanbo.general.web.security.UserAuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
   @Autowired
   private UserAuthenticationService authenticationProvider;
   
   @Override
   protected void configure(HttpSecurity http)
      throws Exception
   {
      http.authorizeRequests()
         .antMatchers("/").permitAll() 
         .and().formLogin().loginPage("/login")
         .usernameParameter("username")
         .passwordParameter("password")
         .defaultSuccessUrl("/index", true).failureUrl("/accessDenied")
         .successHandler(new SavedRequestAwareAuthenticationSuccessHandler()) // not necessary.
         .and().logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
         .logoutSuccessUrl("/logoutPage");
   }

   @Autowired
   protected void configure(AuthenticationManagerBuilder authMgrBuilder)
      throws Exception
   {
      authMgrBuilder.authenticationProvider(authenticationProvider);
   }
   
   @Bean
   public AccessDeniedHandler accessDeniedHandler()
   {
      return new SampleAccessDeniedHandler();
   }
}

This class "SecurityConfig" is annotated with @Configuration. This means the class is providing configuration. It is also annotated with @EnableWebSecurity, which indicates that Spring security will be enabled for this application, and the security configuration will be found in this class (because it is annotated with @EnableWebSecurity).

Annotation @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) is an interesting one. I added it so that I can use role based authorization on MVC action methods. That is, for the controllers, all the action methods would have @PreAuthorize("hasRole('ROLE_...')") or @PreAuthorize("hasAnyRoles('ROLE_...', 'ROLE_...', ...)"). When user accesses these action methods, unless the users are logged in and have the necessary roles, the users would get 403 instead.

Now let's dissect the methods defined in this class. The first one, configure(HttpSecurity http) defines how security works when user interacts with the web app. The chain of method call basic does the following:

  • Any sub url of the web app will be permitted for access anonymously. The reason for this is that I want to lock down only specific pages that are securely accessible.
  • The login page shall be routed to "/login"., which will be a form that user can enter the credential to gain access.
  • The form will contain two fields, "username" and "password" that would be extracted and passed to the authentication manager.
  • When user logs in, the first thing they see would be the page "/index".
  • When user fails authentication, the page that will display would be "/accessDenied".
  • I used SavedRequestAwareAuthenticationSuccessHandler class to handle scenario where after login successfully, the page will redirect to the page that user is trying to access.
  • Logout request is handled with url "/logout". Note that cleaning up the authentication and authorization cookie is done by Spring Security, there is no extra code needed. However, in order to display a log out page, I called the method "logoutSuccess()".

The second method is for setting the authentication provider. This is a very important configuration to do. A customized authentication provider allows you to implement your own way of using input user name and password for authentication. A typical scenario would be taking the user name and password, find the user in database and compare the hashed password against the hashed password in database. If they match, then load the user's roles and attach to the user. Afterwards, whenever the user accesses a page, the roles associated with the user will be used to determine authorization.

The last method of the class defines a bean. There were some issues with the security configuration. I believe it might be a bug within the version of Spring security I was using. This forces me to define a AccessDeniedHandler so that whenever an access denied occurred, it will redirect to an access denied page. Before we get to the definition of my AccessDeniedHandler, which is called SampleAccessDeniedHandler, I will first show you my customized authentication provider.

Customizing Authentication Provider

For this sample web application, I have hardcoded four different users in a text file, called "UserInfo.txt" in the resources folder. My authentication provider (called UserAuthenticationService) will load the file, use deserialize into a Java array. Then authenticate the user with user name and password. Once authenticated, all the user roles which are associated with user will be added to the user's authentication token. Here is the code:

Java
package org.hanbo.general.web.security;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.hanbo.general.web.security.models.UserModel;
import org.hanbo.general.web.security.models.UserRoleModel;
import org.hanbo.general.web.security.utils.SecurityUtils;
//import org.hanbo.writer.admin.services.LoginUserService;
import org.springframework.beans.factory.annotation.Autowired;
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.stereotype.Service;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

@Service
public class UserAuthenticationService
   implements AuthenticationProvider
{
   private static Logger _logger = LogManager.getLogger(UserAuthenticationService.class);
   
   @Autowired
   private LoginUserService userService;
   
   @Override
   public Authentication authenticate(Authentication authentication)
      throws AuthenticationException
   {
      Authentication retVal = null;
      String name = authentication.getName();
      String password = authentication.getCredentials().toString();
      
      if (name == null || name.length() <= 0)
      {
         _logger.error("Authentication failed. User name is null or empty.");
         return retVal;
      }
      
      if (password == null || password.length() <= 0)
      {
         _logger.error("Authentication failed. User password is null or empty.");
         return retVal;
      }
      
      UserModel authenticatedUser = this.authenticateUser(name, password);
      if (authenticatedUser != null)
      {
         boolean isUserActive = authenticatedUser.isActive();
         if (!isUserActive)
         {
            return null;
         }
         
         Authentication userAuth = createAuthentication(
            authenticatedUser, password
         );
         
         return userAuth;
      }
      
      return null;
   }

   @Override
   public boolean supports(Class<?> authentication)
   {
      return authentication.equals(UsernamePasswordAuthenticationToken.class);
   }
   
   private Authentication createAuthentication(
      UserModel userPrincipal, String credential
   )
   {
      List<GrantedAuthority> grantedAuths
         = createLoginUserAuthority(userPrincipal.getUserRoles());
      
      if(grantedAuths.size() == 0)
      {
         return null;
      }
     
      credential = encryptPassword(credential);

      _logger.debug("Creating authentication here...");
      
      Authentication auth
         = new UsernamePasswordAuthenticationToken(
            userPrincipal, credential, grantedAuths
         );

      _logger.debug("Creating authentication here... Done.");

      return auth;
   }
   
   private UserModel authenticateUser(String userName, String userPass)
   {
      UserModel user = userService.getLoginUser(userName);
      if (user != null)
      {
         String passEncrypted = user.getHashedUserPass();
         
         if (SecurityUtils.passwordEquals(userPass, passEncrypted))
         {
            _logger.debug("Authentication Successful.");
            return user;
         }
      }
      
      return null;
   }
   
   private List<GrantedAuthority> createLoginUserAuthority(List<UserRoleModel> userRoles)
   {
      List<GrantedAuthority> grantedAuths = new ArrayList<GrantedAuthority>();
      Set<GrantedAuthority> uniqueAuths = new HashSet<GrantedAuthority>();
      
      for (UserRoleModel userRole : userRoles)
      {
         if (userRole.getRoleName().equals(UserRoleModel.ROLE_SITE_ADMIN))
         {
            uniqueAuths.add(new SimpleGrantedAuthority("ROLE_SITE_ADMIN"));
            uniqueAuths.add(new SimpleGrantedAuthority("ROLE_SITE_STAFF"));
            uniqueAuths.add(new SimpleGrantedAuthority("ROLE_SITE_USER"));
            uniqueAuths.add(new SimpleGrantedAuthority("ROLE_GUEST"));
         }
         else if (userRole.getRoleName().equals(UserRoleModel.ROLE_SITE_SUPERVISOR))
         {
            uniqueAuths.add(new SimpleGrantedAuthority("ROLE_SITE_STAFF"));
            uniqueAuths.add(new SimpleGrantedAuthority("ROLE_SITE_USER"));
            uniqueAuths.add(new SimpleGrantedAuthority("ROLE_GUEST"));                  
         }
         else if (userRole.getRoleName().equals(UserRoleModel.ROLE_SITE_USER))
         {
            uniqueAuths.add(new SimpleGrantedAuthority("ROLE_SITE_USER"));
            uniqueAuths.add(new SimpleGrantedAuthority("ROLE_GUEST"));
         }
         else if (userRole.getRoleName().equals(UserRoleModel.ROLE_GUEST))
         {
            uniqueAuths.add(new SimpleGrantedAuthority("ROLE_GUEST"));
         }
         else
         {
            uniqueAuths.clear();
         }
      }
      
      if (uniqueAuths.size() > 0)
      {
         grantedAuths.addAll(uniqueAuths);
         _logger.info("Number of roles: " + grantedAuths.size());
      }
      
      return grantedAuths;
   }

   private static String encryptPassword(Object credential)
   {
      String encryptedPass = "";
      if (credential != null)
      {
         String password = (String)credential;
         
         encryptedPass = SecurityUtils.encryptPassword(password);
      }
      
      return encryptedPass;
   }
}

This class is very straight forward. The first method authenticate() does the work of authenticating the user. First, it checks for the user name and password, makes sure they are not null or empty. Then the user's name and password are passed to a LoginUserService object. This LoginUserService will load all the users from the text file. Deserialize the text string as a JSon object into a list of UserModel objects. The user name will be used to find the UserModel object that matches, and the input password would be hashed and compared with the hashed password of the UserModel object. If there is a match, then the UserModel object will be returned back to authenticate(). In authenticate(), if there is a valid UserModel object, then it will check make sure the user is still active, and if the user is, then all the roles associated will be added to the authentication token for the user and return. If all the checks fails, null is returned instead of a valid authentication token.

The method that adds the user roles to the authentication token is the method createLoginUserAuthority:

Java
private List<GrantedAuthority> createLoginUserAuthority(List<UserRoleModel> userRoles)
{
   List<GrantedAuthority> grantedAuths = new ArrayList<GrantedAuthority>();
   Set<GrantedAuthority> uniqueAuths = new HashSet<GrantedAuthority>();
   
   for (UserRoleModel userRole : userRoles)
   {
      if (userRole.getRoleName().equals(UserRoleModel.ROLE_SITE_ADMIN))
      {
         uniqueAuths.add(new SimpleGrantedAuthority("ROLE_SITE_ADMIN"));
         uniqueAuths.add(new SimpleGrantedAuthority("ROLE_SITE_STAFF"));
         uniqueAuths.add(new SimpleGrantedAuthority("ROLE_SITE_USER"));
         uniqueAuths.add(new SimpleGrantedAuthority("ROLE_GUEST"));
      }
      else if (userRole.getRoleName().equals(UserRoleModel.ROLE_SITE_SUPERVISOR))
      {
         uniqueAuths.add(new SimpleGrantedAuthority("ROLE_SITE_STAFF"));
         uniqueAuths.add(new SimpleGrantedAuthority("ROLE_SITE_USER"));
         uniqueAuths.add(new SimpleGrantedAuthority("ROLE_GUEST"));                  
      }
      else if (userRole.getRoleName().equals(UserRoleModel.ROLE_SITE_USER))
      {
         uniqueAuths.add(new SimpleGrantedAuthority("ROLE_SITE_USER"));
         uniqueAuths.add(new SimpleGrantedAuthority("ROLE_GUEST"));
      }
      else if (userRole.getRoleName().equals(UserRoleModel.ROLE_GUEST))
      {
         uniqueAuths.add(new SimpleGrantedAuthority("ROLE_GUEST"));
      }
      else
      {
         uniqueAuths.clear();
      }
   }
   
   if (uniqueAuths.size() > 0)
   {
      grantedAuths.addAll(uniqueAuths);
      _logger.info("Number of roles: " + grantedAuths.size());
   }
   
   return grantedAuths;
}

What this method does is that if the user is an admin, it has all the roles (including staff, user, and guest). If the user has the role of a staff, then the user will also have roles user and guest. If the user has the role of a user, then the user will have the roles user and guest. If the user has the role of guest, then the user would only have guest role. If there are no roles associated with the user, the user's roles list will be cleared.

The second method in my UserAuthenticationService is called supports. Here is the code:

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

What it does is tell the web application that the authentication token is of type UsernamePasswordAuthenticationToken. This is all there is about this UserAuthenticationService.

Access Denied Handler

The reason I needed a access denied handler is that I want to intercept the access denied exception, then I can redirect the response to a customized access denied page. The reason I had to do this I believe is either I misconfigured the authorization interaction or it is a bug in the version of Spring Security I am using. I bet I have misconfigured the Spring Authorization. If you are interested, try and fix this. Even though this is a problem, it presented an opportunity to learn something. In this section, I will show you how this access denied handler works. Here is the code:

Java
package org.hanbo.general.web.security;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

@Component
public class SampleAccessDeniedHandler
   implements AccessDeniedHandler
{
   @Override
   public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex) 
        throws IOException, ServletException {
      String redirectUrl = request.getContextPath();
      redirectUrl = redirectUrl + "/accessDenied";
      response.sendRedirect(redirectUrl);
   }
}

As you can see, the implementation of SampleAccessDeniedHandler has just one method to override. It is called handle(). The method takes an HttpServletRequest object, and HttpServletResponse object, and the access denied exception object. Basically, the object of this class will intercept the access denied exception with the original request and a response object that would eventually be returned to the user.

Regardless of the original request, and the exception, the method handle() will first get the context path, which is the base URL of the web application. Then I add sub URL "/accessDenied" to it. And use that as the redirect URL. Finally, I set the redirect URL to the response. And that is it. What happens when this method is invoked, it will redirect to that URL.

The last thing I like to cover is the controller for handling the user authentication and authorization. The class is called UsersController, and here is the code:

Java
package org.hanbo.general.web.controllers;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class UsersController
{
   @RequestMapping(value="login", method= RequestMethod.GET)
   public ModelAndView login()
   {
      
      ModelAndView retVal = new ModelAndView();
      retVal.setViewName("login");
      retVal.addObject("viewName", "Please Log In");
      
      return retVal;
   }
   
   @RequestMapping(value="/accessDenied", method = RequestMethod.GET)
   public ModelAndView loginError()
   {
      ModelAndView retVal = new ModelAndView();
      retVal.setViewName("noAccess");
      retVal.addObject("viewName", "Access Denied");
      retVal.addObject("noAccessTitle", "Access Denied");
      retVal.addObject("noAccessMsg", "You don't have permission to access this page");

      return retVal;
   }
   
   @RequestMapping(value="/logoutPage", method = RequestMethod.GET)
   public ModelAndView logoutPage()
   {
      ModelAndView retVal = new ModelAndView();
      retVal.setViewName("login");
      retVal.addObject("viewName", "Logout Successfully");
      retVal.addObject("noAccessTitle", "Logout Successfully");
      retVal.addObject("noAccessMsg", "You have logged out successfully");
      
      return retVal;
   }
}

In this class, there are three methods, each deals with a URL. All of them are accessible anonymously. The first one is called login(). It would route user request to the log in page. The second one is called loginError(). This one routes user request to the log in error page. And the last one is called logoutPage(), which would display the logout page after user actually logged out.

These methods are all simple action methods (a .NET term). They all return objects of type ModelAndView. The object of ModelAndView, has a few useful methods one can call:

  • setViewName(), this will set the jsp template that will be used for display
  • addObject(), this will add view data to the model so that the view template can use to create the final view

More about Spring MVC will be covered in the next section. The page display for the login page looks like this:

HTML
<%@ page contentType="text/html" pageEncoding="UTF-8"%>
<%@ taglib prefix="t" tagdir="/WEB-INF/tags" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<t:loginTemplate>
   <jsp:attribute name="loginCss">
   </jsp:attribute>
   <jsp:attribute name="loginJs">
      <script src="${pageContext.request.contextPath}/assets/custom/js/login.js"></script>
   </jsp:attribute>
   <jsp:body>
    <div style="margin-top: 100px">
    <div class="row">
       <div class="col-xs-2 col-sm-3 col-md-4 col-lg-4"></div> 
       <div class="col-xs-8 col-sm-6 col-md-4 col-lg-4">
          <div class="content-box">
             <h3>Please Sign In</h3>
             <form id="loginForm" class="form-horizontal" 
             action="<c:url value='/login'/>" method="POST">
                <div class="form-group">
                   <div class="col-xs-12">
                      <input type="text" class="form-control" 
                      id="username" name="username" placeholder="User Name">
                   </div>
                </div>
                <div class="form-group">
                   <div class="col-xs-12">
                      <input type="password" class="form-control" 
                      id="password" name="password" placeholder="Password">
                   </div>
                </div>
                <div id="loginError" class="row" style="display: none;">
                   <div class="col-xs-12">
                      <div id="loginErrorMsg" 
                      class="alert alert-danger alert-padding" role="alert"></div>
                   </div>
                </div>
                <div class="row">
                   <div class="col-xs-2"></div> 
                   <div class="col-xs-5"><input type="submit" 
                   class="form-control btn btn-primary" value="Log In"></div> 
                   <div class="col-xs-5"><input type="reset" 
                   class="form-control btn btn-default" value="Clear"></div>              
                </div>
                <input type="hidden" name="${_csrf.parameterName}" 
                value="${_csrf.token}" />
             </form>
          </div>
       </div> 
       <div class="col-xs-2 col-sm-3 col-md-4 col-lg-4"></div> 
    </div>
    </div>
   </jsp:body>
</t:loginTemplate>

The syntax is different from regular HTML. I was using JSTL so that I can break the pages into components and layout. As you can see, in the above JSP code, there are two input fields:

  • one is called "username"
  • the other is called "password"

Those two fields are the ones used by SecurityConfig to recognize what the user credentials will be (see the configure() method). One important thing to remember is that when the login form is submitted, it is submitted to the "/login" url, and the http method used is "post". The other important thing to remember is that I used csrf token to provide additional security verifications. It is where you see the name="${_csrf.parameterName}". You can disable it if you want to. But for a MVC based web application, using CSRF for additional security measure is good practice.

Spring MVC Controller

To demo Spring MVC with Spring Security, I created two controllers. One is called TestController, which would handle the MVC based web pages. The other is call TestApiController, which would handle the RESTFul requests. Here is the code for TestController:

Java
package org.hanbo.general.web.controllers;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class TestController
{
   @RequestMapping(value="/", method = RequestMethod.GET)
   public ModelAndView defaultPage()
   {
      ModelAndView retVal = new ModelAndView();
      retVal.setViewName("index");
      retVal.addObject("userRole", "anonymous");
      retVal.addObject("pageName", "index");
      
      return retVal;
   }
   
   @RequestMapping(value="/index", method = RequestMethod.GET)
   public ModelAndView index()
   {
      ModelAndView retVal = new ModelAndView();
      retVal.setViewName("index");
      retVal.addObject("userRole", "anonymous");
      retVal.addObject("pageName", "index");
      
      return retVal;
   }
   
   @PreAuthorize("hasRole('ROLE_SITE_ADMIN')")
   @RequestMapping(value="/secure/testPage1", method = RequestMethod.GET)
   public ModelAndView testPage1()
   {
      ModelAndView retVal = new ModelAndView();
      retVal.setViewName("testPage1");
      retVal.addObject("userRole", "site admin");
      retVal.addObject("pageName", "testPage1");
      
      return retVal;
   }

   @PreAuthorize("hasRole('ROLE_SITE_STAFF')")
   @RequestMapping(value="/secure/testPage2", method = RequestMethod.GET)
   public ModelAndView testPage2()
   {
      ModelAndView retVal = new ModelAndView();
      retVal.setViewName("testPage2");
      retVal.addObject("userRole", "site staff");
      retVal.addObject("pageName", "testPage2");
      
      return retVal;
   }
   
   @PreAuthorize("hasRole('ROLE_SITE_USER')")
   @RequestMapping(value="/secure/testPage3", method = RequestMethod.GET)
   public ModelAndView testPage3()
   {
      ModelAndView retVal = new ModelAndView();
      retVal.setViewName("testPage3");
      retVal.addObject("userRole", "site user");
      retVal.addObject("pageName", "testPage3");
      W
      return retVal;
   }
}

Compare to all the configuration code we have been through, this TestController class is pretty easy to understand. The class is annotated with @Controller, which indicates the class TestController is a MVC controller. In this class, there are five public methods, The first two are representing the default home page (accessing through "/" or "/index"), they are the same. The other three methods each represent a page that a user with a specific role can access:

  • testPage1(): Only user with admin role can access this page
  • testPage2(): Only user with staff role can access this page
  • testPage3(): Only user with user role can access this page

All these methods are easy to understand. All these methods don't have any parameters. All of them are annotated with @RequestMapping. This annotation sets up the sub url (via parameter value) against the context path. It also allows a developer to define the HTTP method (via parameter method) which this method can handle. There are a couple more arguments that can be used with this annotation to further specify how this method handles an http request. I am not going to cover them here.

All three methods have the annotation @PreAuthorize. This annotation specifies authorization is necessary to access this method. This is the mechanism for role based authorization. The argument that can be passed in looks like this:

@PreAuthorize("hasRole('<role name>')")

Basically, the value is an expression. Only when the authentication token has the role of <role name>. A role name looks like this: ROLE_SITE_ADMIN; or ROLE_SITE_STAFF; etc. One important thing to remember is that if you use role based authorization, the role name has to start with prefix "ROLE_". Without this prefix, role based authorization will not work. Try it. You can remove the prefix of "ROLE_" from all the occurrences of "ROLE_SITE_STAFF". Then login as a site staff and try to access testPage2. You will get an "Access Denied" error. By looking at these @PreAuthorize annotations, you can guess the user roles necessary to access these pages.

All the methods do similar things, return a ModelAndView object which has a view template, and some values in the model so that they can displayed in the web page. Next, I will cover a little bit on RESTFul APIs.

RESTFul API Demo

For the sake of simplicity, I created a simple rest controller that demonstrates how it can be done, which is very simple to do. The class is called TestApiController. Here is the code:

Java
package org.hanbo.general.web.controllers;

import org.hanbo.general.web.models.CarModel;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import org.apache.log4j.*;

@RestController
public class TestApiController
{
   private static Logger logger = LogManager.getLogger(TestApiController.class);
   
   @PreAuthorize("hasRole('ROLE_SITE_USER')")
   @RequestMapping(value="/secure/api/carModel", method= RequestMethod.GET)
   @ResponseBody
   public ResponseEntity<CarModel> getCarModel(
         @RequestParam("make")
         String make,
         @RequestParam("model")
         String model)
   {
      logger.info("Car Maker: " + make);
      logger.info("Car Model: " + model);
      
      CarModel carEntity = new CarModel();
      carEntity.setMaker("Honda");
      carEntity.setModel("Accord");
      carEntity.setYear(2016);
      carEntity.setPrice(19667.28f);
      
      ResponseEntity<CarModel> retVal = new ResponseEntity<CarModel>(carEntity, HttpStatus.OK);
      return retVal;
   }
}

This class is almost the same as the MVC controller TestController. It is annotated with @RestController, meaning the controller class is for RESTFul service instead for MVC web page navigation. There is only one method in this class. It is called getCarModel(). It takes in a string parameter called "make" and "model", does some calculation, then returns an object back with the make, model, year and price of a car. Of course, this is a dummy action method (a .NET term, again). It is hard coded to return a specific model. What is interesting here is that the car model is serialized into a JSON object and returned. I like to use ResponseEntity because I can set the http status code (codes like 200, 404, 403, 401 or 500) with the response object. But I do realize that sometimes it is overkill, sometimes I just need to return the actual object back, which can be automatically serialized into JSON object for returning.

The method is annotated with @PreAuthorize, @RequestMapping, and @ResponseBody. The annotation @ResponseBody enforces the response to be serialized into JSON string. So technically, I don't need to wrap the object inside a ResponseEntity object. I was just doing it for fun, and demonstrate that if you need to set http status code as part of the response, ResponseEntity is how you can accomplish it.

The two parameters are annotated with @RequestParam. This annotation indicates that the value of these two parameters are from the query string of the url, i.e.:

http://localhost:<port number>/secure/api/carModel?make=Subaru&model=Out+Back

Everything after the question mark is the query string. If you use the above URL, and if you logged in with site user role, then you should see the following response:

{ "maker": "Honda", "model": "Accord", "year": 2016, "price": 19667.28 }

Next, I am going to discuss how to test this application.

How to Build and Test

How to Build

After downloading the source of sample application, the first thing to do is going into all the js folder under asset, and rename the *.sj files back *.js. The project can be built an run via gradle. To build gradle, the command is:

gradle build

To execute it, run the command:

gradle jettyRun

You can also use gradle to create the Eclipse projects:

gradle eclipse

Once the eclipse project and classpath files are generated, you can import them into Eclipse. Note that all these are done with Java 1.8.

How to Test

When you use command gradle jettyRun, if all things goes well, you will see no error in the command line console. Here is a sample screen shot:

Image 1

To access the default index page, the url is: http://localhost:8080/SampleSpring4/index, or: http://localhost:8080/SampleSpring4/. When you navigate to this page, you will see the following:

Image 2

To access test page #1, which is only accessible with site admin account. The user with site admin role is "testadmin". The password is "123test321" (without the double quotes). To test this user role, navigate to: http://localhost:8080/SampleSpring4/secure/testPage1. You will first be presented with the log in page. Enter the user name and password correctly, you will see the following:

Image 3

Look at the navigation bar, there are three test pages available for viewing. The user with site admin role also has site staff, and site user role. So the user has full access to all the pages this web application can offer. You should log out before you try with the next user. On the upper right corner, there is a pull down nav menu called "Account". Click on it, and you will see the "Log Out" option. Click on it will log your user off.

Let's try logging in using user with site staff role. This is super user role with slightly less privilege than the admin role. The user is "teststaff" and password is still "123test321". Navigate to URL: http://localhost:8080/SampleSpring4/secure/testPage2. Then log in with "teststaff", you will see the following screenshot:

Image 4

Now you can see that for this user, there is no test page #1 available. If you attempt to navigate to the test page #1 by url: http://localhost:8080/SampleSpring4/secure/testPage1. You will receive a error page of 403.

Finally, you can test the site user account. First log out, then navigate to: http://localhost:8080/SampleSpring4/secure/testPage3. Log in with user "testuser1" and password "123test321". You will see the following screenshot:

Image 5

Now, try the RESTFul API service. It is accessible with role site user. Navigate to the URL: http://localhost:8080/SampleSpring4/secure/api/getCarModel?make=subaru&model=out+back

{"maker":"Honda","year":2016,"model":"Accord","price":19667.28}

Lastly, we can try with an invalid user. User "testuser2" can be logged in with the same password. However, this user is disabled. So you won't be able to login.

Image 6

Points of Interest

This will be my first and last post on Spring MVC with Spring Security using an app container (Jetty, Glassfish, JBoss). It will be a summary of all the things I have learned regarding Spring MVC, Spring Rest, and Spring Security. The next stage of the work I will be doing is using either Spring Boot or using Jetty jars to create stand alone web applications. It is the future.

I hope this will be of some use to you.

History

  • 3/13/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

 
-- There are no messages in this forum --