Role Based Login in Spring Boot

Role-Based Login in Spring Boot with Spring Security (2026 Guide)

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 /recruiter or /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)

  1. User submits the login form — the browser POSTs username (email) and password to /login.
  2. Spring Security calls CustomLoginDetailService — loadUserByUsername(email) queries the logins table via LoginRepository.
  3. Password is verified — BCrypt matches the submitted password against the stored hash using BCryptPasswordEncoder.
  4. CustomLoginDetail returns authorities — the user's role (Recruiter or Seeker) is wrapped as a SimpleGrantedAuthority.
  5. AuthenticationSuccessHandler redirects — the handler reads the role from the Login entity and sends the user to /recruiter or /seeker.
  6. Authorization check on each request — hasAuthority("Recruiter") on /recruiter/** and hasAuthority("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: