Two Login Form in One Login Page Spring Boot Security

In this post, we will see how to create two login form in a login page using spring boot security in java. The spring boot security module is used to validate these two login forms in one login page. The multiple login forms can be added as like the example explained here.

In many application, we need to handle two different group of users to login from a default login page. There should be two different login entry points in this page. In this case, we need to create two login form in a single login page. One login form is used for the regular customers. the another login form is for administrators or company employees.

In this post, we address two login form in a login page design. The step by step procedure to create the two login form with screenshots are added.

Two Login Form in a Login Page


Preparing Database Tables

There are three tables required for authenticating spring boot security. The three tables are USERS, ROLE and USER_ROLE. The Users table contains user related details such as username, password etc. The role table contains the roles supported in the spring boot security. The users and role table has many to many relationship using user_role table. The mysql database is used in this example. The sql is as below.

In the users table only username and password are important columns, rest of the columns are optional. These columns can be removed if it is not required for your application.

drop table users_role;
drop table role;
drop table users;

create table users(
user_id int primary key AUTO_INCREMENT,
username varchar(50) unique not null,
password varchar(50) not null,
disabled boolean default false,
account_expired boolean default false,
account_locked boolean default false,
credentials_expired boolean default false
);

create table role (
role_id integer primary key AUTO_INCREMENT,
role_name varchar(50)
);

create table user_role (
user_role_id integer primary key AUTO_INCREMENT,
user_id integer references users(user_id),
role_id integer references role(role_id)
);


Preparing Database Table Data

The below sql will provide the sample data in the database table. This data is used to test the spring boot security in this example. The password is not encrypted in this example. The encryption code is modified to insert the encrypted password.

insert into role (role_name) values('ADMIN');
insert into role (role_name) values('USER');

insert into users (username, password) values('admin', 'password');
insert into users (username, password) values('user', 'password');

insert into user_role (user_id, role_id) values(1,1);
insert into user_role (user_id, role_id) values(2,2);


Create project and maven dependency

In the spring boot, create a spring boot security project. Add the maven dependency spring-boot-starter-web and tomcat-embed-jasper for the spring boot web application with MVC module. Add the maven dependency spring-boot-starter-security to enable the spring boot security module. Add the maven dependency spring-boot-starter-data-jpa and mysql-connector-java for database connection using JPA and mysql database.

The following xml shows the actual pom.xml file. Please note that tomcat dependence is also added along with this.

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.7.RELEASE</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.yawintutor</groupId>
	<artifactId>Spring-Application</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>SpringBootSecurityMultipleForm</name>
	<description>Spring Boot Project</description>

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

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<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.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.apache.tomcat.embed</groupId>
			<artifactId>tomcat-embed-jasper</artifactId>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>


Spring Boot Main Class

The spring boot main class is created while creating the project. The default spring boot main class is shown as below.

package com.yawintutor;

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

@SpringBootApplication
public class SpringBootSecurityMultipleFormApplication {

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


Spring Boot Controller Layer

The controller class contains all the method that invokes jsp pages. The login page is same for both admin and application user. After the successful login, admin login will redirect to admin dashboard. The user login will redirect to users dashboard.

src/main/java/com/yawintutor/TestController.java

package com.yawintutor;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping
public class TestController {
	@RequestMapping("/")
	public ModelAndView defaultHome() {
		return new ModelAndView("login");
	}

	@RequestMapping("/login")
	public ModelAndView login() {
		return new ModelAndView("login");
	}

	@RequestMapping("/dashboard")
	public ModelAndView userDashboard() {
		return new ModelAndView("dashboard");
	}

	@RequestMapping("/admin/")
	public ModelAndView admin() {
		return new ModelAndView("login");
	}

	@RequestMapping("/admin/login")
	public ModelAndView adminlogin() {
		return new ModelAndView("login");
	}

	@RequestMapping("/admin/dashboard")
	public ModelAndView admindashboard() {
		return new ModelAndView("admin/dashboard");
	}
}


Spring Boot JSP Layer

The login jsp file contains two login form, the user login form is used for the normal user of the application. The second login form is for admin users. The admin privileged users can login to this form.

src/main/webapp/WEB-INF/jsp/login.jsp

<table width="100%" border=1>
<tr>
<td width=50%>
<center>
<h1>Welcome to Admin Spring Boot Security</h1>
<% if("true".equals(request.getParameter("adminerror"))) {%>
	<h2 style="color:red;">Invalid username / password</h2>
<% } %>
<form method="POST" action="/admin/login">
	User Name : <input type="text" name="username" value="admin"/><br><br>
	Password  : <input type="password" name="password" value="password"/><br><br>
	<input type="submit" name="submit"/>
</form>
</center>
</td>

<td width=50%>
<center>
<h1>Welcome to Spring Boot Security</h1>
<% if("true".equals(request.getParameter("usererror"))) {%>
	<h2 style="color:red;">Invalid username / password</h2>
<% } %>
<form method="POST" action="/login">
	User Name : <input type="text" name="username" value="user"/><br><br>
	Password  : <input type="password" name="password" value="password"/><br><br>
	<input type="submit" name="submit"/>
</form>
</center>
</td>
</tr>

</table>

src/main/webapp/WEB-INF/jsp/dashboard.jsp

<center>
<h1>Welcome to Spring Boot User DashBoard</h1>

<h2>You are in Spring Boot User DashBoard Page</h2>
<br><a href="/logout">logout</a>
</center>

src/main/webapp/WEB-INF/jsp/admin/dashboard.jsp

<center>
<h1>Welcome to Spring Boot Admin DashBoard</h1>

<h2>You are in Spring Boot Admin DashBoard Page</h2>
<br><a href="/logout">logout</a>
</center>


Spring Boot Application.properties Configurations

The application.properties file contains configuration for the jsp files and the mysql database configurations. This example uses mysql database to validate the user and admin credentials. In the application.properties change the database details according to your environment.

spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp

spring.datasource.url=jdbc:mysql://localhost/testdb
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update


Spring Boot Admin Security Configuration

Create an admin security configuration java file to extend the WebSecurityConfigurerAdapter class. In the login page, if an admin user tries to login /admin/login url is called, this admin security configuration will validate the request. The url is configured as “/admin/**.” When you log in successfully, the admin page is redirected to “/admin/dashboard”.

The @order(1) annotation will inform the security of the spring boot to validate this java configuration class first.

src/main/java/com/yawintutor/SpringBootAdminSecurityConfiguration.java

package com.yawintutor;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
@Order(1)
public class SpringBootAdminSecurityConfiguration extends WebSecurityConfigurerAdapter {
	@Autowired
    BCryptPasswordEncoder bCryptPasswordEncoder;
	
	@Autowired
    UserDetailsService adminDetailsServiceImpl;
	
	@Override
	public void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(adminDetailsServiceImpl).passwordEncoder(bCryptPasswordEncoder);

		// Use this code for inmemeory database authentication
		//auth.inMemoryAuthentication()
		//	.withUser("admin").password("{noop}password").roles("ADMIN");
	}

	@Override
	public void configure(HttpSecurity http) throws Exception {
		http
			.antMatcher("/admin/**")
			.authorizeRequests().anyRequest().authenticated()
			.and().formLogin().loginPage("/admin/login")
				.defaultSuccessUrl("/admin/dashboard", true)
				.failureUrl("/admin/login?adminerror=true")
			.permitAll()
			.and().logout().logoutUrl("/admin/logout").logoutSuccessUrl("/admin/login");
		http.csrf().disable();

	}	
}	


Spring Boot User Security Configuration

Create another Java security configuration file for the regular user of the application. The regular user will invoke the login page using the default “http:/localhost:8080/” url. The user login page “http:/localhost:8080/login” will be shown to the user if the user is not authenticated. After successful login, the user is redirected to the “http:/localhost:8080/dashboard” user dashboard page.

The @Order(2) annotation tells the security of the spring boot to invoke this security configuration after the admin configuration.

src/main/java/com/yawintutor/SpringBootUserSecurityConfiguration.java

package com.yawintutor;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
@Order(2)
public class SpringBootUserSecurityConfiguration extends WebSecurityConfigurerAdapter {
	@Autowired
	BCryptPasswordEncoder bCryptPasswordEncoder;
	
	@Autowired
	UserDetailsService userDetailsServiceImpl;
	
	@Override
	public void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userDetailsServiceImpl).passwordEncoder(bCryptPasswordEncoder);

		// use this code for inmemory database authentication
		//auth.inMemoryAuthentication()
		//	.withUser("user").password("{noop}password").roles("USER");
	}

	@Override
	public void configure(HttpSecurity http) throws Exception {
		http
			.antMatcher("/**")
			.authorizeRequests().anyRequest().authenticated()
			.and().formLogin().loginPage("/login")
				.defaultSuccessUrl("/dashboard", true)
				.failureUrl("/login?usererror=true")
			.permitAll()
			.and().logout().logoutSuccessUrl("/login");
		
		http.csrf().disable();
	}
}


Spring Boot Admin Details Service Class

In the spring boot service layer, create a service class with annotation @Service for admin login by implementing spring boot security interface UserDetailsService. In the service class, the username is sent to the JPA repository class and find the admin user details from the database. A UserDetails class object is created from the admin user and sent to the admin spring boot security configurations file.

src/main/java/com/yawintutor/AdminDetailsServiceImpl.java

package com.yawintutor;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class AdminDetailsServiceImpl implements UserDetailsService {

	@Autowired
	private UsersRepository usersRepository;

	@Autowired
	BCryptPasswordEncoder bCryptPasswordEncoder;

	@Override
	public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
		List<Users> usersList = usersRepository.findAdmin(userName);

		if (usersList != null && usersList.size() == 1) {
			Users users = usersList.get(0);

			List<String> roleList = new ArrayList<String>();
			for (Role role : users.getRoles()) {
				roleList.add(role.getRoleName());
			}

            return User.builder()
                	.username(users.getUsername())
                	//change here to store encoded password in db
                	.password( bCryptPasswordEncoder.encode(users.getPassword()) )
                	.disabled(users.isDisabled())
                	.accountExpired(users.isAccountExpired())
                	.accountLocked(users.isAccountLocked())
                	.credentialsExpired(users.isCredentialsExpired())
                	.roles(roleList.toArray(new String[0]))
                	.build();
		} else {
			throw new UsernameNotFoundException("User Name is not Found");
		}
	}
}


Spring Boot User Details Service Class

In the spring boot service layer, create a service class with annotation @Service for user login by implementing spring boot security interface UserDetailsService. In the service class, the username is sent to the JPA repository class and find the user details from the database. A UserDetails class object is created from the database user and sent to the spring boot security configurations file.

src/main/java/com/yawintutor/UserDetailsServiceImpl.java

package com.yawintutor;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

	@Autowired
	private UsersRepository usersRepository;

	@Autowired
	BCryptPasswordEncoder bCryptPasswordEncoder;

	@Override
	public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
		List<Users> usersList = usersRepository.findUser(userName);

		if (usersList != null && usersList.size() == 1) {
			Users users = usersList.get(0);

			List<String> roleList = new ArrayList<String>();
			for (Role role : users.getRoles()) {
				roleList.add(role.getRoleName());
			}

            return User.builder()
                	.username(users.getUsername())
                	//change here to store encoded password in db
                	.password( bCryptPasswordEncoder.encode(users.getPassword()) )
                	.disabled(users.isDisabled())
                	.accountExpired(users.isAccountExpired())
                	.accountLocked(users.isAccountLocked())
                	.credentialsExpired(users.isCredentialsExpired())
                	.roles(roleList.toArray(new String[0]))
                	.build();
		} else {
			throw new UsernameNotFoundException("User Name is not Found");
		}
	}
}


Spring Boot Bean Configuration

The spring boot bean configuration loads the BCryptPasswordEncoder for both admin and user validation.

src/main/java/com/yawintutor/SpringBootConfiguration.java

package com.yawintutor;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class SpringBootConfiguration {
	@Bean
	public BCryptPasswordEncoder getBCryptPasswordEncoder() {
		return new BCryptPasswordEncoder();
	}

}


Spring Boot JPA Repository Layer

In the spring boot JPA Repository layer, create a User repository to get data from the database and authenticate the user information. Two API is created in JPA Repository class, findUser() and findAdmin(). The findUser() api gets the user data from mysql database and sends back to UserDetailsServiceImpl class. The findAdmin() api gets the admin user data from mysql database and sends back to AdminDetailsServiceImpl class.

src/main/java/com/yawintutor/UsersRepository.java

package com.yawintutor;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface UsersRepository extends JpaRepository<Users, Integer>{
	
    @Query("SELECT u FROM Users u join u.roles r WHERE u.username = :username and r.roleName='USER'")
    public List<Users> findUser(@Param("username") String username);

    @Query("SELECT u FROM Users u join u.roles r WHERE u.username = :username and r.roleName='ADMIN'")
    public List<Users> findAdmin(@Param("username") String username);

}


Spring Boot Entity Layer

In this example, three tables are used for the user authentication. The users table contains the user related information. The role table contains the spring boot authentication roles. The user_role is the linking table that provides many to many relationship between users and role table. The below entity classes contains the entity mapping of the database tables.

src/main/java/com/yawintutor/Users.java

package com.yawintutor;

import java.util.List;

import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;

@Entity
public class Users {
	@GeneratedValue(strategy = GenerationType.AUTO)
	@Id
	private long userId;
	private String username;
	private String password;
	private boolean disabled;
	private boolean accountExpired;
	private boolean accountLocked;
	private boolean credentialsExpired;

	@ManyToMany(fetch = FetchType.EAGER)
	@JoinTable(name = "user_role", 
		joinColumns = @JoinColumn(name = "user_id"), 
		inverseJoinColumns = @JoinColumn(name = "role_id"))
	List<Role> roles;

	public long getUserId() {
		return userId;
	}

	public void setUserId(long userId) {
		this.userId = userId;
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public boolean isDisabled() {
		return disabled;
	}

	public void setDisabled(boolean disabled) {
		this.disabled = disabled;
	}

	public boolean isAccountExpired() {
		return accountExpired;
	}

	public void setAccountExpired(boolean accountExpired) {
		this.accountExpired = accountExpired;
	}

	public boolean isAccountLocked() {
		return accountLocked;
	}

	public void setAccountLocked(boolean accountLocked) {
		this.accountLocked = accountLocked;
	}

	public boolean isCredentialsExpired() {
		return credentialsExpired;
	}

	public void setCredentialsExpired(boolean credentialsExpired) {
		this.credentialsExpired = credentialsExpired;
	}

	public List<Role> getRoles() {
		return roles;
	}

	public void setRoles(List<Role> roles) {
		this.roles = roles;
	}
}

src/main/java/com/yawintutor/Role.java

package com.yawintutor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Role {
	@GeneratedValue(strategy = GenerationType.AUTO)
	@Id
	private int roleId;
	private String roleName;

	public int getRoleId() {
		return roleId;
	}

	public void setRoleId(int roleId) {
		this.roleId = roleId;
	}

	public String getRoleName() {
		return roleName;
	}

	public void setRoleName(String roleName) {
		this.roleName = roleName;
	}
}


Run the application

The images below images demonstrate how to test the application. The admin and regular user will be accessed as “http://localhost:8080/“.

username : user
password : password

username : admin
password : password

Try the same way for the admin user in the left side of the login form.



Summary

In real time application, the same login page is required to login two different groups of users. In this case, two login form is required in a login page. The spring boot security will be used to validate in this post.



Related Articles

Leave a Reply

Your email address will not be published. Required fields are marked *