In Spring Boot Security, we will discuss how to create multiple login pages using spring boot security with database authentication and authorization for different users role such as admin, user in the custom login pages in spring boot.

In real-time applications, we needed to have different login pages to be accessed within the same application. One for the regular consumer and the other for the administrative functions. In this post, we’ll see how to create two login pages, one for regular users and the other for admin url.

In designing special purpose applications, a login page is required to authenticate users to access the application. We need another url within the framework that is only accessed by administrators for admin support and maintenance purposes. The admin url is not accessible to common people and can only be accessed by internal use.

Typically, the website is accessed with the default url, such as http:/localhost/, and the admin url will be like http:/localhost / admin. In the article, we will see how to build these two login pages using the spring boot security. Spring boot security is used in this example along with basic jsp sites.



Login Pages

CUSTOMER LOGIN PAGE
INTERNAL USERS or ADMINISTRATOR 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.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 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.4.RELEASE</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.yawintutor</groupId>
	<artifactId>Spring-Application</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>SpringBootSecurityMultipleLogin</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 Admin Security Configuration

Create an admin security configuration java file to extend the WebSecurityConfigurerAdapter class. If http:/localhost:8080/admin / url is called, this admin security configuration will validate the request. The url is configured as “/admin/**.” If an admin page is invoked, the login url “/admin / login” will be invoked for authentication. 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);
	}

	@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/accessdenied")
			.permitAll()
			.and().logout().logoutUrl("/admin/logout").logoutSuccessUrl("/admin/login")
			.and().exceptionHandling().accessDeniedPage("/admin/accessdenied");
		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 admin user is not allowed to log in to the regular user login page, and vice versa. 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);
	}

	@Override
	public void configure(HttpSecurity http) throws Exception {
		http
			.antMatcher("/**")
			.authorizeRequests().anyRequest().authenticated()
			.and().formLogin().loginPage("/login")
				.defaultSuccessUrl("/dashboard", true)
				.failureUrl("/accessdenied")
			.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 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;
	}
}


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 Application.properties Configurations

The application.properties file contains two set of properties. The first set is the configuration for the jsp files. The second set contains the mysql database connection configurations.

application.properties

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 Controller Layer

To invoke jsp files, a controller class is created. The login page, dashboard page, and default page must be added for both user and admin access. Admin access will start with “/admin/.”

Create a controller class for the spring boot security application. The controller class will receive the http request from the browser, process the request and send the response back to the browser.

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("/accessdenied")
	public ModelAndView userAccessError() {
		return new ModelAndView("accessdenied");
	}
	
	@RequestMapping("/admin/")
	public ModelAndView admin() {
		return new ModelAndView("admin/login");
	}

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

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

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


Leave a Reply