Role-Based Login in Spring Boot with Spring Security
In this tutorial, you will build a role-based login system in Spring Boot using Spring Security. Users register with a role (Recruiter or Seeker), log in with their email and BCrypt-hashed password, and are automatically redirected to the correct dashboard based on their role.
You will learn how CustomUserDetailsService works, how to configure
role-based URL access with hasAuthority(), how to use a custom
AuthenticationSuccessHandler for redirects, and how to secure
Thymeleaf templates with Spring Security's dialect.
Overview
This project demonstrates a small Online Job Portal application with two roles: Recruiter and Seeker. Each role has its own dashboard, and access is enforced by Spring Security at the URL level.
The application authenticates users against a MySQL database using Spring Data JPA. On successful login, the user is redirected to a role-specific page. Failed logins and unauthorized access are handled by custom error pages.
Key Features
- Custom UserDetailsService — loads users from MySQL instead of in-memory storage.
- BCrypt password encoding — passwords are hashed, never stored in plain text.
- Role-based URL access —
/recruiter/**requires the Recruiter role;/seeker/**requires Seeker. - Custom AuthenticationSuccessHandler — redirects users based on their role.
- CSRF protection — enabled for the login form.
- Thymeleaf Security dialect — conditionally show/hide UI elements based on authentication.
- JPA + MySQL persistence — Login, Recruiter, and Seeker entities saved to database tables.
OnlineJobPortalApplication.java
Standard Spring Boot entry point.
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OnlineJobPortalApplication {
public static void main(String[] args) {
SpringApplication.run(OnlineJobPortalApplication.class, args);
}
}
SecurityConfig.java
The main security configuration. It wires up CustomLoginDetailService,
defines role-based URL access, sets the custom success handler, and enables BCrypt
password encoding.
package com.example.demo.config;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import com.example.demo.model.Login;
import com.example.demo.respository.LoginRepository;
import com.example.demo.service.CustomLoginDetailService;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomLoginDetailService customLoginDetailService;
@Autowired
private LoginRepository loginRepository;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customLoginDetailService)
.passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.requireCsrfProtectionMatcher(new AntPathRequestMatcher("**/login"))
.and()
.authorizeRequests()
.antMatchers("/", "/register", "/login").permitAll()
.antMatchers("/recruiter/**").hasAuthority("Recruiter")
.antMatchers("/seeker/**").hasAuthority("Seeker")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.usernameParameter("username")
.permitAll()
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException, ServletException {
Login login = loginRepository
.getLoginByEmail(request.getParameter("username"));
response.sendRedirect(login.getRole().toLowerCase());
}
})
.failureForwardUrl("/error")
.and()
.logout().permitAll();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Key configuration points:
hasAuthority("Recruiter")— URL access requires the exact role name stored in the database.usernameParameter("username")— tells Spring Security which form field holds the email.AuthenticationSuccessHandler— reads the role and redirects to/recruiteror/seeker.failureForwardUrl("/error")— forwards failed logins to/error, which redirects to/404.
LoginController.java
Handles registration, login page, and role-specific dashboard endpoints. New users are BCrypt-encoded before being saved.
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import com.example.demo.model.Login;
import com.example.demo.respository.LoginRepository;
@Controller
public class LoginController {
@Autowired
private LoginRepository loginRepository;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@PostMapping("/register")
public String register(Login login) {
String encodedPassword = bCryptPasswordEncoder.encode(login.getPassword());
login.setPassword(encodedPassword);
loginRepository.save(login);
return "redirect:/" + login.getRole().toLowerCase();
}
@PostMapping("/home")
public String home(Login login) {
return login.getRole();
}
@GetMapping("/recruiter")
public String recruiterPage(Login login) {
return "recruiter";
}
@GetMapping("/seeker")
public String seekerPage(Login login) {
return "seeker";
}
@GetMapping("/login")
public String loginPage() {
return "login";
}
}
PageController.java
Handles public pages — home and error handling.
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class PageController {
@GetMapping("/")
public String mainPage() {
return "index";
}
@GetMapping("/error")
public String errorPage() {
return "redirect:/404";
}
@GetMapping("/404")
public String error() {
return "error";
}
}
RecruiterController.java
package com.example.demo.controller;
public class RecruiterController {
}
SeekerController.java
package com.example.demo.controller;
public class SeekerController {
}
CustomLoginDetail.java
A custom implementation of Spring Security's UserDetails interface. It wraps
the Login entity and exposes the user's email, password, and role to the
framework.
package com.example.demo.model;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
@Component
public class CustomLoginDetail implements UserDetails {
private Login login;
public CustomLoginDetail(Login login) {
this.login = login;
}
public CustomLoginDetail() {
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority(login.getRole()));
return authorities;
}
@Override
public String getPassword() {
return login.getPassword();
}
@Override
public String getUsername() {
return login.getEmail();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
Entities — Login, Recruiter, Seeker
Three JPA entities are used to persist users and their role-specific profiles.
Login.java
package com.example.demo.model;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "logins")
public class Login {
@Id
private String email;
private String password;
private String role;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
}
Recruiter.java
package com.example.demo.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "recruiters")
public class Recruiter {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String name;
private String company;
@Column(unique = true)
private String email;
@Column(unique = true)
private String contact;
private String website;
private String companydesc;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCompany() {
return company;
}
public void setCompany(String company) {
this.company = company;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getContact() {
return contact;
}
public void setContact(String contact) {
this.contact = contact;
}
public String getWebsite() {
return website;
}
public void setWebsite(String website) {
this.website = website;
}
public String getCompanydesc() {
return companydesc;
}
public void setCompanydesc(String companydesc) {
this.companydesc = companydesc;
}
}
Seeker.java
package com.example.demo.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "seekers")
public class Seeker {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String name;
@Column(unique = true)
private String email;
private String mobile;
private String resumeurl;
private String image;
private String category;
private String experience;
private String technology;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getResumeurl() {
return resumeurl;
}
public void setResumeurl(String resumeurl) {
this.resumeurl = resumeurl;
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getExperience() {
return experience;
}
public void setExperience(String experience) {
this.experience = experience;
}
public String getTechnology() {
return technology;
}
public void setTechnology(String technology) {
this.technology = technology;
}
}
Repositories
Three Spring Data JPA repositories handle database access.
LoginRepository.java
package com.example.demo.respository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.example.demo.model.Login;
@Repository
public interface LoginRepository extends JpaRepository<Login, String> {
Login getLoginByEmail(String email);
}
RecruiterRepository.java
package com.example.demo.respository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.example.demo.model.Recruiter;
@Repository
public interface RecruiterRepository extends JpaRepository<Recruiter, Integer> {
}
SeekerRepository.java
package com.example.demo.respository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.example.demo.model.Seeker;
@Repository
public interface SeekerRepository extends JpaRepository<Seeker, Integer> {
}
Services — CustomLoginDetailService, LoginService, RecruiterService, SeekerService
CustomLoginDetailService.java
Implements UserDetailsService. Spring Security calls
loadUserByUsername() during authentication to fetch the user from the
database.
package com.example.demo.service;
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 com.example.demo.model.CustomLoginDetail;
import com.example.demo.model.Login;
import com.example.demo.respository.LoginRepository;
@Service
public class CustomLoginDetailService implements UserDetailsService {
@Autowired
private LoginRepository loginRepository;
@Override
public UserDetails loadUserByUsername(String email)
throws UsernameNotFoundException {
System.out.println("loaduserbyusername " + email);
Login login = loginRepository.getLoginByEmail(email);
if (login == null) {
throw new UsernameNotFoundException("Could not find login user");
}
return new CustomLoginDetail(login);
}
}
LoginService.java
package com.example.demo.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.demo.model.Login;
import com.example.demo.model.Recruiter;
import com.example.demo.model.Seeker;
import com.example.demo.respository.LoginRepository;
import com.example.demo.respository.RecruiterRepository;
import com.example.demo.respository.SeekerRepository;
@Service
public class LoginService {
@Autowired
private LoginRepository loginRepository;
@Autowired
private RecruiterRepository recruiterRepository;
@Autowired
private SeekerRepository seekerRepository;
public String register(Login login) {
try {
if (login.getRole().equalsIgnoreCase("recruiter")) {
Recruiter recruiter = new Recruiter();
recruiter.setEmail(login.getEmail());
recruiterRepository.save(recruiter);
} else {
Seeker seeker = new Seeker();
seeker.setEmail(login.getEmail());
seekerRepository.save(seeker);
}
loginRepository.save(login);
return "Successfully Registered";
} catch (Exception e) {
System.out.println(e.toString());
}
return "Registered Failed";
}
}
RecruiterService.java
package com.example.demo.service;
public class RecruiterService {
}
SeekerService.java
package com.example.demo.service;
public class SeekerService {
}
Frontend Templates & JS
login.js
$(document).ready(function() {
$("#register").click(function() {
var email = $("#email").val();
var pwd = $("#pwd").val();
var role = $("#role").val();
if (email == "") {
alert("Please Enter Email");
$("#email").focus();
} else if (pwd == "") {
alert("Please Enter Password");
$("#pwd").focus();
} else if (role == "Select Role") {
alert("Please Select Role");
$("#role").focus();
} else {
$.post("register", {
email: email,
password: pwd,
role: role
}, function(response) {
alert(response);
});
}
});
});
add.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<span th:insert="head.html"></span>
error.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<span th:insert="head.html"></span>
<div class="container">
<div align="center">
<div>
<button onclick="location.href='/' ">GO TO HOME PAGE</button>
</div>
<br>
<div>
<img src="https://mir-s3-cdn-cf.behance.net/project_modules/fs/6b443b70759853.5bade2d0a6685.png"
width="80%" height="500px">
</div>
</div>
</div>
head.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<title>Online Job Portal</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
<style>
.navbar {
border-radius: 0px !important;
}
</style>
</head>
<nav class="navbar navbar-inverse" style="background-color: maroon;">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle"
data-toggle="collapse" data-target="#myNavbar">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">JOB PORTAL</a>
</div>
<div class="collapse navbar-collapse" id="myNavbar">
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
</ul>
<div sec:authorize="isAuthenticated()">
<ul class="nav navbar-nav navbar-right">
<li><a>Add Job</a></li>
<li><a>Job List</a></li>
<li><a>Profile</a></li>
<li>
<form action="/logout" method="post"
style="margin-top: 5px;">
<input type="submit" value="Logout" class="btn btn-link">
<input type="hidden"
th:name="${_csrf.parameterName}"
th:value="${_csrf.token}" />
</form>
</li>
</ul>
</div>
</div>
</div>
</nav>
application.properties
server.port=9999
debug=true
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.url=jdbc:mysql://localhost:3306/jobportal
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
Note: In modern Spring Boot (2.4+), the MySQL driver class name is auto-detected. If you must specify it explicitly, use com.mysql.cj.jdbc.Driver (not the old com.mysql.jdbc.Driver).
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Output Screens
Below are the output screenshots of the running application:
How It Works (Step-by-Step)
-
User submits the login form — the browser POSTs
username(email) andpasswordto/login. -
Spring Security calls CustomLoginDetailService —
loadUserByUsername(email)queries theloginstable viaLoginRepository. -
Password is verified — BCrypt matches the submitted password
against the stored hash using
BCryptPasswordEncoder. -
CustomLoginDetail returns authorities — the user's role
(Recruiter or Seeker) is wrapped as a
SimpleGrantedAuthority. -
AuthenticationSuccessHandler redirects — the handler reads the
role from the
Loginentity and sends the user to/recruiteror/seeker. -
Authorization check on each request —
hasAuthority("Recruiter")on/recruiter/**andhasAuthority("Seeker")on/seeker/**enforce role-based access.
Frequently Asked Questions
How does role-based login work in Spring Boot?
Spring Security authenticates the user against a CustomUserDetailsService,
which loads the user and their role from the database. On successful login, an
AuthenticationSuccessHandler reads the role and redirects the user to the
appropriate page (e.g., /recruiter or /seeker). Access to each
URL is restricted using hasAuthority() in the security configuration.
What is CustomUserDetailsService in Spring Security?
CustomUserDetailsService is a class that implements Spring Security's
UserDetailsService interface. It overrides
loadUserByUsername() to fetch the user from the database and return a
UserDetails implementation that Spring Security uses for authentication and
authorization.
Why should I use BCryptPasswordEncoder for passwords?
BCryptPasswordEncoder hashes passwords with a salted, adaptive algorithm.
Storing plain-text passwords is a critical security flaw. BCrypt protects against
rainbow-table and brute-force attacks, and Spring Security integrates with it out of the
box.
How do I redirect users to different pages after login based on their role?
Set a custom AuthenticationSuccessHandler on the formLogin()
configuration. Inside onAuthenticationSuccess(), read the user's role from
the repository (using the submitted username) and call
response.sendRedirect() with a URL based on that role.
What is the difference between hasRole() and hasAuthority() in Spring Security?
hasRole('ADMIN') implicitly prepends ROLE_ to the name and
checks for ROLE_ADMIN. hasAuthority('ADMIN') checks for the
exact string ADMIN with no prefix. If your database stores roles as
Recruiter or Seeker without a prefix, use
hasAuthority() instead of hasRole().
Conclusion
In this tutorial, you built a complete role-based login system in Spring
Boot using Spring Security, CustomUserDetailsService, BCrypt
password encoding, and a custom AuthenticationSuccessHandler. Users
register with a role, log in securely, and are redirected to the correct dashboard
automatically.
You can extend this project with role-specific CRUD operations for jobs, applications,
and profiles, or migrate to newer Spring Security APIs (Spring Boot 3+ uses
SecurityFilterChain instead of WebSecurityConfigurerAdapter).
Related topics: Spring Security JWT Authentication, Spring Boot 3 SecurityFilterChain, OAuth2 Login with Spring Boot, Thymeleaf Security Dialect, Role vs Authority in Spring Security
No comments:
Post a Comment