Skip to content

Latest commit

 

History

History
2392 lines (1883 loc) · 60.8 KB

File metadata and controls

2392 lines (1883 loc) · 60.8 KB

Spring Security - Complete Implementation Guide

A comprehensive guide for implementing Spring Security in Spring Boot applications with authentication, authorization, custom login forms, database integration, and password encryption.

📋 Table of Contents

🎯 Overview

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

Application Example Structure

Home Page (/)               → EMPLOYEE role required
Leadership Page (/leaders)  → MANAGER role required
Systems Page (/systems)     → ADMIN role required

🔐 Security Concepts

Authentication

  • Definition: Verifying user identity (user ID and password)
  • Process: Check credentials against stored values in app/database

Authorization

  • Definition: Determining if authenticated user has permission
  • Process: Check if user has authorized role for requested resource

Security Implementation Methods

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

✨ Features

  • 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

🛠️ Technology Stack

  • 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)

📁 Project Structure

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

🚀 Setup and Configuration

Step 1: Maven Dependencies

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>

Step 2: Application Properties

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=DEBUG

💻 Implementation Guide

Phase 1: Basic Security with In-Memory Users

1. Create Security Configuration

Create 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

2. Create Demo Controller

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";
    }
}

3. Create Home Page Template

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

4. Test Basic Security

# 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)

Phase 2: Custom Login Form

1. Update Security Configuration

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: /logout

2. Create Login Controller

Create 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";
    }
}

3. Create Plain Login Form

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: ?error appended on failed login
  • Logout parameter: ?logout appended after logout

4. Create Fancy Bootstrap Login Form

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"
}

Phase 3: Role-Based Access Control

1. Update Security Configuration for Role Restrictions

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")
)

2. Create Additional Pages

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>

3. Create Access Denied Page

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>

4. Add Controller Mapping for Access Denied

Update DemoController.java:

@GetMapping("/access-denied")
public String showAccessDenied() {
    return "access-denied";
}

💾 Database Integration

Phase 4: JDBC Authentication with Default Schema

1. Create Database and Tables

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;

2. Configure Database Connection

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=true

3. Update Security Configuration for JDBC

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
     * 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:

  • DataSource is automatically injected by Spring Boot
  • Uses default Spring Security table schema
  • No need to hardcode users anymore
  • Passwords stored in database

4. Test Database Authentication

# Run application
mvn spring-boot:run

# Test with database credentials
Username: john    Password: test123
Username: mary    Password: test123
Username: susan   Password: test123

🔒 Password Encryption

Phase 5: BCrypt Password Encryption

1. Generate BCrypt Passwords

Option 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"

2. Create Database with BCrypt Passwords

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                                        │

3. Run BCrypt SQL Script

# 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;

4. Test BCrypt Authentication

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: fun123

BCrypt Login Process Explained

1. 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

🗄️ Custom Tables

Phase 6: Using Custom Database Schema

1. Create Custom Tables

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

2. Update Security Configuration for Custom Tables

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:

  1. Username
  2. Password
  3. Enabled flag (boolean)
SELECT user_id, pw, active FROM members WHERE user_id=?

Authorities Query Must Return:

  1. Username
  2. 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'

3. Run Custom Tables SQL Script

# 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;

4. Test Custom Tables Authentication

# Run application
mvn spring-boot:run

# Test with custom table credentials
Username: john    Password: fun123
Username: mary    Password: fun123
Username: susan   Password: fun123

🧪 Testing

Manual Testing Guide

Test 1: Login with Different Users

# 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 2: Login Errors

# 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

Test 3: Logout Functionality

# 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

Test 4: Access Denied Page

# 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

Test 5: Database Integration

# 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

Test 6: Password Encryption

# 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

Testing with cURL

Test Login (Get CSRF Token)

# 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/

Test Logout

# Logout
curl -b cookies.txt -X POST http://localhost:8080/logout

Testing Checklist

Authentication 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

🐛 Troubleshooting

Common Issues and Solutions

1. Login Always Fails

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;

2. 403 Access Denied on Login

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();
}

3. CSRF Token Error

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();
}

4. Password Column Too Short

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 |

5. Custom Tables Not Working

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 results

6. Thymeleaf Template Not Found

Symptoms:

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.html

7. Roles Not Displaying

Symptoms:

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>

8. Database Connection Fails

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=UTC

9. Context Path Issues

Symptoms:

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!

10. Fresh Session Testing

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:8080

Debug Logging

Add 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

📚 Additional Resources

Official Documentation

Tutorials and Guides

Spring Security Features Not Covered

  • 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

📄 Quick Reference

Security Configuration Template

@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();
    }
}

Thymeleaf Security Expressions

<!-- 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>

Password Formats

Plaintext:  {noop}mypassword
BCrypt:     {bcrypt}$2a$10$...
Argon2:     {argon2}$argon2id$...
Pbkdf2:     {pbkdf2}...
SCrypt:     {scrypt}$...

⚙️ CSRF Token Handling

Automatic CSRF Token in Forms

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>

CSRF in AJAX Requests

// 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'})
});

Include CSRF in HTML

<!-- Add to base template -->
<head>
    <meta name="_csrf" th:content="${_csrf.token}"/>
    <meta name="_csrf_header" th:content="${_csrf.headerName}"/>
</head>

📋 Session Management

Session Timeout Configuration

# 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

Remember-Me Functionality

@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>

Concurrent Session Control

@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();
    }
}

🚪 Logout Implementation

Logout Form

<!-- 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>

Logout Configuration

@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();
    }
}

Logout Success Handler

@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();
}

⚠️ Common Security Issues & Solutions

Issue: CSRF Token Not Sent in POST

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>

Issue: Session Not Persisting

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();
}

Issue: User Sees Authorization Denied Page

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();
}

Issue: Access Denied Handler Not Invoked

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();
}

🧪 Security Testing

Unit Testing Security Configuration

@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"));
    }
}

📊 Security Monitoring

Audit Logging

@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! 🚀