Skip to content

Latest commit

 

History

History
1433 lines (1133 loc) · 43.2 KB

File metadata and controls

1433 lines (1133 loc) · 43.2 KB

Spring Boot REST API Security - Complete Guide

📋 Table of Contents


🎯 Overview

This guide demonstrates how to secure Spring Boot REST APIs using Spring Security with progressive complexity levels:

  1. In-memory user storage
  2. Database storage with plain-text passwords
  3. Database storage with encrypted passwords (bcrypt)
  4. Custom database schemas

What You'll Learn

  • Secure REST API endpoints
  • Define users and roles
  • Protect URLs based on roles
  • Store credentials in database (plain-text → encrypted)
  • Use custom database schemas

🔐 Security Concepts

Authentication

Verifies user identity by checking credentials (username/password) against stored values.

Authorization

Determines if authenticated user has permission to access specific resources based on their role.

Security Implementation

Spring Security uses Servlet Filters to:

  • Pre-process/post-process web requests
  • Route requests based on security logic
  • Validate credentials and roles

⚙️ Setup & Configuration

Step 1: Add Spring Security Dependency

Add to pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Result: All endpoints are automatically secured.

Step 2: Override Default Credentials (Optional)

In src/main/resources/application.properties:

spring.security.user.name=scott
spring.security.user.password=test123

Default behavior:

  • Username: user
  • Password: Check console logs for auto-generated password

👥 User Management

In-Memory User Configuration

Create security configuration class:

File: DemoSecurityConfig.java

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 {
    
    @Bean
    public InMemoryUserDetailsManager userDetailsManager() {
        
        UserDetails john = User.builder()
                .username("john")
                .password("{noop}test123")
                .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 Format

{id}encodedPassword

Common IDs:

  • {noop} - Plain text passwords
  • {bcrypt} - BCrypt encrypted passwords

Example: {noop}test123


� Complete Security Examples

Example 1: Security Configuration with In-Memory Users

File: src/main/java/com/example/config/SecurityConfig.java

package com.example.config;

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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    public InMemoryUserDetailsManager userDetailsManager() {
        
        UserDetails employee = User.builder()
            .username("employee")
            .password("{noop}emp123")
            .roles("EMPLOYEE")
            .build();
        
        UserDetails manager = User.builder()
            .username("manager")
            .password("{noop}mgr123")
            .roles("EMPLOYEE", "MANAGER")
            .build();
        
        UserDetails admin = User.builder()
            .username("admin")
            .password("{noop}adm123")
            .roles("EMPLOYEE", "MANAGER", "ADMIN")
            .build();
        
        return new InMemoryUserDetailsManager(employee, manager, admin);
    }
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())  // Disable for stateless APIs
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/employees").hasRole("EMPLOYEE")
                .requestMatchers("/api/employees/{id}").hasRole("EMPLOYEE")
                .requestMatchers("/api/employees").hasRole("MANAGER")
                .requestMatchers("/api/employees/delete/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .httpBasic(basic -> {});
        
        return http.build();
    }
}

Example 2: Protected REST Controller

File: src/main/java/com/example/controller/SecureEmployeeController.java

package com.example.controller;

import com.example.entity.Employee;
import com.example.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/api/employees")
public class SecureEmployeeController {
    
    @Autowired
    private EmployeeService employeeService;
    
    // Public endpoint - no authentication required
    @GetMapping("/public/count")
    public ResponseEntity<Long> getEmployeeCount() {
        return ResponseEntity.ok(employeeService.getTotalEmployees());
    }
    
    // EMPLOYEE role required
    @GetMapping
    @PreAuthorize("hasRole('EMPLOYEE')")
    public ResponseEntity<List<Employee>> getAllEmployees(Authentication auth) {
        System.out.println("User: " + auth.getName() + " accessed employee list");
        return ResponseEntity.ok(employeeService.getAllEmployees());
    }
    
    // EMPLOYEE role required
    @GetMapping("/{id}")
    @PreAuthorize("hasRole('EMPLOYEE')")
    public ResponseEntity<Employee> getEmployeeById(@PathVariable Integer id) {
        Employee employee = employeeService.getEmployeeById(id);
        return ResponseEntity.ok(employee);
    }
    
    // MANAGER role required
    @PostMapping
    @PreAuthorize("hasRole('MANAGER')")
    public ResponseEntity<Employee> createEmployee(@RequestBody Employee employee) {
        Employee createdEmployee = employeeService.createEmployee(employee);
        return ResponseEntity.status(HttpStatus.CREATED).body(createdEmployee);
    }
    
    // MANAGER role required
    @PutMapping("/{id}")
    @PreAuthorize("hasRole('MANAGER')")
    public ResponseEntity<Employee> updateEmployee(
            @PathVariable Integer id,
            @RequestBody Employee employeeDetails) {
        Employee updatedEmployee = employeeService.updateEmployee(id, employeeDetails);
        return ResponseEntity.ok(updatedEmployee);
    }
    
    // ADMIN role required
    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<Void> deleteEmployee(@PathVariable Integer id) {
        employeeService.deleteEmployee(id);
        return ResponseEntity.noContent().build();
    }
    
    // Get current user info
    @GetMapping("/current-user")
    public ResponseEntity<String> getCurrentUser(Authentication auth) {
        return ResponseEntity.ok("Current user: " + auth.getName() + 
            " with roles: " + auth.getAuthorities());
    }
}

Example 3: Testing Security

File: src/test/java/com/example/controller/SecurityTest.java

package com.example.controller;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;

@SpringBootTest
@AutoConfigureMockMvc
public class SecurityTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Test
    public void testUnauthenticatedAccessDenied() throws Exception {
        mockMvc.perform(get("/api/employees"))
            .andExpect(status().isUnauthorized());
    }
    
    @Test
    @WithMockUser(username = "employee", roles = {"EMPLOYEE"})
    public void testEmployeeCanViewEmployees() throws Exception {
        mockMvc.perform(get("/api/employees"))
            .andExpect(status().isOk());
    }
    
    @Test
    @WithMockUser(username = "manager", roles = {"MANAGER"})
    public void testManagerCanCreateEmployee() throws Exception {
        String employeeJson = "{\"firstName\":\"John\",\"lastName\":\"Doe\",\"email\":\"john@example.com\"}";
        
        mockMvc.perform(post("/api/employees")
            .with(csrf())
            .contentType("application/json")
            .content(employeeJson))
            .andExpect(status().isCreated());
    }
    
    @Test
    @WithMockUser(username = "employee", roles = {"EMPLOYEE"})
    public void testEmployeeCannotCreateEmployee() throws Exception {
        String employeeJson = "{\"firstName\":\"John\",\"lastName\":\"Doe\",\"email\":\"john@example.com\"}";
        
        mockMvc.perform(post("/api/employees")
            .with(csrf())
            .contentType("application/json")
            .content(employeeJson))
            .andExpect(status().isForbidden());
    }
    
    @Test
    @WithMockUser(username = "admin", roles = {"ADMIN"})
    public void testAdminCanDeleteEmployee() throws Exception {
        mockMvc.perform(delete("/api/employees/1")
            .with(csrf()))
            .andExpect(status().isNoContent());
    }
}

Example 4: Login with cURL

# GET request with basic auth (employee role)
curl -u employee:emp123 http://localhost:8080/api/employees

# GET request with basic auth (manager role)
curl -u manager:mgr123 http://localhost:8080/api/employees

# Try unauthorized access (will fail)
curl http://localhost:8080/api/employees

# Create employee as manager
curl -u manager:mgr123 \
  -X POST http://localhost:8080/api/employees \
  -H "Content-Type: application/json" \
  -d '{
    "firstName":"Jane",
    "lastName":"Smith",
    "email":"jane@example.com",
    "department":"Marketing",
    "salary":70000
  }'

# Try to create as employee (will fail with 403)
curl -u employee:emp123 \
  -X POST http://localhost:8080/api/employees \
  -H "Content-Type: application/json" \
  -d '{
    "firstName":"Bob",
    "lastName":"Wilson",
    "email":"bob@example.com",
    "department":"IT",
    "salary":75000
  }'

# Delete employee as admin
curl -u admin:adm123 -X DELETE http://localhost:8080/api/employees/1

�🛡️ Role-Based Access Control

Define Access Rules by HTTP Method and Role

HTTP Method Endpoint Action Required Role
GET /api/employees Read all EMPLOYEE
GET /api/employees/{id} Read single EMPLOYEE
POST /api/employees Create MANAGER
PUT /api/employees Update MANAGER
PATCH /api/employees/{id} Partial Update MANAGER
DELETE /api/employees/{id} Delete ADMIN

Security Filter Chain Configuration

File: DemoSecurityConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class DemoSecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        
        http.authorizeHttpRequests(configurer ->
                configurer
                    // GET requests - EMPLOYEE role
                    .requestMatchers(HttpMethod.GET, "/api/employees").hasRole("EMPLOYEE")
                    .requestMatchers(HttpMethod.GET, "/api/employees/**").hasRole("EMPLOYEE")
                    
                    // POST requests - MANAGER role
                    .requestMatchers(HttpMethod.POST, "/api/employees").hasRole("MANAGER")
                    
                    // PUT requests - MANAGER role
                    .requestMatchers(HttpMethod.PUT, "/api/employees/**").hasRole("MANAGER")
                    
                    // PATCH requests - MANAGER role
                    .requestMatchers(HttpMethod.PATCH, "/api/employees/**").hasRole("MANAGER")
                    
                    // DELETE requests - ADMIN role
                    .requestMatchers(HttpMethod.DELETE, "/api/employees/**").hasRole("ADMIN")
        );
        
        // Use HTTP Basic authentication
        http.httpBasic(Customizer.withDefaults());
        
        // Disable CSRF (not needed for stateless REST APIs)
        http.csrf(csrf -> csrf.disable());
        
        return http.build();
    }
}

Request Matcher Syntax

Single role:

.requestMatchers(HttpMethod.GET, "/api/employees").hasRole("EMPLOYEE")

Multiple roles:

.requestMatchers(HttpMethod.POST, "/api/employees").hasAnyRole("MANAGER", "ADMIN")

Wildcard paths (** matches all sub-paths):

.requestMatchers(HttpMethod.GET, "/api/employees/**").hasRole("EMPLOYEE")

CSRF Protection

When to disable CSRF:

  • REST APIs for non-browser clients
  • Stateless APIs using POST, PUT, DELETE, PATCH

When to enable CSRF:

  • Traditional web apps with HTML forms
  • Browser-based form submissions
// Disable CSRF for REST APIs
http.csrf(csrf -> csrf.disable());

Important Note for Spring Data REST

When using Spring Data REST with PUT requests, the ID is passed in the URL:

PUT /api/employees/{employeeId}

Use this configuration:

.requestMatchers(HttpMethod.PUT, "/api/employees/**").hasRole("MANAGER")

💾 Database Integration

Database Schema Setup

Spring Security provides predefined table schemas for user authentication.

Step 1: Create Database Tables

File: create-tables.sql

Users Table:

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 Users (plain-text passwords):

INSERT INTO `users` 
VALUES 
    ('john', '{noop}test123', 1),
    ('mary', '{noop}test123', 1),
    ('susan', '{noop}test123', 1);

Authorities Table (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 Authorities:

INSERT INTO `authorities` 
VALUES 
    ('john', 'ROLE_EMPLOYEE'),
    ('mary', 'ROLE_EMPLOYEE'),
    ('mary', 'ROLE_MANAGER'),
    ('susan', 'ROLE_EMPLOYEE'),
    ('susan', 'ROLE_MANAGER'),
    ('susan', 'ROLE_ADMIN');

Note: Spring Security uses ROLE_ prefix internally for authorities.

Step 2: Add MySQL Dependency

File: pom.xml

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

Step 3: Configure Database Connection

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

Step 4: Update Security Configuration for JDBC

File: DemoSecurityConfig.java

import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;

@Configuration
public class DemoSecurityConfig {
    
    @Bean
    public UserDetailsManager userDetailsManager(DataSource dataSource) {
        // DataSource is auto-configured by Spring Boot
        return new JdbcUserDetailsManager(dataSource);
    }
    
    // ... SecurityFilterChain bean remains the same
}

Key Points:

  • DataSource is auto-configured by Spring Boot
  • No more hard-coded users!
  • Spring Security automatically queries the database

🔒 Password Encryption

BCrypt Algorithm

Why BCrypt?

  • One-way encrypted hashing
  • Adds random salt for additional protection
  • Defeats brute force attacks
  • Recommended by Spring Security team

Password Format

{bcrypt}$2a$10$qeS0HEh7urweMojsnwNAR.vcXJeXR1UcMRZ2WcGQl9YeuspUdgF.q

Structure:

  • {bcrypt} - 8 characters (encoding algorithm ID)
  • Encrypted password - 60 characters
  • Total: Minimum 68 characters

Generate BCrypt Passwords

Option 1: Online Tool Visit: BCrypt Password Generator

Option 2: Java Code

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class PasswordGenerator {
    public static void main(String[] args) {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        String password = "fun123";
        String encodedPassword = encoder.encode(password);
        System.out.println("Encoded: " + encodedPassword);
    }
}

Step 1: Modify Users Table DDL

CREATE TABLE `users` (
    `username` varchar(50) NOT NULL,
    `password` char(68) NOT NULL,  -- Changed to 68 characters
    `enabled` tinyint NOT NULL,
    PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

Step 2: Insert Encrypted Passwords

INSERT INTO `users` 
VALUES 
    ('john', '{bcrypt}$2a$10$qeS0HEh7urweMojsnwNAR.vcXJeXR1UcMRZ2WcGQl9YeuspUdgF.q', 1),
    ('mary', '{bcrypt}$2a$04$eFytJDGtjbThXa80FyOOBuFdK2IwjyWefYkMpiBEFlpBwDH.5PM0K', 1),
    ('susan', '{bcrypt}$2a$04$eFytJDGtjbThXa80FyOOBuFdK2IwjyWefYkMpiBEFlpBwDH.5PM0K', 1);

No Java Code Changes Required! Spring Security automatically:

  1. Reads the encoding algorithm ID ({bcrypt})
  2. Encrypts the plaintext password from login form
  3. Compares encrypted passwords
  4. Never decrypts the database password

Login Process

  1. User enters plaintext password
  2. Spring Security retrieves encrypted password from database
  3. Reads encoding algorithm ({bcrypt})
  4. Encrypts plaintext password using salt from DB password
  5. Compares encrypted passwords
  6. Login succeeds if passwords match

Important: Database password is NEVER decrypted (one-way encryption).


🗄️ Custom Tables

Problem

What if your database doesn't match Spring Security's default schema?

Example Custom Schema:

Members Table:

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;

Roles Table:

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;

Step 1: Create Custom Tables

Insert Sample Data:

INSERT INTO `members` 
VALUES 
    ('john', '{bcrypt}$2a$10$qeS0HEh7urweMojsnwNAR.vcXJeXR1UcMRZ2WcGQl9YeuspUdgF.q', 1),
    ('mary', '{bcrypt}$2a$04$eFytJDGtjbThXa80FyOOBuFdK2IwjyWefYkMpiBEFlpBwDH.5PM0K', 1),
    ('susan', '{bcrypt}$2a$04$eFytJDGtjbThXa80FyOOBuFdK2IwjyWefYkMpiBEFlpBwDH.5PM0K', 1);

INSERT INTO `roles` 
VALUES 
    ('john', 'ROLE_EMPLOYEE'),
    ('mary', 'ROLE_EMPLOYEE'),
    ('mary', 'ROLE_MANAGER'),
    ('susan', 'ROLE_EMPLOYEE'),
    ('susan', 'ROLE_MANAGER'),
    ('susan', 'ROLE_ADMIN');

Step 2: Configure Custom Queries

File: DemoSecurityConfig.java

import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;

@Configuration
public class DemoSecurityConfig {
    
    @Bean
    public UserDetailsManager userDetailsManager(DataSource dataSource) {
        
        JdbcUserDetailsManager theUserDetailsManager = new JdbcUserDetailsManager(dataSource);
        
        // Query to find user by username
        // ? parameter will be the username from login
        theUserDetailsManager.setUsersByUsernameQuery(
            "select user_id, pw, active from members where user_id=?"
        );
        
        // Query to find authorities/roles by username
        // ? parameter will be the username from login
        theUserDetailsManager.setAuthoritiesByUsernameQuery(
            "select user_id, role from roles where user_id=?"
        );
        
        return theUserDetailsManager;
    }
    
    // ... SecurityFilterChain bean remains the same
}

Query Requirements:

  • User query must return: username, password, enabled
  • Authority query must return: username, authority
  • ? is replaced with the username from login form

📝 Complete Examples

Example 1: In-Memory Configuration

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
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() {
        
        UserDetails john = User.builder()
                .username("john")
                .password("{noop}test123")
                .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);
    }
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        
        http.authorizeHttpRequests(configurer ->
                configurer
                    .requestMatchers(HttpMethod.GET, "/api/employees").hasRole("EMPLOYEE")
                    .requestMatchers(HttpMethod.GET, "/api/employees/**").hasRole("EMPLOYEE")
                    .requestMatchers(HttpMethod.POST, "/api/employees").hasRole("MANAGER")
                    .requestMatchers(HttpMethod.PUT, "/api/employees/**").hasRole("MANAGER")
                    .requestMatchers(HttpMethod.PATCH, "/api/employees/**").hasRole("MANAGER")
                    .requestMatchers(HttpMethod.DELETE, "/api/employees/**").hasRole("ADMIN")
        );
        
        http.httpBasic(Customizer.withDefaults());
        http.csrf(csrf -> csrf.disable());
        
        return http.build();
    }
}

Example 2: JDBC with Default Tables

import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
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;

@Configuration
public class DemoSecurityConfig {
    
    @Bean
    public UserDetailsManager userDetailsManager(DataSource dataSource) {
        return new JdbcUserDetailsManager(dataSource);
    }
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        
        http.authorizeHttpRequests(configurer ->
                configurer
                    .requestMatchers(HttpMethod.GET, "/api/employees").hasRole("EMPLOYEE")
                    .requestMatchers(HttpMethod.GET, "/api/employees/**").hasRole("EMPLOYEE")
                    .requestMatchers(HttpMethod.POST, "/api/employees").hasRole("MANAGER")
                    .requestMatchers(HttpMethod.PUT, "/api/employees/**").hasRole("MANAGER")
                    .requestMatchers(HttpMethod.PATCH, "/api/employees/**").hasRole("MANAGER")
                    .requestMatchers(HttpMethod.DELETE, "/api/employees/**").hasRole("ADMIN")
        );
        
        http.httpBasic(Customizer.withDefaults());
        http.csrf(csrf -> csrf.disable());
        
        return http.build();
    }
}

application.properties:

spring.datasource.url=jdbc:mysql://localhost:3306/employee_directory
spring.datasource.username=springstudent
spring.datasource.password=springstudent

Example 3: JDBC with Custom Tables

import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
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;

@Configuration
public class DemoSecurityConfig {
    
    @Bean
    public UserDetailsManager userDetailsManager(DataSource dataSource) {
        
        JdbcUserDetailsManager theUserDetailsManager = new JdbcUserDetailsManager(dataSource);
        
        theUserDetailsManager.setUsersByUsernameQuery(
            "select user_id, pw, active from members where user_id=?"
        );
        
        theUserDetailsManager.setAuthoritiesByUsernameQuery(
            "select user_id, role from roles where user_id=?"
        );
        
        return theUserDetailsManager;
    }
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        
        http.authorizeHttpRequests(configurer ->
                configurer
                    .requestMatchers(HttpMethod.GET, "/api/employees").hasRole("EMPLOYEE")
                    .requestMatchers(HttpMethod.GET, "/api/employees/**").hasRole("EMPLOYEE")
                    .requestMatchers(HttpMethod.POST, "/api/employees").hasRole("MANAGER")
                    .requestMatchers(HttpMethod.PUT, "/api/employees/**").hasRole("MANAGER")
                    .requestMatchers(HttpMethod.PATCH, "/api/employees/**").hasRole("MANAGER")
                    .requestMatchers(HttpMethod.DELETE, "/api/employees/**").hasRole("ADMIN")
        );
        
        http.httpBasic(Customizer.withDefaults());
        http.csrf(csrf -> csrf.disable());
        
        return http.build();
    }
}

🧪 Testing with cURL

Test as EMPLOYEE (john)

# Read all employees (Success - 200 OK)
curl -u john:test123 http://localhost:8080/api/employees

# Read single employee (Success - 200 OK)
curl -u john:test123 http://localhost:8080/api/employees/1

# Try to create employee (Fail - 403 Forbidden)
curl -u john:test123 -X POST http://localhost:8080/api/employees \
  -H "Content-Type: application/json" \
  -d '{"firstName":"David","lastName":"Brown","email":"david@example.com"}'

Test as MANAGER (mary)

# Read employees (Success - 200 OK)
curl -u mary:test123 http://localhost:8080/api/employees

# Create employee (Success - 201 Created)
curl -u mary:test123 -X POST http://localhost:8080/api/employees \
  -H "Content-Type: application/json" \
  -d '{"firstName":"David","lastName":"Brown","email":"david@example.com"}'

# Update employee (Success - 200 OK)
curl -u mary:test123 -X PUT http://localhost:8080/api/employees/1 \
  -H "Content-Type: application/json" \
  -d '{"firstName":"John","lastName":"Updated","email":"john@example.com"}'

# Partial update (Success - 200 OK)
curl -u mary:test123 -X PATCH http://localhost:8080/api/employees/1 \
  -H "Content-Type: application/json" \
  -d '{"email":"newemail@example.com"}'

# Try to delete (Fail - 403 Forbidden)
curl -u mary:test123 -X DELETE http://localhost:8080/api/employees/1

Test as ADMIN (susan)

# All operations allowed

# Delete employee (Success - 200 OK)
curl -u susan:test123 -X DELETE http://localhost:8080/api/employees/1

🚀 Quick Start Checklist

Basic Setup

  • Add spring-boot-starter-security dependency
  • Create DemoSecurityConfig class with @Configuration
  • Define users with InMemoryUserDetailsManager
  • Configure SecurityFilterChain with role-based access
  • Disable CSRF for REST APIs

Database Integration

  • Add MySQL dependency
  • Create users and authorities tables
  • Configure application.properties with database connection
  • Update config to use JdbcUserDetailsManager
  • Test with plain-text passwords

Password Encryption

  • Generate BCrypt passwords
  • Modify password column to char(68)
  • Update INSERT statements with {bcrypt} prefix
  • Test login with encrypted passwords

Custom Tables

  • Create custom database schema
  • Configure custom SQL queries in DemoSecurityConfig
  • Test authentication with custom tables

📚 Additional Resources


🔐 JWT (JSON Web Token) Authentication

What is JWT?

JWT is a stateless authentication method perfect for REST APIs:

  • Client sends credentials
  • Server returns JWT token
  • Client includes token in subsequent requests
  • Server validates token without storing session

JWT Structure

Header.Payload.Signature

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

JWT Implementation with Spring Security

Step 1: Add Dependency

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.3</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.3</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.3</version>
    <scope>runtime</scope>
</dependency>

Step 2: JWT Provider

@Component
public class JwtTokenProvider {
    
    @Value("${app.jwtSecret:mySecretKeyThatMustBeLongerThan32Characters}")
    private String jwtSecret;
    
    @Value("${app.jwtExpirationInMs:86400000}")  // 24 hours
    private long jwtExpirationInMs;
    
    public String generateToken(String username) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
        
        return Jwts.builder()
            .subject(username)
            .issuedAt(now)
            .expiration(expiryDate)
            .signWith(Keys.hmacShaKeyFor(jwtSecret.getBytes()), Jwts.SIG.HS512)
            .compact();
    }
    
    public String getUsernameFromJwt(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes()))
            .build()
            .parseClaimsJws(token)
            .getBody()
            .getSubject();
    }
    
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                .setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes()))
                .build()
                .parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}

Step 3: JWT Authentication Filter

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtTokenProvider tokenProvider;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) 
            throws ServletException, IOException {
        
        String token = extractTokenFromRequest(request);
        
        if (token != null && tokenProvider.validateToken(token)) {
            String username = tokenProvider.getUsernameFromJwt(token);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            
            UsernamePasswordAuthenticationToken auth = 
                new UsernamePasswordAuthenticationToken(userDetails, null, 
                    userDetails.getAuthorities());
            
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        
        filterChain.doFilter(request, response);
    }
    
    private String extractTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

🛡️ CSRF Protection for REST APIs

Understanding CSRF

CSRF (Cross-Site Request Forgery) - attacker tricks user into performing unwanted action.

Disable CSRF for Stateless REST APIs

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())  // Stateless APIs don't need CSRF
            .sessionManagement(session -> 
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/**").authenticated()
                .anyRequest().permitAll())
            .httpBasic(basic -> {})
            .addFilterBefore(jwtAuthenticationFilter(), 
                UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
}

CSRF with Forms (Web Applications)

For Thymeleaf forms, include CSRF token:

<form method="POST" action="/api/employees">
    <input type="hidden" name="_csrf" th:value="${_csrf.token}"/>
    <input type="text" name="firstName" />
    <button type="submit">Save</button>
</form>

⚠️ Security Best Practices

1. Always Use HTTPS

# Enforce HTTPS in production
server.ssl.enabled=true
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=${SSL_PASSWORD}
server.ssl.key-store-type=PKCS12

2. Secure Password Storage

// ✅ CORRECT - Use BCryptPasswordEncoder
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

// ❌ WRONG - Storing plain text
password = "myPassword"  // NEVER!

3. HTTP Security Headers

@Configuration
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .headers(headers -> headers
                .contentSecurityPolicy(csp -> 
                    csp.policyDirectives("default-src 'self'"))
                .xssProtection(xss -> xss.enable())
                .frameOptions(frame -> frame.deny()))
            .build();
        
        return http.build();
    }
}

4. Rate Limiting (Prevent Brute Force)

@Component
public class RateLimitingFilter extends OncePerRequestFilter {
    
    private final Map<String, List<Long>> requestCounts = new ConcurrentHashMap<>();
    private static final int MAX_REQUESTS = 100;
    private static final long TIME_WINDOW = 60000; // 1 minute
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) 
            throws ServletException, IOException {
        
        String clientIp = request.getRemoteAddr();
        List<Long> requests = requestCounts.computeIfAbsent(clientIp, k -> new ArrayList<>());
        
        long now = System.currentTimeMillis();
        requests.removeIf(timestamp -> now - timestamp > TIME_WINDOW);
        
        if (requests.size() >= MAX_REQUESTS) {
            response.sendError(HttpServletResponse.SC_TOO_MANY_REQUESTS, 
                "Rate limit exceeded");
            return;
        }
        
        requests.add(now);
        filterChain.doFilter(request, response);
    }
}

5. 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", 
            event.getAuthentication().getName());
    }
    
    @EventListener
    public void onAuthenticationFailure(AuthenticationFailureBadCredentialsEvent event) {
        logger.warn("Failed login attempt for user: {}", 
            event.getAuthentication().getName());
    }
}

⚠️ Common Security Issues & Solutions

Issue: Passwords Exposed in Logs

Problem: Password appears in logs during authentication failure

DEBUG - Attempting authentication for user: john with password: secret123

Solution: Use logging filters and sanitization

@Component
public class SensitiveDataFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) 
            throws ServletException, IOException {
        
        if (request.getParameter("password") != null) {
            // Don't log passwords
            logger.debug("Password field detected - will not log");
        }
        
        filterChain.doFilter(request, response);
    }
}

Issue: Token Expiration Not Handled

Problem: Expired tokens still accepted

Solution: Add token refresh mechanism

@PostMapping("/api/auth/refresh")
public ResponseEntity<String> refreshToken(@RequestHeader("Authorization") String token) {
    if (tokenProvider.validateToken(token)) {
        String username = tokenProvider.getUsernameFromJwt(token);
        String newToken = tokenProvider.generateToken(username);
        return ResponseEntity.ok(newToken);
    }
    return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}

Issue: Sensitive Information in Error Messages

Problem: Stack traces and database details exposed to users

ERROR - org.hibernate.exception.ConstraintViolationException: 
Duplicate entry 'user@example.com' for key 'users.email_unique'

Solution: Return generic error messages

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(DataIntegrityViolationException.class)
    public ResponseEntity<ErrorResponse> handleDataIntegrity(
            DataIntegrityViolationException e) {
        logger.error("Data integrity violation", e);
        ErrorResponse error = new ErrorResponse("Validation failed", 
            "Please check your input and try again");
        return ResponseEntity.badRequest().body(error);
    }
}

📝 Security Testing Checklist

✅ Authentication
  ├── Test valid credentials
  ├── Test invalid credentials
  ├── Test missing credentials
  └── Test token expiration

✅ Authorization
  ├── Test role-based access
  ├── Test permission checks
  ├── Test cross-user access attempts
  └── Test privilege escalation

✅ API Security
  ├── Test CSRF protection (if applicable)
  ├── Test SQL injection prevention
  ├── Test XSS prevention
  └── Test rate limiting

✅ Data Security
  ├── Test password hashing
  ├── Test HTTPS enforcement
  ├── Test sensitive data logging
  └── Test data exposure in responses

✅ Session Management
  ├── Test session timeout
  ├── Test concurrent sessions
  ├── Test logout functionality
  └── Test session fixation prevention

⚠️ Important Notes

  1. Password Column Length: Must be at least 68 characters for BCrypt
  2. ROLE_ Prefix: Spring Security adds this automatically, but must be in database
  3. CSRF: Disable for stateless REST APIs
  4. Wildcard Paths: Use /** for matching all sub-paths
  5. Spring Data REST: Use /** with PUT requests when ID is in URL
  6. No Decryption: BCrypt is one-way; passwords are never decrypted
  7. JWT Secret: Keep secret key secure and at least 256-bit long
  8. HTTPS: Always use HTTPS in production for token transmission
  9. Token Storage: Store tokens securely on client side (HttpOnly cookies or secure storage)
  10. Token Rotation: Implement token refresh for long-lived sessions

Happy Coding! 🚀