Spring Security Fundamentals: A Complete Guide

Security is a critical aspect of web applications. You need to protect your resources from unauthorized access and ensure that only authenticated users can access private resources.

This comprehensive guide covers everything you need to know about Spring Security - from basic concepts to production-ready implementations.

Web Application Security Overview

Public vs Private Resources

In most web applications, you have two types of resources:

1. Public Resources - Available to everyone without authentication

  • Homepage
  • About page
  • Contact page
  • Product listing pages
  • Login page
  • Registration page

2. Private Resources - Available only to authenticated users

  • User profile
  • Dashboard
  • Shopping cart
  • Order history
  • Admin panel

Real-World Example: Think of a shopping website like Amazon:

  • Anyone can browse products (public)
  • Only logged-in users can add to cart or make purchases (private)
  • Only admins can manage products and orders (private + role-based)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Web Application                 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Public Resources                       β”‚
β”‚  βœ“ /home        (Everyone)              β”‚
β”‚  βœ“ /products    (Everyone)              β”‚
β”‚  βœ“ /login       (Everyone)              β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Private Resources                      β”‚
β”‚  βœ“ /profile     (Authenticated Users)   β”‚
β”‚  βœ“ /cart        (Authenticated Users)   β”‚
β”‚  βœ“ /admin       (Admin Role Only)       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Role-Based Access Control (RBAC)

RBAC is a method to control access to resources based on the user’s role in the organization.

Common Roles:

  • USER - Regular users with basic access
  • ADMIN - Administrators with full access
  • MANAGER - Managers with specific permissions
  • GUEST - Limited access

Example Scenario:

User: John (Role: USER)
- Can view products βœ“
- Can place orders βœ“
- Can view own profile βœ“
- Cannot access admin panel βœ—

User: Sarah (Role: ADMIN)
- Can view products βœ“
- Can place orders βœ“
- Can view own profile βœ“
- Can access admin panel βœ“
- Can manage all users βœ“

RBAC Benefits:

  1. Simplified Management - Assign roles instead of individual permissions
  2. Scalability - Easy to add new users with predefined roles
  3. Security - Principle of least privilege (users get only what they need)
  4. Compliance - Meet regulatory requirements

Java Web Application Security

Before diving into Spring Security, let’s understand how security works in traditional Java web applications.

Servlets to Handle HTTP Requests

Servlets are Java classes that handle HTTP requests and responses.

Example Servlet:

@WebServlet("/profile")
public class ProfileServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        // Check if user is logged in
        HttpSession session = request.getSession(false);
        if (session == null || session.getAttribute("user") == null) {
            response.sendRedirect("/login");
            return;
        }

        // User is authenticated, show profile
        response.getWriter().println("Welcome to your profile!");
    }
}

Problems with Manual Security Checks:

  • Code duplication in every servlet
  • Easy to forget security checks
  • Hard to maintain
  • No centralized security logic

Filters to Intercept Requests

Filters are Java components that intercept requests before they reach servlets. They’re perfect for implementing security checks.

Filter Lifecycle:

Browser β†’ Filter β†’ Servlet β†’ Response
           ↓
    Security Check

Example Security Filter:

@WebFilter("/secure/*")
public class AuthenticationFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // Check if user is authenticated
        HttpSession session = httpRequest.getSession(false);
        if (session == null || session.getAttribute("user") == null) {
            // Not authenticated - redirect to login
            httpResponse.sendRedirect("/login");
            return;
        }

        // Authenticated - continue to the requested resource
        chain.doFilter(request, response);
    }
}

How Filters Work:

  1. Request arrives at /secure/profile
  2. Filter intercepts the request
  3. Filter checks if user is authenticated
  4. If authenticated: Pass request to servlet
  5. If not authenticated: Redirect to login page

Filter Pattern Matching:

@WebFilter("/admin/*")    // Protects all admin URLs
@WebFilter("/secure/*")   // Protects all secure URLs
@WebFilter("/*")          // Intercepts ALL requests

Authentication and Authorization

Authentication - β€œWho are you?”

  • Verifies the identity of the user
  • Checks username and password
  • Confirms user is who they claim to be

Authorization - β€œWhat can you do?”

  • Determines what resources the user can access
  • Checks user’s roles and permissions
  • Enforces access control rules

Example Flow:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 1. User Login Request                                β”‚
β”‚    Username: john@example.com                        β”‚
β”‚    Password: secret123                               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                 ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 2. Authentication (Who are you?)                     β”‚
β”‚    - Verify username exists                          β”‚
β”‚    - Check password matches                          β”‚
β”‚    - Load user roles                                 β”‚
β”‚    Result: βœ“ Authenticated (Roles: USER, MANAGER)    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                 ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 3. User tries to access /admin/users                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                 ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 4. Authorization (What can you do?)                  β”‚
β”‚    Required Role: ADMIN                              β”‚
β”‚    User Roles: USER, MANAGER                         β”‚
β”‚    Result: βœ— Access Denied (Missing ADMIN role)      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Traditional Java Implementation:

public class SecurityHelper {

    // Authentication
    public User authenticate(String username, String password) {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new AuthenticationException("User not found");
        }

        if (!passwordMatches(password, user.getPassword())) {
            throw new AuthenticationException("Invalid password");
        }

        return user;
    }

    // Authorization
    public boolean authorize(User user, String requiredRole) {
        return user.getRoles().contains(requiredRole);
    }
}

Limitations of Manual Implementation:

  • Complex code to maintain
  • Security vulnerabilities if not done correctly
  • Need to handle sessions, cookies, CSRF tokens manually
  • Password encryption and hashing
  • Remember-me functionality
  • Account lockout after failed attempts

This is where Spring Security comes in! It provides all these features out of the box.

Spring Security Architecture

Spring Security is a powerful and highly customizable authentication and access-control framework. It’s the de-facto standard for securing Spring-based applications.

Spring Security Filter Chain

Spring Security uses a chain of filters to handle security concerns. When a request comes in, it passes through multiple security filters before reaching your controller.

Filter Chain Flow:

HTTP Request
     ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Spring Security Filter Chain              β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  1. SecurityContextPersistenceFilter       β”‚
β”‚     (Load security context from session)   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  2. LogoutFilter                           β”‚
β”‚     (Handle logout requests)               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  3. UsernamePasswordAuthenticationFilter   β”‚
β”‚     (Process login form submission)        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  4. BasicAuthenticationFilter              β”‚
β”‚     (Handle HTTP Basic authentication)     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  5. ExceptionTranslationFilter             β”‚
β”‚     (Handle security exceptions)           β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  6. FilterSecurityInterceptor              β”‚
β”‚     (Check authorization/access control)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   ↓
           Your Controller

Key Filters Explained:

  1. SecurityContextPersistenceFilter

    • Loads the security context (user info) from session
    • Makes it available throughout the request
  2. UsernamePasswordAuthenticationFilter

    • Processes login form submissions
    • Authenticates username and password
    • Creates authentication token
  3. ExceptionTranslationFilter

    • Catches security exceptions
    • Redirects to login page if not authenticated
    • Shows access denied page if not authorized
  4. FilterSecurityInterceptor

    • Final filter in the chain
    • Checks if user has required roles/permissions
    • Either allows access or throws AccessDeniedException

Example Request Flow:

User submits login form
  ↓
UsernamePasswordAuthenticationFilter intercepts
  ↓
Extracts username="john" password="secret123"
  ↓
Calls AuthenticationManager.authenticate()
  ↓
UserDetailsService loads user from database
  ↓
PasswordEncoder compares passwords
  ↓
If successful: Create Authentication object
  ↓
Store in SecurityContext
  ↓
Redirect to homepage

Spring Security Configuration

Spring Security can be configured in multiple ways:

1. Default Configuration (Zero Configuration)

  • Spring Boot auto-configures security
  • All endpoints protected by default
  • Single user with generated password

2. Properties-based Configuration

  • Configure username/password in application.properties
  • Simple but limited customization

3. Java-based Configuration

  • Create a configuration class
  • Full control over security behavior
  • Most flexible approach

Configuration Class Structure:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll()
            )
            .logout(logout -> logout
                .logoutSuccessUrl("/")
            );

        return http.build();
    }
}

Spring Security Authentication Methods

Spring Security supports multiple authentication methods:

1. Basic Authentication

Sends credentials with each request in HTTP headers (Base64 encoded).

GET /api/users HTTP/1.1
Authorization: Basic am9objpzZWNyZXQxMjM=

Configuration:

http.httpBasic(Customizer.withDefaults());

Use Case: REST APIs, simple integrations

Pros:

  • Simple to implement
  • Stateless (no session)

Cons:

  • Credentials sent with every request
  • Must use HTTPS
  • No logout mechanism

2. Form-Based Login

Traditional HTML form for login.

<form action="/login" method="POST">
    <input type="text" name="username" />
    <input type="password" name="password" />
    <button type="submit">Login</button>
</form>

Configuration:

http.formLogin(form -> form
    .loginPage("/login")
    .defaultSuccessUrl("/home")
    .permitAll()
);

Use Case: Web applications with UI

Pros:

  • User-friendly
  • Session-based
  • Supports remember-me

Cons:

  • Requires session management
  • CSRF protection needed

3. JWT (JSON Web Token)

Token-based authentication for stateless applications.

GET /api/users HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Use Case: Modern SPAs, microservices

Pros:

  • Stateless
  • Scalable
  • Works across domains

Cons:

  • Token revocation complexity
  • Larger payload size

4. OAuth2

Delegated authentication using third-party providers.

Configuration:

http.oauth2Login(oauth -> oauth
    .loginPage("/login")
    .defaultSuccessUrl("/home")
);

Use Case: Social login (Google, Facebook, GitHub)

Pros:

  • No password management
  • Trusted providers
  • Better user experience

Cons:

  • Depends on external service
  • Complex setup

Spring Security Key Components

Understanding these core components is essential for working with Spring Security.

1. UserDetails

UserDetails is an interface that represents a user in Spring Security. It provides core user information.

Interface Definition:

public interface UserDetails {
    String getUsername();
    String getPassword();
    Collection<? extends GrantedAuthority> getAuthorities();
    boolean isAccountNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();
}

Implementation Example:

public class CustomUserDetails implements UserDetails {

    private String username;
    private String password;
    private List<GrantedAuthority> authorities;
    private boolean enabled;

    public CustomUserDetails(User user) {
        this.username = user.getEmail();
        this.password = user.getPassword();
        this.authorities = user.getRoles().stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
            .collect(Collectors.toList());
        this.enabled = user.isActive();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

Spring’s Default Implementation:

// Quick way to create UserDetails
UserDetails user = org.springframework.security.core.userdetails.User
    .withUsername("john")
    .password("{bcrypt}$2a$10$...")
    .roles("USER", "ADMIN")
    .build();

Key Points:

  • Represents the authenticated user
  • Contains credentials and authorities
  • Can be customized to include additional fields (email, phone, etc.)

2. UserDetailsService

UserDetailsService is responsible for loading user-specific data. It has one method: loadUserByUsername().

Interface Definition:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

Implementation Example:

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {

        // Load user from database
        User user = userRepository.findByEmail(username)
            .orElseThrow(() -> new UsernameNotFoundException(
                "User not found: " + username));

        // Convert to UserDetails
        return new CustomUserDetails(user);
    }
}

Real-World Example with JPA:

// Entity
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String email;

    @Column(nullable = false)
    private String password;

    @ElementCollection(fetch = FetchType.EAGER)
    private Set<String> roles = new HashSet<>();

    private boolean active = true;

    // Getters and setters
}

// Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

// UserDetailsService
@Service
public class DatabaseUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String email)
            throws UsernameNotFoundException {

        User user = userRepository.findByEmail(email)
            .orElseThrow(() -> new UsernameNotFoundException(
                "User not found with email: " + email));

        return org.springframework.security.core.userdetails.User
            .withUsername(user.getEmail())
            .password(user.getPassword())
            .roles(user.getRoles().toArray(new String[0]))
            .disabled(!user.isActive())
            .build();
    }
}

Key Points:

  • Called by Spring Security during authentication
  • Should throw UsernameNotFoundException if user not found
  • Typically loads user from database, LDAP, or external API

3. PasswordEncoder

PasswordEncoder is used to encode passwords and verify them during authentication.

Interface Definition:

public interface PasswordEncoder {
    String encode(CharSequence rawPassword);
    boolean matches(CharSequence rawPassword, String encodedPassword);
}

Common Encoders:

  1. BCryptPasswordEncoder (Recommended)
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}
  1. Pbkdf2PasswordEncoder
@Bean
public PasswordEncoder passwordEncoder() {
    return new Pbkdf2PasswordEncoder();
}
  1. SCryptPasswordEncoder
@Bean
public PasswordEncoder passwordEncoder() {
    return new SCryptPasswordEncoder();
}

Usage Example:

@Service
public class UserService {

    private final PasswordEncoder passwordEncoder;
    private final UserRepository userRepository;

    //constructor

    public void registerUser(String email, String rawPassword) {
        // Encode password before saving
        String encodedPassword = passwordEncoder.encode(rawPassword);

        User user = new User();
        user.setEmail(email);
        user.setPassword(encodedPassword);
        user.getRoles().add("USER");

        userRepository.save(user);
    }

    public boolean checkPassword(String email, String rawPassword) {
        User user = userRepository.findByEmail(email)
            .orElseThrow(() -> new UsernameNotFoundException("User not found"));

        // Verify password
        return passwordEncoder.matches(rawPassword, user.getPassword());
    }
}

BCrypt Example:

PasswordEncoder encoder = new BCryptPasswordEncoder();

// Encoding
String rawPassword = "myPassword123";
String encoded = encoder.encode(rawPassword);
// Output: $2a$10$xjfHKf7d8YhKJF9fJK8f.eW...

// Verification
boolean matches = encoder.matches("myPassword123", encoded);
// Output: true

boolean matches2 = encoder.matches("wrongPassword", encoded);
// Output: false

Password Storage Format:

{algorithm}encodedPassword

Examples:
{bcrypt}$2a$10$xjfHKf7d8YhKJF9fJK8f.eW...
{pbkdf2}$2a$10$xjfHKf7d8YhKJF9fJK8f.eW...
{noop}plainTextPassword  // No encoding (NOT RECOMMENDED)

Never store plain text passwords! BCrypt is recommended because it’s adaptive and includes salt automatically.

4. AuthenticationManager

AuthenticationManager is the main interface for authentication in Spring Security.

Interface Definition:

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication)
        throws AuthenticationException;
}

Default Implementation: ProviderManager

ProviderManager delegates to a list of AuthenticationProvider instances.

Authentication Flow:

1. User submits credentials
         ↓
2. Create Authentication object (unauthenticated)
         ↓
3. AuthenticationManager.authenticate(auth)
         ↓
4. ProviderManager loops through AuthenticationProviders
         ↓
5. DaoAuthenticationProvider:
   - Calls UserDetailsService.loadUserByUsername()
   - Loads UserDetails from database
   - Calls PasswordEncoder.matches()
   - Compares passwords
         ↓
6. If successful: Return Authentication object (authenticated)
   If failed: Throw AuthenticationException
         ↓
7. Store Authentication in SecurityContext

Configuration Example:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    AuthenticationManager authenticationManager(UserDetailsService uds, PasswordEncoder pe) {
        var authProvider = new DaoAuthenticationProvider(uds);
        authProvider.setPasswordEncoder(pe);

        return new ProviderManager(authProvider);
    }
}

Manual Authentication Example:

@RestController
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @PostMapping("/login")
    public ResponseEntity<String> login(@RequestBody LoginRequest request) {
        try {
            // Create unauthenticated token
            Authentication authentication = new UsernamePasswordAuthenticationToken(
                request.getUsername(),
                request.getPassword()
            );

            // Authenticate
            Authentication authenticated =
                authenticationManager.authenticate(authentication);

            // Store in security context
            SecurityContextHolder.getContext().setAuthentication(authenticated);

            return ResponseEntity.ok("Login successful");

        } catch (AuthenticationException e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body("Invalid credentials");
        }
    }
}

Key Components Working Together:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         AuthenticationManager                       β”‚
β”‚           (ProviderManager)                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                 ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      DaoAuthenticationProvider                      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  - Uses UserDetailsService                          β”‚
β”‚  - Uses PasswordEncoder                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             ↓                    ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ UserDetailsService β”‚  β”‚  PasswordEncoder    β”‚
β”‚                    β”‚  β”‚                     β”‚
β”‚ loadUserByUsername β”‚  β”‚  encode()           β”‚
β”‚                    β”‚  β”‚  matches()          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Spring Security in Web Applications

Let’s explore different ways to configure Spring Security in a Spring Boot application, from simplest to most advanced.

1. Default Spring Security Configuration

When you add Spring Security dependency, Spring Boot automatically configures security.

Add Dependency:

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

What Happens Automatically:

  1. All endpoints/urls are protected - Requires authentication for every URL
  2. Default user created - Username: user
  3. Random password generated - Printed in console logs
  4. Default login page - Auto-generated at /login
  5. HTTP Basic authentication - Enabled by default
  6. CSRF protection - Enabled by default

Console Output:

Using generated security password: 8e4e5c3a-6f5b-4d8e-9a3f-2c1d5e6f7a8b

To use Thymeleaf views, add the following dependency:

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

Create thymeleaf templates in src/main/resources/templates directory.

Testing Default Security:

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@RestController
public class HomeController {

    @GetMapping("/")
    public String home() {
        return "Welcome Home!";
    }

    @GetMapping("/user")
    public String user() {
        return "User Page";
    }
}

Accessing the Application:

  • Navigate to http://localhost:8080/
  • Redirected to /login
  • Enter username: user
  • Enter password: (from console)
  • Access granted

2. Security Configuration with Properties

Configure basic security settings using application.properties or application.yml.

application.properties:

# Single user configuration
spring.security.user.name=admin
spring.security.user.password=admin123
spring.security.user.roles=ADMIN

Testing:

curl -u admin:admin123 http://localhost:8080/user

Limitations:

  • Only one user can be configured
  • No password encryption
  • Cannot configure URL-specific security
  • Not suitable for production

3. In-Memory Authentication

Configure multiple users in memory using Java configuration.

SecurityConfig.java:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/home", "/public/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .defaultSuccessUrl("/home")
                .permitAll()
            )
            .logout(logout -> logout
                .logoutSuccessUrl("/")
                .permitAll()
            );

        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
        UserDetails user = User.builder()
            .username("user")
            .password(passwordEncoder.encode("user123"))
            .roles("USER")
            .build();

        UserDetails admin = User.builder()
            .username("admin")
            .password(passwordEncoder.encode("admin123"))
            .roles("ADMIN", "USER")
            .build();

        UserDetails manager = User.builder()
            .username("manager")
            .password(passwordEncoder.encode("manager123"))
            .roles("MANAGER")
            .build();

        return new InMemoryUserDetailsManager(user, admin, manager);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

URL Access Control Explained:

.requestMatchers("/", "/home", "/public/**").permitAll()
// Everyone can access: /, /home, /public/anything

.requestMatchers(HttpMethod.GET, "/api/products").permitAll()
// Everyone can access: /api/products using only HTTP GET method

.requestMatchers("/admin/**").hasRole("ADMIN")
// Only ADMIN role: /admin/users, /admin/settings

.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
// USER or ADMIN role: /user/profile, /user/orders

.anyRequest().authenticated()
// All other URLs require authentication

Testing Different Users:

@RestController
public class TestController {

    @GetMapping("/")
    public String home() {
        return "Public Home Page";
    }

    @GetMapping("/user/profile")
    public String userProfile() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        return "User Profile: " + auth.getName();
    }

    @GetMapping("/admin/dashboard")
    public String adminDashboard() {
        return "Admin Dashboard";
    }
}

Access Control Results:

URLuseradminmanagerAnonymous
/βœ“βœ“βœ“βœ“
/user/profileβœ“βœ“βœ—βœ—
/admin/dashboardβœ—βœ“βœ—βœ—

4. JDBC Authentication

Store users in a database and authenticate against it.

Step 1: Database Schema

Spring Security provides default schema:

-- Users table
CREATE TABLE users (
    username VARCHAR(50) NOT NULL PRIMARY KEY,
    password VARCHAR(100) NOT NULL,
    enabled BOOLEAN NOT NULL
);

-- Authorities table
CREATE TABLE authorities (
    username VARCHAR(50) NOT NULL,
    authority VARCHAR(50) NOT NULL,
    FOREIGN KEY (username) REFERENCES users(username)
);

-- Optional: Create unique index
CREATE UNIQUE INDEX ix_auth_username ON authorities (username, authority);

Step 2: Insert Sample Data

-- Insert users (passwords are BCrypt encoded)
-- password for 'john' is 'john123'
INSERT INTO users (username, password, enabled)
VALUES ('john', '$2a$10$xjfHKf7d8YhKJF9fJK8f.eW5J3F5fJf8f.eW5J3F5fJf', true);

-- password for 'sarah' is 'sarah123'
INSERT INTO users (username, password, enabled)
VALUES ('sarah', '$2a$10$aK9fJK8f.eW5J3F5fJf8f.eW5J3F5fJf8f.eW5J3F5f', true);

-- Insert authorities
INSERT INTO authorities (username, authority) VALUES ('john', 'ROLE_USER');
INSERT INTO authorities (username, authority) VALUES ('sarah', 'ROLE_ADMIN');
INSERT INTO authorities (username, authority) VALUES ('sarah', 'ROLE_USER');

Step 3: Security Configuration

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private DataSource dataSource;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/home").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults())
            .logout(Customizer.withDefaults());

        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
        return manager;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Step 4: Custom Schema (Optional)

If you have your own user table structure:

CREATE TABLE app_users (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    email VARCHAR(100) NOT NULL UNIQUE,
    password VARCHAR(100) NOT NULL,
    full_name VARCHAR(100),
    active BOOLEAN DEFAULT true
);

CREATE TABLE user_roles (
    user_id BIGINT NOT NULL,
    role VARCHAR(50) NOT NULL,
    FOREIGN KEY (user_id) REFERENCES app_users(id)
);

Custom JDBC Configuration:

@Bean
public UserDetailsService userDetailsService() {
    JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);

    // Custom query to load user
    manager.setUsersByUsernameQuery(
        "SELECT email, password, active FROM app_users WHERE email = ?"
    );

    // Custom query to load authorities
    manager.setAuthoritiesByUsernameQuery(
        "SELECT u.email, r.role " +
        "FROM app_users u " +
        "JOIN user_roles r ON u.id = r.user_id " +
        "WHERE u.email = ?"
    );

    return manager;
}

5. Custom UserDetailsService

The most flexible approach - implement your own UserDetailsService with custom entity structure.

Step 1: Create User Entity

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String email;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String fullName;

    private boolean enabled = true;

    private boolean accountNonLocked = true;

    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
    @Column(name = "role")
    private Set<String> roles = new HashSet<>();

    @CreationTimestamp
    private Instant createdAt;

    @UpdateTimestamp
    private Instant updatedAt;

    // Constructors, getters, setters
}

Step 2: Create Repository

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
    boolean existsByEmail(String email);
}

Step 3: Implement Custom UserDetails

public class CustomUserDetails implements UserDetails {

    private final User user;

    public CustomUserDetails(User user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return user.getRoles().stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
            .collect(Collectors.toList());
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getEmail();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return user.isAccountNonLocked();
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return user.isEnabled();
    }

    // Additional methods to access User entity
    public String getFullName() {
        return user.getFullName();
    }

    public Long getId() {
        return user.getId();
    }
}

Step 4: Implement UserDetailsService

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    //constructor

    @Override
    public UserDetails loadUserByUsername(String email)
            throws UsernameNotFoundException {

        User user = userRepository.findByEmail(email)
            .orElseThrow(() -> new UsernameNotFoundException(
                "User not found with email: " + email));

        return new CustomUserDetails(user);
    }
}

Step 5: Security Configuration

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/register", "/css/**", "/js/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .loginProcessingUrl("/perform-login")
                .defaultSuccessUrl("/dashboard", true)
                .failureUrl("/login?error=true")
                .usernameParameter("email")  // Custom field name
                .passwordParameter("password")
                .permitAll()
            )
            .logout(logout -> logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout=true")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
                .permitAll()
            )
            .exceptionHandling(ex -> ex
                .accessDeniedPage("/access-denied")
            );

        return http.build();
    }
}

Step 6: User Registration Service

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    public User registerNewUser(UserRegistrationDto dto) {
        // Check if user already exists
        if (userRepository.existsByEmail(dto.getEmail())) {
            throw new RuntimeException("Email already exists");
        }

        // Create new user
        User user = new User();
        user.setEmail(dto.getEmail());
        user.setPassword(passwordEncoder.encode(dto.getPassword()));
        user.setFullName(dto.getFullName());
        user.setEnabled(true);
        user.getRoles().add("USER");

        return userRepository.save(user);
    }

    public User getCurrentUser() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth == null || !auth.isAuthenticated()) {
            return null;
        }

        CustomUserDetails userDetails = (CustomUserDetails) auth.getPrincipal();
        return userRepository.findById(userDetails.getId())
            .orElse(null);
    }
}

Step 7: Controller Example

@Controller
public class AuthController {

    @Autowired
    private UserService userService;

    @GetMapping("/login")
    public String loginPage() {
        return "login";
    }

    @GetMapping("/register")
    public String registerPage(Model model) {
        model.addAttribute("user", new UserRegistrationDto());
        return "register";
    }

    @PostMapping("/register")
    public String registerUser(@Valid @ModelAttribute UserRegistrationDto dto,
                              BindingResult result) {
        if (result.hasErrors()) {
            return "register";
        }

        try {
            userService.registerNewUser(dto);
            return "redirect:/login?registered=true";
        } catch (Exception e) {
            result.rejectValue("email", "error.email", e.getMessage());
            return "register";
        }
    }

    @GetMapping("/dashboard")
    public String dashboard(Model model) {
        User user = userService.getCurrentUser();
        model.addAttribute("user", user);
        return "dashboard";
    }
}

Accessing Current User in Controllers:

@RestController
@RequestMapping("/api")
public class ApiController {

    // Method 1: Using SecurityContextHolder
    @GetMapping("/user/info")
    public Map<String, Object> getUserInfo() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        CustomUserDetails userDetails = (CustomUserDetails) auth.getPrincipal();

        return Map.of(
            "email", userDetails.getUsername(),
            "fullName", userDetails.getFullName(),
            "roles", userDetails.getAuthorities()
        );
    }

    // Method 2: Using @AuthenticationPrincipal annotation
    @GetMapping("/user/profile")
    public Map<String, Object> getProfile(
            @AuthenticationPrincipal CustomUserDetails userDetails) {

        return Map.of(
            "email", userDetails.getUsername(),
            "fullName", userDetails.getFullName()
        );
    }

    // Method 3: Using Authentication parameter
    @GetMapping("/user/details")
    public Map<String, Object> getDetails(Authentication authentication) {
        CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();

        return Map.of(
            "name", authentication.getName(),
            "authorities", authentication.getAuthorities()
        );
    }
}

6. Spring Security Logout

Spring Security provides built-in logout functionality.

Basic Logout Configuration:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .logout(logout -> logout
            .logoutUrl("/logout")                    // URL to trigger logout
            .logoutSuccessUrl("/login?logout=true")   // Redirect after logout
            .invalidateHttpSession(true)              // Invalidate session
            .deleteCookies("JSESSIONID")              // Delete cookies
            .clearAuthentication(true)                // Clear authentication
            .permitAll()
        );

    return http.build();
}

Logout Form (GET request):

<form action="/logout" method="post">
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
    <button type="submit">Logout</button>
</form>

Logout Link (using Thymeleaf):

<form th:action="@{/logout}" method="post">
    <button type="submit">Logout</button>
</form>

Custom Logout Handler:

@Component
public class CustomLogoutHandler implements LogoutHandler {

    @Override
    public void logout(HttpServletRequest request,
                      HttpServletResponse response,
                      Authentication authentication) {

        // Custom logout logic
        if (authentication != null) {
            String username = authentication.getName();
            System.out.println("User " + username + " logged out");

            // Log to database, send notification, etc.
        }
    }
}

Logout Best Practices:

  1. Always use POST for logout (prevent CSRF attacks)
  2. Invalidate session to free server resources
  3. Clear authentication from SecurityContext
  4. Delete cookies to remove client-side data
  5. Log logout events for audit trails
  6. Redirect appropriately based on user type

Method-Level Security

Method-level security allows you to protect individual methods in your application using annotations. This provides fine-grained access control at the service or controller layer.

Enabling Method Security

To use method-level security, you need to enable it in your configuration:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity  // Enable method-level security
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults());

        return http.build();
    }
}

Method Security Annotations

Spring Security provides several annotations for method-level security:

1. @PreAuthorize

Checks authorization before method execution. Most commonly used annotation.

Basic Example:

@Service
public class UserService {

    @PreAuthorize("hasRole('ADMIN')")
    public void deleteUser(Long userId) {
        // Only admins can delete users
        userRepository.deleteById(userId);
    }

    @PreAuthorize("hasRole('USER')")
    public User getUserProfile(Long userId) {
        // Only authenticated users with USER role can access
        return userRepository.findById(userId).orElseThrow();
    }

    @PreAuthorize("hasAnyRole('USER', 'ADMIN')")
    public List<User> getAllUsers() {
        // Users with either USER or ADMIN role can access
        return userRepository.findAll();
    }
}

Advanced Example with SpEL (Spring Expression Language):

@Service
public class PostService {

    @Autowired
    private PostRepository postRepository;

    // Allow access only to the post owner or admin
    @PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
    public void updatePost(Long postId, Long userId, String content) {
        Post post = postRepository.findById(postId).orElseThrow();
        post.setContent(content);
        postRepository.save(post);
    }

    // Check if user is owner of the post
    @PreAuthorize("@postSecurityService.isOwner(#postId, authentication.principal.id)")
    public void deletePost(Long postId) {
        postRepository.deleteById(postId);
    }

    // Multiple conditions
    @PreAuthorize("hasRole('PREMIUM_USER') and #userId == authentication.principal.id")
    public void accessPremiumContent(Long userId) {
        // Only premium users can access their own premium content
    }
}

Custom Security Service:

@Service("postSecurityService")
public class PostSecurityService {

    @Autowired
    private PostRepository postRepository;

    public boolean isOwner(Long postId, Long userId) {
        return postRepository.findById(postId)
            .map(post -> post.getUserId().equals(userId))
            .orElse(false);
    }

    public boolean canEdit(Long postId, Long userId) {
        Post post = postRepository.findById(postId).orElse(null);
        if (post == null) return false;

        // Owner can edit, or admin, or if post is in draft status
        return post.getUserId().equals(userId) ||
               post.getStatus().equals("DRAFT");
    }
}

2. @PostAuthorize

Checks authorization after method execution. Useful when you need to check the return value.

@Service
public class DocumentService {

    // Only allow if returned document belongs to current user
    @PostAuthorize("returnObject.userId == authentication.principal.id")
    public Document getDocument(Long documentId) {
        return documentRepository.findById(documentId).orElseThrow();
    }

    // Allow if user is admin or owns the returned document
    @PostAuthorize("hasRole('ADMIN') or returnObject.userId == authentication.principal.id")
    public Document getDocumentDetails(Long documentId) {
        return documentRepository.findById(documentId).orElseThrow();
    }
}

3. @PreFilter

Filters collection parameters before method execution.

@Service
public class BatchService {

    // Filter the list, keeping only items where userId matches authenticated user
    @PreFilter("filterObject.userId == authentication.principal.id")
    public void processOrders(List<Order> orders) {
        // Only process orders that belong to the current user
        orders.forEach(order -> orderRepository.save(order));
    }
}

4. @PostFilter

Filters collection return values after method execution.

@Service
public class PostService {

    // Filter results to only show posts from user's friends or public posts
    @PostFilter("filterObject.isPublic == true or filterObject.userId == authentication.principal.id")
    public List<Post> getAllPosts() {
        return postRepository.findAll();
    }

    // Admin sees all, users see only their own
    @PostFilter("hasRole('ADMIN') or filterObject.userId == authentication.principal.id")
    public List<Order> getAllOrders() {
        return orderRepository.findAll();
    }
}

Performance Note: @PostFilter loads all records then filters in memory. For large datasets, filter at the database level instead.

SpEL Expressions in Method Security

Spring Expression Language (SpEL) provides powerful expressions for complex security rules:

Common SpEL Variables:

VariableDescriptionExample
authenticationCurrent Authentication objectauthentication.principal.id
principalCurrent user principalprincipal.username
#paramNameMethod parameter#userId == principal.id
returnObjectMethod return value (@PostAuthorize)returnObject.userId == principal.id
filterObjectCurrent item in collection (filters)filterObject.ownerId == principal.id

SpEL Examples:

@Service
public class SecurityExamplesService {

    // Check if parameter matches current user
    @PreAuthorize("#userId == authentication.principal.id")
    public void updateProfile(Long userId, ProfileDto dto) { }

    // Check if user has specific authority
    @PreAuthorize("hasAuthority('WRITE_PRIVILEGE')")
    public void writeData() { }

    // Multiple conditions with AND
    @PreAuthorize("hasRole('USER') and #userId == authentication.principal.id")
    public void updateUserData(Long userId) { }

    // Multiple conditions with OR
    @PreAuthorize("hasRole('ADMIN') or #ownerId == authentication.principal.id")
    public void deleteResource(Long ownerId) { }

    // Check authentication status
    @PreAuthorize("isAuthenticated()")
    public void authenticatedOnly() { }

    // Allow anonymous access
    @PreAuthorize("permitAll()")
    public void publicAccess() { }

    // Call custom bean method
    @PreAuthorize("@customSecurityService.hasAccess(#resourceId, authentication.principal.id)")
    public void checkCustomAccess(Long resourceId) { }
}

Method Security with Custom Annotations

Create custom security annotations for reusability:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
public @interface IsAdmin {
}

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
public @interface IsOwnerOrAdmin {
}

// Usage
@Service
public class CustomAnnotationService {

    @IsAdmin
    public void adminOnlyMethod() {
        // Only admins can access
    }

    @IsOwnerOrAdmin
    public void ownerOrAdminMethod(Long userId) {
        // Owner or admin can access
    }
}

Exception Handling

When authorization fails, Spring Security throws AccessDeniedException. Handle it with a global exception handler:

@RestControllerAdvice
public class SecurityExceptionHandler {

    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<ErrorResponse> handleAccessDenied(AccessDeniedException ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.FORBIDDEN.value(),
            "Access Denied",
            "You don't have permission to access this resource"
        );
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error);
    }

    @ExceptionHandler(AuthenticationException.class)
    public ResponseEntity<ErrorResponse> handleAuthentication(AuthenticationException ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.UNAUTHORIZED.value(),
            "Authentication Failed",
            ex.getMessage()
        );
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
    }
}

Next Steps: Once you’re comfortable with these fundamentals, explore JWT authentication for REST APIs, OAuth2 for social login, and Spring Security with microservices.

Comments

Join the discussion and share your thoughts