A comprehensive guide for implementing Spring Security in Spring Boot applications with authentication, authorization, custom login forms, database integration, and password encryption.
- Overview
- Security Concepts
- Features
- Technology Stack
- Project Structure
- Setup and Configuration
- Implementation Guide
- Database Integration
- Password Encryption
- Custom Tables
- Testing
- Troubleshooting
This guide covers implementing Spring Security to secure Spring MVC web applications with:
- User authentication and authorization
- Custom login/logout pages
- Role-based access control
- Database storage for users and roles
- Password encryption using BCrypt
- Custom access denied pages
- Dynamic content display based on roles
Home Page (/) → EMPLOYEE role required
Leadership Page (/leaders) → MANAGER role required
Systems Page (/systems) → ADMIN role required
- Definition: Verifying user identity (user ID and password)
- Process: Check credentials against stored values in app/database
- Definition: Determining if authenticated user has permission
- Process: Check if user has authorized role for requested resource
1. Declarative Security
- Define security constraints in configuration
- Uses Java config with
@Configuration - Separates security from application code
2. Programmatic Security
- Custom application coding using Spring Security API
- Greater customization for specific requirements
- Multiple Login Options: HTTP Basic, Default form, Custom form
- Role-Based Access Control: Restrict URLs by user roles
- Dynamic Content: Show/hide content based on roles
- Database Integration: Store users/passwords/roles in database
- Password Encryption: BCrypt hashing for secure storage
- Custom Error Pages: Custom access denied pages
- Session Management: Logout functionality with session cleanup
- CSRF Protection: Built-in Cross-Site Request Forgery protection
- Spring Boot 3.x
- Spring Security 6.x
- Spring MVC
- Thymeleaf (Template Engine)
- Thymeleaf Spring Security (Integration)
- MySQL/PostgreSQL (Database)
- BCrypt (Password Encryption)
- Bootstrap 5 (Optional - UI styling)
spring-security-demo/
│
├── src/main/java/
│ └── com/myapp/springboot/demosecurity/
│ ├── controller/
│ │ ├── DemoController.java
│ │ └── LoginController.java
│ ├── security/
│ │ └── DemoSecurityConfig.java
│ └── DemoSecurityApplication.java
│
├── src/main/resources/
│ ├── templates/
│ │ ├── home.html
│ │ ├── plain-login.html
│ │ ├── fancy-login.html
│ │ ├── leaders.html
│ │ ├── systems.html
│ │ └── access-denied.html
│ ├── static/
│ │ └── css/
│ ├── application.properties
│ └── sql/
│ ├── setup-spring-security-demo-database-plaintext.sql
│ └── setup-spring-security-demo-database-bcrypt.sql
│
└── pom.xml
Add to 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>3.2.0</version>
</parent>
<groupId>com.myapp</groupId>
<artifactId>spring-security-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<!-- Spring MVC Web Support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Thymeleaf Template Engine -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Thymeleaf extras for Spring Security -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
<!-- MySQL Driver (or PostgreSQL) -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Spring Boot DevTools (Optional) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>Create src/main/resources/application.properties:
# Server Configuration
server.port=8080
# Database Configuration
spring.datasource.url=jdbc:mysql://localhost:3306/employee_directory
spring.datasource.username=springstudent
spring.datasource.password=springstudent
# JPA/Hibernate Properties
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
# Thymeleaf Configuration
spring.thymeleaf.cache=false
# Logging
logging.level.org.springframework.security=DEBUGCreate src/main/java/com/myapp/springboot/demosecurity/security/DemoSecurityConfig.java:
package com.myapp.springboot.demosecurity.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class DemoSecurityConfig {
/**
* In-Memory User Authentication
* Good for getting started, testing, demos
*/
@Bean
public InMemoryUserDetailsManager userDetailsManager() {
UserDetails john = User.builder()
.username("john")
.password("{noop}test123") // {noop} = plain text password
.roles("EMPLOYEE")
.build();
UserDetails mary = User.builder()
.username("mary")
.password("{noop}test123")
.roles("EMPLOYEE", "MANAGER")
.build();
UserDetails susan = User.builder()
.username("susan")
.password("{noop}test123")
.roles("EMPLOYEE", "MANAGER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(john, mary, susan);
}
}Password Storage Format:
{id}encodedPassword
Examples:
{noop}test123 → Plain text password
{bcrypt}$2a$10$... → BCrypt encrypted password
Create src/main/java/com/myapp/springboot/demosecurity/controller/DemoController.java:
package com.myapp.springboot.demosecurity.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class DemoController {
@GetMapping("/")
public String showHome() {
return "home";
}
// Add request mapping for /leaders
@GetMapping("/leaders")
public String showLeaders() {
return "leaders";
}
// Add request mapping for /systems
@GetMapping("/systems")
public String showSystems() {
return "systems";
}
}Create src/main/resources/templates/home.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
<head>
<meta charset="UTF-8">
<title>myapp Company Home Page</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body>
<div class="container">
<h2>myapp Company Home Page</h2>
<hr>
<!-- Display user name and role -->
<p>
Welcome: <span sec:authentication="principal.username"></span>
<br><br>
Role(s): <span sec:authentication="principal.authorities"></span>
</p>
<hr>
<!-- Add a link to point to /leaders ... this is for the managers -->
<div sec:authorize="hasRole('MANAGER')">
<p>
<a th:href="@{/leaders}">Leadership Meeting</a>
(Only for Manager peeps)
</p>
</div>
<!-- Add a link to point to /systems ... this is for the admins -->
<div sec:authorize="hasRole('ADMIN')">
<p>
<a th:href="@{/systems}">IT Systems Meeting</a>
(Only for Admin peeps)
</p>
</div>
<hr>
<!-- Add a logout button -->
<form action="#" th:action="@{/logout}" method="POST">
<input type="submit" value="Logout" class="btn btn-outline-danger"/>
</form>
</div>
</body>
</html>Key Thymeleaf Security Features:
| Expression | Description |
|---|---|
sec:authentication="principal.username" |
Display logged-in username |
sec:authentication="principal.authorities" |
Display user roles |
sec:authorize="hasRole('MANAGER')" |
Show content only if user has MANAGER role |
sec:authorize="hasAnyRole('ADMIN','MANAGER')" |
Show content if user has any listed role |
# Run application
mvn spring-boot:run
# Access in browser
http://localhost:8080
# Test with credentials:
Username: john Password: test123 (EMPLOYEE)
Username: mary Password: test123 (EMPLOYEE, MANAGER)
Username: susan Password: test123 (EMPLOYEE, MANAGER, ADMIN)Update DemoSecurityConfig.java to add custom login configuration:
package com.myapp.springboot.demosecurity.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class DemoSecurityConfig {
@Bean
public InMemoryUserDetailsManager userDetailsManager() {
// ... (same as before)
}
/**
* Security Filter Chain Configuration
* Configure security: login, logout, access control
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(configurer ->
configurer
.anyRequest().authenticated() // Any request requires authentication
)
.formLogin(form ->
form
.loginPage("/showMyLoginPage") // Custom login page
.loginProcessingUrl("/authenticateTheUser") // Submit URL
.permitAll() // Allow everyone to see login
)
.logout(logout ->
logout.permitAll() // Allow everyone to logout
);
return http.build();
}
}Configuration Breakdown:
// Any request must be authenticated (logged in)
.anyRequest().authenticated()
// Custom form login
.formLogin(form -> form
.loginPage("/showMyLoginPage") // Show this page for login
.loginProcessingUrl("/authenticateTheUser") // POST data here (Spring handles it)
.permitAll() // Everyone can see login page
)
// Logout support
.logout(logout -> logout.permitAll()) // Default URL: /logoutCreate src/main/java/com/myapp/springboot/demosecurity/controller/LoginController.java:
package com.myapp.springboot.demosecurity.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class LoginController {
@GetMapping("/showMyLoginPage")
public String showMyLoginPage() {
return "plain-login";
}
}Create src/main/resources/templates/plain-login.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Custom Login Page</title>
<style>
.failed {
color: red;
}
.logout {
color: green;
}
</style>
</head>
<body>
<h3>My Custom Login Page</h3>
<!-- Login Form -->
<form action="#" th:action="@{/authenticateTheUser}" method="POST">
<!-- Check for login error -->
<div th:if="${param.error}">
<i class="failed">Sorry! You entered invalid username/password.</i>
</div>
<!-- Check for logout -->
<div th:if="${param.logout}">
<i class="logout">You have been logged out.</i>
</div>
<p>
User name: <input type="text" name="username" />
</p>
<p>
Password: <input type="password" name="password" />
</p>
<input type="submit" value="Login" />
</form>
</body>
</html>Important Form Fields:
- Action URL:
/authenticateTheUser(Spring Security processes this) - Method: Must be
POST - Username field: Must be named
username - Password field: Must be named
password - Error parameter:
?errorappended on failed login - Logout parameter:
?logoutappended after logout
Create src/main/resources/templates/fancy-login.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Custom Login Page</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-container {
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
padding: 40px;
max-width: 400px;
width: 100%;
}
.login-title {
text-align: center;
margin-bottom: 30px;
color: #667eea;
font-weight: bold;
}
.btn-login {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 8px;
color: white;
font-weight: bold;
transition: transform 0.2s;
}
.btn-login:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
</style>
</head>
<body>
<div class="login-container">
<h2 class="login-title">Login Portal</h2>
<!-- Login Form -->
<form action="#" th:action="@{/authenticateTheUser}" method="POST">
<!-- Login Error Alert -->
<div th:if="${param.error}" class="alert alert-danger" role="alert">
<i class="bi bi-exclamation-triangle-fill"></i>
Invalid username or password. Please try again.
</div>
<!-- Logout Success Alert -->
<div th:if="${param.logout}" class="alert alert-success" role="alert">
<i class="bi bi-check-circle-fill"></i>
You have been successfully logged out.
</div>
<!-- Username Field -->
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text"
class="form-control"
id="username"
name="username"
placeholder="Enter your username"
required
autofocus>
</div>
<!-- Password Field -->
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password"
class="form-control"
id="password"
name="password"
placeholder="Enter your password"
required>
</div>
<!-- Submit Button -->
<button type="submit" class="btn btn-login">
Sign In
</button>
</form>
<div class="text-center mt-3">
<small class="text-muted">© 2024 myapp Company</small>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>To use fancy login, update LoginController.java:
@GetMapping("/showMyLoginPage")
public String showMyLoginPage() {
return "fancy-login"; // Changed from "plain-login"
}Update DemoSecurityConfig.java:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(configurer ->
configurer
.requestMatchers("/").hasRole("EMPLOYEE")
.requestMatchers("/leaders/**").hasRole("MANAGER")
.requestMatchers("/systems/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form ->
form
.loginPage("/showMyLoginPage")
.loginProcessingUrl("/authenticateTheUser")
.permitAll()
)
.logout(logout ->
logout.permitAll()
)
.exceptionHandling(configurer ->
configurer
.accessDeniedPage("/access-denied")
);
return http.build();
}Access Control Breakdown:
// Root path - requires EMPLOYEE role
.requestMatchers("/").hasRole("EMPLOYEE")
// Leadership pages - requires MANAGER role
.requestMatchers("/leaders/**").hasRole("MANAGER")
// Systems pages - requires ADMIN role
.requestMatchers("/systems/**").hasRole("ADMIN")
// Multiple roles (alternative syntax)
.requestMatchers("/hr/**").hasAnyRole("MANAGER", "ADMIN")
// Custom access denied page
.exceptionHandling(configurer ->
configurer.accessDeniedPage("/access-denied")
)Leaders Page - src/main/resources/templates/leaders.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Leaders Home Page</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body>
<div class="container">
<h2>Leaders Home Page</h2>
<hr>
<p>
Welcome to the LEADERS home page!
<br><br>
This page is restricted to MANAGER role only.
</p>
<hr>
<a th:href="@{/}">Back to Home Page</a>
</div>
</body>
</html>Systems Page - src/main/resources/templates/systems.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Systems Home Page</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body>
<div class="container">
<h2>Systems Home Page</h2>
<hr>
<p>
Welcome to the SYSTEMS home page!
<br><br>
This page is restricted to ADMIN role only.
</p>
<hr>
<a th:href="@{/}">Back to Home Page</a>
</div>
</body>
</html>Create src/main/resources/templates/access-denied.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Access Denied</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet">
<style>
body {
background: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.access-denied-container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
text-align: center;
max-width: 500px;
}
.error-code {
font-size: 72px;
font-weight: bold;
color: #dc3545;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="access-denied-container">
<div class="error-code">403</div>
<h2 class="text-danger mb-4">Access Denied</h2>
<p class="lead mb-4">
You are not authorized to access this resource.
</p>
<p class="text-muted mb-4">
If you believe this is an error, please contact your system administrator.
</p>
<a th:href="@{/}" class="btn btn-primary">
Return to Home Page
</a>
</div>
</body>
</html>Update DemoController.java:
@GetMapping("/access-denied")
public String showAccessDenied() {
return "access-denied";
}Create SQL script src/main/resources/sql/setup-spring-security-demo-database-plaintext.sql:
-- Drop existing database if needed
DROP DATABASE IF EXISTS `employee_directory`;
-- Create database
CREATE DATABASE IF NOT EXISTS `employee_directory`;
USE `employee_directory`;
--
-- Table structure for table `users`
-- DEFAULT SPRING SECURITY SCHEMA
--
CREATE TABLE `users` (
`username` VARCHAR(50) NOT NULL,
`password` VARCHAR(50) NOT NULL,
`enabled` TINYINT NOT NULL,
PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Insert sample data into `users`
-- NOTE: The passwords are stored in plain-text using {noop}
--
INSERT INTO `users` VALUES
('john','{noop}test123',1),
('mary','{noop}test123',1),
('susan','{noop}test123',1);
--
-- Table structure for table `authorities`
-- DEFAULT SPRING SECURITY SCHEMA
-- NOTE: "authorities" is same as "roles"
--
CREATE TABLE `authorities` (
`username` VARCHAR(50) NOT NULL,
`authority` VARCHAR(50) NOT NULL,
UNIQUE KEY `authorities_idx_1` (`username`,`authority`),
CONSTRAINT `authorities_ibfk_1`
FOREIGN KEY (`username`)
REFERENCES `users` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Insert sample data into `authorities`
-- NOTE: Spring Security uses ROLE_ prefix internally
--
INSERT INTO `authorities` VALUES
('john','ROLE_EMPLOYEE'),
('mary','ROLE_EMPLOYEE'),
('mary','ROLE_MANAGER'),
('susan','ROLE_EMPLOYEE'),
('susan','ROLE_MANAGER'),
('susan','ROLE_ADMIN');Run the SQL script:
# Connect to MySQL
mysql -u root -p
# Run the script
source /path/to/setup-spring-security-demo-database-plaintext.sql
# Verify tables
USE employee_directory;
SHOW TABLES;
SELECT * FROM users;
SELECT * FROM authorities;Update src/main/resources/application.properties:
# JDBC connection properties
spring.datasource.url=jdbc:mysql://localhost:3306/employee_directory
spring.datasource.username=springstudent
spring.datasource.password=springstudent
# JPA/Hibernate properties
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=trueUpdate DemoSecurityConfig.java:
package com.myapp.springboot.demosecurity.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import javax.sql.DataSource;
@Configuration
public class DemoSecurityConfig {
/**
* JDBC Authentication
* Uses default Spring Security schema (users, authorities tables)
* DataSource is auto-configured by Spring Boot
*/
@Bean
public UserDetailsManager userDetailsManager(DataSource dataSource) {
return new JdbcUserDetailsManager(dataSource);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// ... (same as before)
}
}Key Points:
DataSourceis automatically injected by Spring Boot- Uses default Spring Security table schema
- No need to hardcode users anymore
- Passwords stored in database
# Run application
mvn spring-boot:run
# Test with database credentials
Username: john Password: test123
Username: mary Password: test123
Username: susan Password: test123Option 1: Online Tool
Visit: https://www.luv2code.com/generate-bcrypt-password
Example inputs and outputs:
Plain Text: test123
BCrypt: $2a$10$qeS0HEh7urweMojsnwNAR.vcXJeXR1UcMRZ2WcGQl9YeuspUdgF.q
Plain Text: fun123
BCrypt: $2a$04$eFytJDGtjbThXa80FyOOBuFdK2IwjyWefYkMpiBEFlpBwDH.5PM0K
Option 2: Java Code
Create a test class to generate passwords:
package com.myapp.springboot.demosecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class BCryptPasswordGenerator {
public static void main(String[] args) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String plainTextPassword = "test123";
String encodedPassword = passwordEncoder.encode(plainTextPassword);
System.out.println("Plain text password: " + plainTextPassword);
System.out.println("Encoded password: " + encodedPassword);
// Generate multiple passwords
String[] passwords = {"test123", "fun123", "admin123"};
for (String password : passwords) {
String encoded = passwordEncoder.encode(passwor
System.out.println("\nPassword: " + password);
System.out.println("Encoded: " + encoded);
}
}
}Run the generator:
mvn exec:java -Dexec.mainClass="com.myapp.springboot.demosecurity.BCryptPasswordGenerator"Create SQL script src/main/resources/sql/setup-spring-security-demo-database-bcrypt.sql:
-- Drop existing database if needed
DROP DATABASE IF EXISTS `employee_directory`;
-- Create database
CREATE DATABASE IF NOT EXISTS `employee_directory`;
USE `employee_directory`;
--
-- Table structure for table `users`
-- Password column must be at least 68 characters
-- {bcrypt} = 8 chars, encoded password = 60 chars
--
CREATE TABLE `users` (
`username` VARCHAR(50) NOT NULL,
`password` CHAR(68) NOT NULL,
`enabled` TINYINT NOT NULL,
PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Insert sample data into `users`
-- Passwords are encrypted using BCrypt
-- Default password for all users: fun123
--
INSERT INTO `users` VALUES
('john','{bcrypt}$2a$10$qeS0HEh7urweMojsnwNAR.vcXJeXR1UcMRZ2WcGQl9YeuspUdgF.q',1),
('mary','{bcrypt}$2a$10$qeS0HEh7urweMojsnwNAR.vcXJeXR1UcMRZ2WcGQl9YeuspUdgF.q',1),
('susan','{bcrypt}$2a$10$qeS0HEh7urweMojsnwNAR.vcXJeXR1UcMRZ2WcGQl9YeuspUdgF.q',1);
--
-- Table structure for table `authorities`
--
CREATE TABLE `authorities` (
`username` VARCHAR(50) NOT NULL,
`authority` VARCHAR(50) NOT NULL,
UNIQUE KEY `authorities_idx_1` (`username`,`authority`),
CONSTRAINT `authorities_ibfk_1`
FOREIGN KEY (`username`)
REFERENCES `users` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Insert sample data into `authorities`
--
INSERT INTO `authorities` VALUES
('john','ROLE_EMPLOYEE'),
('mary','ROLE_EMPLOYEE'),
('mary','ROLE_MANAGER'),
('susan','ROLE_EMPLOYEE'),
('susan','ROLE_MANAGER'),
('susan','ROLE_ADMIN');Password Format Breakdown:
{bcrypt}$2a$10$qeS0HEh7urweMojsnwNAR.vcXJeXR1UcMRZ2WcGQl9YeuspUdgF.q
│ │ │
│ └─── Encrypted password (60 characters) │
│ │
└─── Algorithm ID (8 characters including braces) │
│
Total: 68 characters minimum │
# Connect to MySQL
mysql -u root -p
# Run the BCrypt script
source /path/to/setup-spring-security-demo-database-bcrypt.sql
# Verify data
USE employee_directory;
SELECT * FROM users;
SELECT * FROM authorities;No code changes needed! Spring Security automatically handles BCrypt passwords.
# Run application
mvn spring-boot:run
# Test with BCrypt encrypted passwords
Username: john Password: fun123
Username: mary Password: fun123
Username: susan Password: fun1231. User enters plaintext password in login form
↓
2. Spring Security retrieves encrypted password from database
↓
3. Reads algorithm ID ({bcrypt})
↓
4. Encrypts plaintext password using BCrypt with salt from DB password
↓
5. Compares encrypted login password WITH encrypted DB password
↓
6. If match: Login successful
If no match: Login failed
NOTE: Database password is NEVER decrypted
BCrypt is a one-way encryption algorithm
Create SQL script src/main/resources/sql/setup-spring-security-custom-tables.sql:
-- Drop existing tables if needed
DROP TABLE IF EXISTS `roles`;
DROP TABLE IF EXISTS `members`;
-- Drop database and recreate
DROP DATABASE IF EXISTS `employee_directory`;
CREATE DATABASE IF NOT EXISTS `employee_directory`;
USE `employee_directory`;
--
-- Custom table for users (called 'members')
--
CREATE TABLE `members` (
`user_id` VARCHAR(50) NOT NULL,
`pw` CHAR(68) NOT NULL,
`active` TINYINT NOT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Insert sample data into `members`
-- Default password for all: fun123
--
INSERT INTO `members` VALUES
('john','{bcrypt}$2a$10$qeS0HEh7urweMojsnwNAR.vcXJeXR1UcMRZ2WcGQl9YeuspUdgF.q',1),
('mary','{bcrypt}$2a$10$qeS0HEh7urweMojsnwNAR.vcXJeXR1UcMRZ2WcGQl9YeuspUdgF.q',1),
('susan','{bcrypt}$2a$10$qeS0HEh7urweMojsnwNAR.vcXJeXR1UcMRZ2WcGQl9YeuspUdgF.q',1);
--
-- Custom table for roles
--
CREATE TABLE `roles` (
`user_id` VARCHAR(50) NOT NULL,
`role` VARCHAR(50) NOT NULL,
UNIQUE KEY `authorities_idx_1` (`user_id`,`role`),
CONSTRAINT `authorities_ibfk_1`
FOREIGN KEY (`user_id`)
REFERENCES `members` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Insert sample data into `roles`
--
INSERT INTO `roles` VALUES
('john','ROLE_EMPLOYEE'),
('mary','ROLE_EMPLOYEE'),
('mary','ROLE_MANAGER'),
('susan','ROLE_EMPLOYEE'),
('susan','ROLE_MANAGER'),
('susan','ROLE_ADMIN');Custom Schema Details:
| Default Table | Custom Table | Default Column | Custom Column |
|---|---|---|---|
| users | members | username | user_id |
| users | members | password | pw |
| users | members | enabled | active |
| authorities | roles | username | user_id |
| authorities | roles | authority | role |
Update DemoSecurityConfig.java:
package com.myapp.springboot.demosecurity.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import javax.sql.DataSource;
@Configuration
public class DemoSecurityConfig {
/**
* JDBC Authentication with Custom Tables
* Tell Spring Security how to query custom tables
*/
@Bean
public UserDetailsManager userDetailsManager(DataSource dataSource) {
JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager(dataSource);
// Define query to retrieve a user by username
jdbcUserDetailsManager.setUsersByUsernameQuery(
"SELECT user_id, pw, active FROM members WHERE user_id=?"
);
// Define query to retrieve the authorities/roles by username
jdbcUserDetailsManager.setAuthoritiesByUsernameQuery(
"SELECT user_id, role FROM roles WHERE user_id=?"
);
return jdbcUserDetailsManager;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(configurer ->
configurer
.requestMatchers("/").hasRole("EMPLOYEE")
.requestMatchers("/leaders/**").hasRole("MANAGER")
.requestMatchers("/systems/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form ->
form
.loginPage("/showMyLoginPage")
.loginProcessingUrl("/authenticateTheUser")
.permitAll()
)
.logout(logout ->
logout.permitAll()
)
.exceptionHandling(configurer ->
configurer
.accessDeniedPage("/access-denied")
);
return http.build();
}
}Query Requirements:
User Query Must Return:
- Username
- Password
- Enabled flag (boolean)
SELECT user_id, pw, active FROM members WHERE user_id=?Authorities Query Must Return:
- Username
- Role/Authority
SELECT user_id, role FROM roles WHERE user_id=?The ? (question mark):
- Parameter placeholder
- Spring Security will substitute the username from login form
- Example: If user enters "john", query becomes:
SELECT user_id, pw, active FROM members WHERE user_id='john'
# Connect to MySQL
mysql -u root -p
# Run the custom tables script
source /path/to/setup-spring-security-custom-tables.sql
# Verify tables
USE employee_directory;
SHOW TABLES;
SELECT * FROM members;
SELECT * FROM roles;# Run application
mvn spring-boot:run
# Test with custom table credentials
Username: john Password: fun123
Username: mary Password: fun123
Username: susan Password: fun123# Start application
mvn spring-boot:run
# Test User 1: EMPLOYEE only
URL: http://localhost:8080
Username: john
Password: fun123
Expected Results:
✓ Can access home page (/)
✗ Cannot access /leaders (403 error)
✗ Cannot access /systems (403 error)
✓ Only sees home page content
✓ Does not see Manager or Admin links# Test User 2: EMPLOYEE + MANAGER
Username: mary
Password: fun123
Expected Results:
✓ Can access home page (/)
✓ Can access /leaders
✗ Cannot access /systems (403 error)
✓ Sees Manager link on home page
✗ Does not see Admin link# Test User 3: EMPLOYEE + MANAGER + ADMIN
Username: susan
Password: fun123
Expected Results:
✓ Can access home page (/)
✓ Can access /leaders
✓ Can access /systems
✓ Sees both Manager and Admin links
✓ Has full access to all pages# Test invalid credentials
Username: invalid
Password: wrong
Expected Results:
✓ Returns to login page
✓ Shows error message: "Sorry! You entered invalid username/password."
✓ URL contains ?error parameter# Login as any user
# Click "Logout" button
Expected Results:
✓ Session cleared
✓ Redirects to login page
✓ Shows message: "You have been logged out."
✓ URL contains ?logout parameter
✓ Cannot access protected pages without re-login# Login as john (EMPLOYEE only)
# Manually navigate to: http://localhost:8080/leaders
Expected Results:
✓ Redirected to /access-denied page
✓ Shows 403 error page
✓ Shows "Access Denied" message
✓ Provides link back to home page# Verify database authentication works
mysql -u springstudent -p
USE employee_directory;
# Check users
SELECT * FROM users;
# or for custom tables:
SELECT * FROM members;
# Check roles
SELECT * FROM authorities;
# or for custom tables:
SELECT * FROM roles;
# Test login with database credentials# Verify BCrypt passwords work
# Login with BCrypt encrypted passwords
Expected Results:
✓ Can login with plaintext password (fun123)
✓ Password in database is encrypted
✓ Spring Security decrypts and validates automatically
✓ Login successful if password matches# Get login page and extract CSRF token
curl -c cookies.txt http://localhost:8080/showMyLoginPage
# Login with credentials
curl -b cookies.txt -c cookies.txt \
-d "username=john&password=fun123" \
-X POST http://localhost:8080/authenticateTheUser
# Access protected resource
curl -b cookies.txt http://localhost:8080/# Logout
curl -b cookies.txt -X POST http://localhost:8080/logoutAuthentication Tests:
□ Valid credentials allow login
□ Invalid credentials show error message
□ Error message displays correctly
□ Logout clears session
□ Logout message displays correctly
Authorization Tests:
□ EMPLOYEE can access home page
□ EMPLOYEE cannot access /leaders
□ EMPLOYEE cannot access /systems
□ MANAGER can access home and /leaders
□ MANAGER cannot access /systems
□ ADMIN can access all pages
UI Tests:
□ Custom login form displays correctly
□ Error messages styled properly
□ Success messages styled properly
□ Links hidden/shown based on role
□ User name displays correctly
□ User roles display correctly
□ Access denied page displays correctly
Database Tests:
□ Can connect to database
□ Users retrieved from database
□ Roles retrieved from database
□ BCrypt passwords work correctly
□ Custom tables query correctly
Symptoms:
Always redirected back to login with error
Valid credentials don't work
Solutions:
# Check 1: Verify database connection
mysql -u springstudent -p
USE employee_directory;
SELECT * FROM users;
# Check 2: Verify passwords in database
# For BCrypt, password should start with {bcrypt}
# For plaintext, password should start with {noop}
# Check 3: Check application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/employee_directory
spring.datasource.username=springstudent
spring.datasource.password=springstudent
# Check 4: Enable debug logging
logging.level.org.springframework.security=DEBUG
# Check 5: Verify ROLE_ prefix in database
# Authorities table should have: ROLE_EMPLOYEE, not just EMPLOYEE
SELECT * FROM authorities;Symptoms:
Cannot access login page
403 Forbidden error
Solutions:
// Ensure login page permits all users
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(configurer ->
configurer
.requestMatchers("/showMyLoginPage").permitAll() // Add this
.anyRequest().authenticated()
)
.formLogin(form ->
form
.loginPage("/showMyLoginPage")
.loginProcessingUrl("/authenticateTheUser")
.permitAll() // Ensure this is present
);
return http.build();
}Symptoms:
Could not verify the provided CSRF token
403 Forbidden on form submission
Solutions:
<!-- Ensure Thymeleaf form uses th:action -->
<form action="#" th:action="@{/authenticateTheUser}" method="POST">
<!-- Thymeleaf automatically adds CSRF token -->
</form>
<!-- If using plain HTML, manually add CSRF token -->
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>// Or disable CSRF for testing (NOT recommended for production)
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // Only for testing!
// ... rest of config
return http.build();
}Symptoms:
Data too long for column 'password'
SQL error when inserting users
Solutions:
-- For BCrypt, password column must be at least 68 characters
ALTER TABLE users MODIFY password CHAR(68) NOT NULL;
-- Verify column length
DESCRIBE users;
-- Should show:
-- password | char(68) | NO | | NULL |Symptoms:
Bad SQL grammar exception
PreparedStatementCallback error
Solutions:
// Check query syntax exactly
@Bean
public UserDetailsManager userDetailsManager(DataSource dataSource) {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
// Query must return EXACTLY 3 columns in this order:
// 1. username, 2. password, 3. enabled
manager.setUsersByUsernameQuery(
"SELECT user_id, pw, active FROM members WHERE user_id=?"
);
// Query must return EXACTLY 2 columns in this order:
// 1. username, 2. role
manager.setAuthoritiesByUsernameQuery(
"SELECT user_id, role FROM roles WHERE user_id=?"
);
return manager;
}# Test queries in MySQL directly
mysql -u springstudent -p
USE employee_directory;
# Test user query
SELECT user_id, pw, active FROM members WHERE user_id='john';
# Test authorities query
SELECT user_id, role FROM roles WHERE user_id='john';
# Both should return resultsSymptoms:
Error resolving template "home"
Template might not exist
Solutions:
# Verify file location
src/main/resources/templates/home.html
# Check controller return value matches filename
@GetMapping("/")
public String showHome() {
return "home"; // Must match: templates/home.html
}
# For subdirectories
return "employees/list"; // Must match: templates/employees/list.htmlSymptoms:
sec:authentication not showing roles
Empty authorities display
Solutions:
<!-- Verify namespace declaration -->
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
<!-- Check POM dependency -->
<!-- pom.xml should have: -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
<!-- Verify expression syntax -->
<span sec:authentication="principal.authorities"></span>Symptoms:
Communications link failure
Unable to connect to database
Solutions:
# Check MySQL is running
sudo systemctl status mysql
# or
brew services list | grep mysql
# Test connection
mysql -h localhost -u springstudent -p
# Verify database exists
SHOW DATABASES;
# Check firewall/port
netstat -an | grep 3306
# Update application.properties with correct URL
spring.datasource.url=jdbc:mysql://localhost:3306/employee_directory?useSSL=false&serverTimezone=UTCSymptoms:
404 Not Found for login
Links not working correctly
Solutions:
# If using custom context path
server.servlet.context-path=/myapp
# All URLs become: http://localhost:8080/myapp/...<!-- Always use Thymeleaf @ symbol for links -->
<a th:href="@{/leaders}">Leaders</a>
<!-- Becomes: /myapp/leaders automatically -->
<!-- DON'T hardcode paths -->
<a href="/leaders">Leaders</a> ❌ Wrong!Problem: Browser caches session, making testing difficult
Solutions:
# Option 1: Use Incognito/Private Window
# Chrome: Ctrl+Shift+N
# Firefox: Ctrl+Shift+P
# Option 2: Clear cookies
# Chrome DevTools: F12 → Application → Cookies → Clear
# Option 3: Use different browser
# Option 4: Use cURL with fresh cookies each time
curl -c /dev/null -b /dev/null http://localhost:8080Add to application.properties:
# Spring Security Debug
logging.level.org.springframework.security=DEBUG
# SQL Debug
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
# Web Debug
logging.level.org.springframework.web=DEBUG
# View all logs
logging.level.root=INFO- Spring Security Reference: https://docs.spring.io/spring-security/reference/
- Spring Boot Security: https://docs.spring.io/spring-boot/docs/current/reference/html/web.html#web.security
- Thymeleaf Spring Security: https://github.com/thymeleaf/thymeleaf-extras-springsecurity
- BCrypt Information: https://www.luv2code.com/why-bcrypt
- BCrypt Generator: https://www.luv2code.com/generate-bcrypt-password
- Password Best Practices: https://www.luv2code.com/password-hashing-best-practices
- Post/Redirect/Get Pattern: https://www.luv2code.com/post-redirect-get
- OAuth 2.0 / OpenID Connect
- LDAP Authentication
- Remember-Me Authentication
- Method-Level Security (@PreAuthorize, @Secured)
- Custom Authentication Providers
- Multi-Factor Authentication
- JWT Token Authentication
- SAML Authentication
@Configuration
public class SecurityConfig {
// In-Memory Users
@Bean
public InMemoryUserDetailsManager userDetailsManager() {
UserDetails user = User.builder()
.username("user")
.password("{noop}password")
.roles("EMPLOYEE")
.build();
return new InMemoryUserDetailsManager(user);
}
// JDBC Users (Default Schema)
@Bean
public UserDetailsManager userDetailsManager(DataSource dataSource) {
return new JdbcUserDetailsManager(dataSource);
}
// JDBC Users (Custom Schema)
@Bean
public UserDetailsManager userDetailsManager(DataSource dataSource) {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
manager.setUsersByUsernameQuery("SELECT user_id, pw, active FROM members WHERE user_id=?");
manager.setAuthoritiesByUsernameQuery("SELECT user_id, role FROM roles WHERE user_id=?");
return manager;
}
// Security Filter Chain
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(config -> config
.requestMatchers("/").hasRole("EMPLOYEE")
.requestMatchers("/leaders/**").hasRole("MANAGER")
.requestMatchers("/systems/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/showMyLoginPage")
.loginProcessingUrl("/authenticateTheUser")
.permitAll()
)
.logout(logout -> logout.permitAll())
.exceptionHandling(config -> config
.accessDeniedPage("/access-denied")
);
return http.build();
}
}<!-- Display username -->
<span sec:authentication="principal.username"></span>
<!-- Display roles -->
<span sec:authentication="principal.authorities"></span>
<!-- Show content for specific role -->
<div sec:authorize="hasRole('MANAGER')">Manager content</div>
<!-- Show content for any of multiple roles -->
<div sec:authorize="hasAnyRole('MANAGER','ADMIN')">Content</div>
<!-- Show content for authenticated users -->
<div sec:authorize="isAuthenticated()">Logged in content</div>
<!-- Show content for anonymous users -->
<div sec:authorize="isAnonymous()">Please login</div>Plaintext: {noop}mypassword
BCrypt: {bcrypt}$2a$10$...
Argon2: {argon2}$argon2id$...
Pbkdf2: {pbkdf2}...
SCrypt: {scrypt}$...
Spring Security automatically includes CSRF tokens in Thymeleaf forms:
<!-- CSRF token automatically added for POST requests -->
<form method="POST" action="/logout">
<!-- Spring Security adds hidden CSRF field automatically -->
<button type="submit">Logout</button>
</form>
<!-- View CSRF token value -->
<form method="POST">
<input type="hidden" name="_csrf" th:value="${_csrf.token}" />
<button type="submit">Save</button>
</form>// Get CSRF token from meta tag
const token = document.querySelector('meta[name="_csrf"]').getAttribute("content");
const header = document.querySelector('meta[name="_csrf_header"]').getAttribute("content");
// Send with AJAX request
fetch('/api/save', {
method: 'POST',
headers: {
[header]: token,
'Content-Type': 'application/json'
},
body: JSON.stringify({firstName: 'John', lastName: 'Doe'})
});<!-- Add to base template -->
<head>
<meta name="_csrf" th:content="${_csrf.token}"/>
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
</head># application.properties
server.servlet.session.timeout=15m
# Customize session cookie
server.servlet.session.cookie.name=JSESSIONID
server.servlet.session.cookie.secure=true
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.same-site=strict@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.rememberMe(remember -> remember
.rememberMeParameter("remember-me")
.tokenValiditySeconds(86400) // 24 hours
.key("rememberMeKey"))
.build();
return http.build();
}
}HTML Form:
<form method="POST" action="/login">
<input type="text" name="username" />
<input type="password" name="password" />
<label>
<input type="checkbox" name="remember-me" /> Remember Me
</label>
<button type="submit">Login</button>
</form>@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.sessionFixationProtection(SessionFixationProtectionStrategy.MIGRATE_SESSION)
.maximumSessions(1) // Only one active session per user
.maxSessionsPreventsLogin(false)
.expiredUrl("/login?expired"))
.build();
return http.build();
}
}<!-- Custom logout form -->
<form th:action="@{/logout}" method="post">
<button type="submit" class="btn btn-link">Logout</button>
</form>
<!-- Or simple link with hidden form -->
<a href="#" onclick="document.getElementById('logoutForm').submit();">Logout</a>
<form id="logoutForm" th:action="@{/logout}" method="post">
</form>@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.invalidateHttpSession(true)
.clearAuthentication(true)
.deleteCookies("JSESSIONID"))
.build();
return http.build();
}
}@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
private static final Logger logger = LoggerFactory.getLogger(CustomLogoutSuccessHandler.class);
@Override
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException, ServletException {
if (authentication != null) {
logger.info("User {} logged out successfully",
authentication.getName());
}
response.sendRedirect("/login?logout");
}
}
// Register in SecurityConfig
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
CustomLogoutSuccessHandler logoutHandler)
throws Exception {
http
.logout(logout -> logout
.logoutSuccessHandler(logoutHandler))
.build();
return http.build();
}Problem: Form submission fails with CSRF error
<!-- ❌ WRONG - Manual form without token -->
<form method="POST" action="/save">
<input type="text" name="name" />
<button type="submit">Save</button>
</form>
<!-- ✅ CORRECT - Thymeleaf automatically adds token -->
<form method="POST" th:action="@{/save}" th:object="${object}">
<input type="text" th:field="*{name}" />
<button type="submit">Save</button>
</form>Problem: User logs in but gets logged out immediately
Solutions:
// Solution 1: Increase session timeout
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.sessionFixationProtectionStrategy(SessionFixationProtectionStrategy.NONE)
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
.build();
return http.build();
}
// Solution 2: Ensure session is created
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
.build();
return http.build();
}Problem: Authorized user gets 403 Forbidden
Solutions:
// Ensure user has correct role (including ROLE_ prefix)
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/admin/**").hasRole("ADMIN") // Automatically adds ROLE_ prefix
.requestMatchers("/employee/**").hasRole("EMPLOYEE")
.anyRequest().authenticated())
.build();
return http.build();
}Problem: Custom access denied page not showing
Solution: Register access denied handler
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException exception)
throws IOException, ServletException {
response.sendRedirect("/access-denied");
}
}
// Register in SecurityConfig
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
CustomAccessDeniedHandler accessDeniedHandler)
throws Exception {
http
.exceptionHandling(exceptionHandling -> exceptionHandling
.accessDeniedHandler(accessDeniedHandler))
.build();
return http.build();
}@SpringBootTest
@AutoConfigureMockMvc
public class SecurityTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testLoginPageAccessible() throws Exception {
mockMvc.perform(get("/login"))
.andExpect(status().isOk());
}
@Test
public void testProtectedPageRequiresLogin() throws Exception {
mockMvc.perform(get("/dashboard"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrlPattern("**/login"));
}
@Test
public void testLoginWithValidCredentials() throws Exception {
mockMvc.perform(post("/authenticate")
.param("username", "employee")
.param("password", "password123"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/dashboard"));
}
@Test
public void testLoginWithInvalidCredentials() throws Exception {
mockMvc.perform(post("/authenticate")
.param("username", "employee")
.param("password", "wrong"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrlPattern("**/login?error"));
}
@Test
@WithMockUser(username = "admin", roles = "ADMIN")
public void testAdminCanAccessAdminPage() throws Exception {
mockMvc.perform(get("/admin"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(username = "employee", roles = "EMPLOYEE")
public void testEmployeeCannotAccessAdminPage() throws Exception {
mockMvc.perform(get("/admin"))
.andExpect(status().isForbidden());
}
@Test
public void testLogout() throws Exception {
mockMvc.perform(post("/logout"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrlPattern("**/login?logout"));
}
}@Component
public class SecurityAuditListener {
private static final Logger logger = LoggerFactory.getLogger(SecurityAuditListener.class);
@EventListener
public void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
logger.info("User {} authenticated successfully from {}",
event.getAuthentication().getName(),
getClientIp(event));
}
@EventListener
public void onAuthenticationFailure(AuthenticationFailureBadCredentialsEvent event) {
logger.warn("Failed login attempt for user {} from {}",
event.getAuthentication().getName(),
getClientIp(event));
}
private String getClientIp(ApplicationEvent event) {
// Extract IP from request
return "0.0.0.0"; // Implementation specific
}
}Happy Coding! 🚀