Spring Security 5 Form Login with Database Provider

Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. It is one of the powerful and highly customizable authentication and access-control framework in Java ecosystem.

This article is going to focus on Spring Security Form Login which is one of the most necessary part of the most web applications. The example I am presenting here is a part of pdf (Programming Discussion Forum), a web application built with Spring 5, Hibernate 5, Tiles and i18n.

1. Setting up maven dependencies

The main Maven dependencies required for form login are spring-security-web and spring-security-config. However, to provide database backed UserDetailsService, we need to have dependencies to support that as well. 

<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>com.bitMiners</groupId>
   <artifactId>pdf-app</artifactId>
   <version>0.0.1</version>
   <packaging>war</packaging>

   <properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      <spring.version>5.1.3.RELEASE</spring.version>
      <aspect.version>1.9.2</aspect.version>
      <jackson.version>2.9.8</jackson.version>
      <hibernate.version>5.2.17.Final</hibernate.version>
      <hibernate.validator>5.2.1.Final</hibernate.validator>
      <spring.data.jpa>2.1.3.Final</spring.data.jpa>
      <c3p0.version>0.9.5.2</c3p0.version>
      <spring-security.version>5.1.2.RELEASE</spring-security.version>
   </properties>

   <dependencies>
      <!-- spring -->
      <dependency>
         <groupId>org.springframework</groupId>
         <artifactId>spring-tx</artifactId>
         <version>${spring.version}</version>
      </dependency>
      <dependency>
         <groupId>org.springframework</groupId>
         <artifactId>spring-orm</artifactId>
         <version>${spring.version}</version>
      </dependency>
      <dependency>
         <groupId>org.springframework</groupId>
         <artifactId>spring-webmvc</artifactId>
         <version>${spring.version}</version>
      </dependency>

      <!--security-->
      <dependency>
         <groupId>org.springframework.security</groupId>
         <artifactId>spring-security-config</artifactId>
         <version>${spring-security.version}</version>
         <exclusions>
            <exclusion>
               <artifactId>spring-asm</artifactId>
               <groupId>org.springframework</groupId>
            </exclusion>
         </exclusions>
      </dependency>
      <dependency>
         <groupId>org.springframework.security</groupId>
         <artifactId>spring-security-web</artifactId>
         <version>${spring-security.version}</version>
      </dependency>
      <dependency>
         <groupId>org.springframework.security</groupId>
         <artifactId>spring-security-taglibs</artifactId>
         <version>${spring-security.version}</version>
      </dependency>

      <!-- servlets and jps -->
      <dependency>
         <groupId>javax.servlet</groupId>
         <artifactId>jstl</artifactId>
         <version>1.2</version>
      </dependency>
      <dependency>
         <groupId>javax.servlet</groupId>
         <artifactId>javax.servlet-api</artifactId>
         <version>4.0.1</version>
         <scope>provided</scope>
      </dependency>

      <dependency>
         <groupId>org.apache.tiles</groupId>
         <artifactId>tiles-extras</artifactId>
         <version>3.0.8</version>
         <exclusions>
            <exclusion>
               <groupId>org.slf4j</groupId>
               <artifactId>jcl-over-slf4j</artifactId>
            </exclusion>
         </exclusions>
      </dependency>
      <!-- Hibernate -->
      <dependency>
         <groupId>org.hibernate</groupId>
         <artifactId>hibernate-core</artifactId>
         <version>${hibernate.version}</version>
      </dependency>
      <!-- Hibernate-C3P0 Integration -->
      <dependency>
         <groupId>org.hibernate</groupId>
         <artifactId>hibernate-c3p0</artifactId>
         <version>${hibernate.version}</version>
      </dependency>

      <!-- c3p0 -->
      <dependency>
         <groupId>com.mchange</groupId>
         <artifactId>c3p0</artifactId>
         <version>${c3p0.version}</version>
      </dependency>

      <dependency>
         <groupId>mysql</groupId>
         <artifactId>mysql-connector-java</artifactId>
         <version>5.1.34</version>
      </dependency>
   </dependencies>
</project>

You can checkout pom.xml in Github repository for other plugins detail.

2. Entity Mapping

Let us create two @Entity classes, named as User and Authority, to map with database tables as follows.

package com.bitMiners.pdf.domain;

import org.hibernate.validator.constraints.NotEmpty;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "user")
public class User implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @NotEmpty
    @Column(nullable = false, unique = true)
    private String username;
    @NotEmpty
    private String password;

    private Date dateCreated;

    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JoinTable(name = "user_authority",
            joinColumns = { @JoinColumn(name = "user_id") },
            inverseJoinColumns = { @JoinColumn(name = "authority_id") })
    private Set<Authority> authorities = new HashSet<>();

    public User() {
    }
    // getters and setters
}
package com.bitMiners.pdf.domain;

import com.bitMiners.pdf.domain.types.AuthorityType;

import javax.persistence.*;

@Entity
@Table(name = "authority")
public class Authority {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    @Enumerated(EnumType.STRING)
    private AuthorityType name;

    public Authority() {}

    public Authority(AuthorityType name) {
        this.name = name;
    }
    // getters and setters
}

It is always good to have authorities as configurable value, but for easiness, let us define as enum for now.

package com.bitMiners.pdf.domain.types;

public enum  AuthorityType {
    ROLE_ADMIN,
    ROLE_USER
}

3. Database table populate

Once entities are defined, you can put import.sql under resources folder in project structure so that Hibernate can populate tables with SQL statements inside.

INSERT INTO `authority`(`name`, `id`) VALUES ('ROLE_ADMIN', 1);
INSERT INTO `authority`(`name`, `id`) VALUES ('ROLE_USER', 2);

INSERT INTO `user_authority`(`authority_id`, `user_id`) VALUES (1, 1);
INSERT INTO `user_authority`(`authority_id`, `user_id`) VALUES (2, 2);

INSERT INTO `user` (`id`, `username`, `password`, `dateCreated`) VALUES (1,'ironman','$2a$10$jXlure/BaO7K9WSQ8AMiOu3Ih3Am3kmmnVkWWHZEcQryZ8QPO3FgC','2015-11-15 22:14:54');

INSERT INTO `user` (`id`, `username`, `password`, `dateCreated`) VALUES (2,'rabi','$2a$10$0tFJKcOV/Io6I3vWs9/Tju8OySoyMTpGAyO0zaAOCswMbpfma0BSK','2015-10-15 22:14:54');

4. Retrieving a User

In order to retrieve a user associated with a username, let us create UserRepositoryImpl which implements UserRepository as below:

package com.bitMiners.pdf.repositories;
import com.bitMiners.pdf.domain.User;

public interface UserRepository extends CrudRepository<User, Integer> {
    User getUserByUsername(String username);
}
package com.bitMiners.pdf.repositories.impl;

import com.bitMiners.pdf.domain.User;
import com.bitMiners.pdf.repositories.UserRepository;
import org.hibernate.SessionFactory;
import org.hibernate.query.NativeQuery;
import org.hibernate.query.Query;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
@Transactional
public class UserRepositoryImpl implements UserRepository {

    @Autowired
    private SessionFactory sessionFactory;

    @Override
    public User getUserByUsername(String username) {
        Query<User> query = sessionFactory.getCurrentSession().createQuery("FROM User u where u.username=:username", User.class);
        query.setParameter("username", username);
        return query.uniqueResult();
    }
}

5. UserDetailsService implementation

We need to implement the org.springframework.security.core.userdetails.UserDetailsService interface in order to provide our own service implementation. I have added UserDetailsServiceImpl which implements UserDetailsService to retrieve the User object using the repository, and if it exists, wrap it into a PdfUserDetails object, which implements UserDetails, and returns it as below:

package com.bitMiners.pdf.services.impl;

import com.bitMiners.pdf.domain.PdfUserDetails;
import com.bitMiners.pdf.domain.User;
import com.bitMiners.pdf.repositories.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
    private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

    @Autowired
    private UserRepository userRepository;

    @Transactional(readOnly = true)
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.getUserByUsername(username);

        if (user == null) {
            throw new UsernameNotFoundException("User not found.");
        }

        log.info("loadUserByUsername() : {}", username);

        return new PdfUserDetails(user);
    }
}

PdfUserDetails model is defined as below:

package com.bitMiners.pdf.domain;

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

import java.util.Collection;
import java.util.stream.Collectors;

public class PdfUserDetails implements UserDetails {
    private User user;
 
    public PdfUserDetails(User user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return user.getAuthorities().stream().map(authority -> new SimpleGrantedAuthority(authority.getName().toString())).collect(Collectors.toList());
    }

    public int getId() {
        return user.getId();
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    public User getUserDetails() {
        return user;
    }
}

6. Spring MVC controller

Let us add LoginController to handle custom success and failure during login.

package com.bitMiners.pdf.controllers;

import com.bitMiners.pdf.domain.PdfUserDetails;
import com.bitMiners.pdf.domain.User;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;

import javax.servlet.http.HttpSession;

@SessionAttributes({"currentUser"})
@Controller
public class LoginController {
    private static final Logger log = LogManager.getLogger(LoginController.class);

    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public String login() {
        return "login";
    }

    @RequestMapping(value = "/loginFailed", method = RequestMethod.GET)
    public String loginError(Model model) {
        log.info("Login attempt failed");
        model.addAttribute("error", "true");
        return "login";
    }

    @RequestMapping(value = "/logout", method = RequestMethod.GET)
    public String logout(SessionStatus session) {
        SecurityContextHolder.getContext().setAuthentication(null);
        session.setComplete();
        return "redirect:/welcome";
    }

    @RequestMapping(value = "/postLogin", method = RequestMethod.POST)
    public String postLogin(Model model, HttpSession session) {
        log.info("postLogin()");

        // read principal out of security context and set it to session
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        validatePrinciple(authentication.getPrincipal());
        User loggedInUser = ((PdfUserDetails) authentication.getPrincipal()).getUserDetails();

        model.addAttribute("currentUser", loggedInUser.getUsername());
        session.setAttribute("userId", loggedInUser.getId());
        return "redirect:/wallPage";
    }

    private void validatePrinciple(Object principal) {
        if (!(principal instanceof PdfUserDetails)) {
            throw new  IllegalArgumentException("Principal can not be null!");
        }
    }
}

7. Spring Security Java Configuration

Now, let’s add a Spring Security configuration class that extends WebSecurityConfigurerAdapter. Here, addition of annotation @EnableWebSecurity provides Spring Security and also provides MVC integration support.

package com.bitMiners.pdf.config;

import com.bitMiners.pdf.services.impl.UserDetailsServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public UserDetailsService userDetailsService() {
        return new UserDetailsServiceImpl();
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService());
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(authenticationProvider());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests().antMatchers("/wallPage").hasAnyRole("ADMIN", "USER")
                .and()
                .authorizeRequests().antMatchers("/login", "/resource/**").permitAll()
                .and()
          .formLogin().loginPage("/login").usernameParameter("username").passwordParameter("password").permitAll()
                .loginProcessingUrl("/doLogin")
                .successForwardUrl("/postLogin")
                .failureUrl("/loginFailed")
                .and()
                .logout().logoutUrl("/doLogout").logoutSuccessUrl("/logout").permitAll()
                .and()
                .csrf().disable();
    }
}

The above configuration has following elements to create the login form:

authorizeRequests() is how we allow anonymous access on /login,/resource/** and secure rest of the resource paths.

formLogin() is to define login form with username and password input. This has other methods that we can use to configure the behavior of the form login:

  • loginPage() – the custom login page url
  • loginProcessingUrl() – the url to submit the username and password to
  • defaultSuccessUrl() – the landing page after a successful login
  • failureUrl() – the landing page after an unsuccessful login

Authentication Manager is DaoAuthenticationProvider, backed by UserDetailsService which is accessing database via UserRepository repository.

BCryptPasswordEncoder is a password encoder.

8. Add Spring Security to Web Application

Now, we need to let Spring know about our Spring Security Config by registering on root config as below:

package com.bitMiners.pdf.config;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[] { HibernateConfig.class, WebSecurityConfig.class };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[] { WebMvcConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }
}

I have skipped including HibernateConfig.class and WebMvcConfig.class here for brevity. But you can find them on Github repository easily.

8. Adding Login Form

Let us add login form on login.jsp as below:

<div class="panel-body">
    <form action="doLogin" method="post">
        <fieldset>
            <legend>Please sign in</legend>

            <c:if test="${not empty error}">
                <div class="alert alert-danger">
                    <spring:message code="AbstractUserDetailsAuthenticationProvider.badCredentials"/>
                    <br/>
                </div>
            </c:if>
            <div class="form-group">
                <input class="form:input-large" placeholder="User Name"
                       name='username' type="text">
            </div>
            <div class="form-group">
                <input class=" form:input-large" placeholder="Password"
                       name='password' type="password" value="">
            </div>
            <input class="btn" type="submit"
                   value="Login">
        </fieldset>
    </form>
</div>

Note here, action doLogin for form submission is same as of login processing url in security config above.

9. Demoing App

http://localhost:8080/login takes you on login page:

login-page

If you try with bad credentials, you'll see the error message as below:

login-failIf login authentication successful, then it redirects to the wallpage. Below is screenshot for user with role ROLE_USER. It is for you to find out how it looks for ROLE_ADMIN 🙂

If you click on logout button once you logged in, then it will take you to the home page after clearing all the session.

10. Conclusion

In this example we configured a Spring Security form login authentication process and saw how easily we can configure advanced authentication process with methods available.

The project is available on Github repository. You can clone it and run it with Vagrant or IDE configuration or however you want.

Happy coding!!