Spring Security 5 Form Login With Database Provider
We look at how to use Spring Security 5 to add authentication/authorization protocols to a Java-based application.
Join the DZone community and get the full member experience.
Join For FreeSpring Security is a framework that focuses on providing both authentication and authorization to Java applications. It is one of the most powerful and highly customizable authentication and access control frameworks in the Java ecosystem.
This article is going to focus on Spring Security Form Login which is one of the most necessary parts of 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>
<hibernate.version>5.2.17.Final</hibernate.version>
<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 my GitGub repository for other plugin details.
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 ease, let us define this as an enum for now.
package com.bitMiners.pdf.domain.types;
public enum AuthorityType {
ROLE_ADMIN,
ROLE_USER
}
3. Populate a Database Table
Once entities are defined, you can put import.sql
under the resources folder in the 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, the addition of tge 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 the following elements to create the login form:
authorizeRequests() is how we allow anonymous access to /login,/resource/**
and secure the rest of the resource paths.
formLogin() is used to define the 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 which we submit the username and password.
- 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 a database via the UserRepository
repository.
BCryptPasswordEncoder is a password encoder.
8. Add Spring Security to aWeb 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 the GitHub repository.
8. Adding a Login Form
Let us add a login form in the login.jsp file, as shown 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, the action doLogin
for form submission is same as of the login processing URL in the security config above.
9. Demoing App
http://localhost:8080/login
takes you to the login page:
If you try with bad credentials, you'll see the error message as below:
If login authentication is successful, then it redirects to the homepage.
If you click on logout button once you are 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 processes with the methods available.
The project is available on GitHub. You can clone it and run it with Vagrant or an IDE configuration or however you want.
Happy coding!
Opinions expressed by DZone contributors are their own.
Comments