Skip to main content

Spring Security: Complete Architecture Guide

Let me break down Spring Security's architecture from a bird's eye view to the implementation details in your codebase.


๐ŸŒ Bird's Eye View: Spring Security Architectureโ€‹

HTTP Request
โ†“
Filter Chain (Servlet Filters)
โ”œโ”€ CORS Filter
โ”œโ”€ JWT Authentication Filter โ† Your custom filter
โ”œโ”€ UsernamePasswordAuthenticationFilter
โ”œโ”€ Exception Translation Filter
โ””โ”€ Authorization Filter
โ†“
SecurityContext (Thread-Local Storage)
โ””โ”€ Authentication Object
โ”œโ”€ Principal (UserDetails)
โ”œโ”€ Credentials (password)
โ””โ”€ Authorities (roles/permissions)
โ†“
Your Controller
โ””โ”€ Access SecurityContextHolder to get current user

๐Ÿ”‘ Core Concepts & Relationshipsโ€‹

1. UserDetails Interfaceโ€‹

Location: org.springframework.security.core.userdetails.UserDetails

public interface UserDetails {
String getUsername(); // Unique identifier
String getPassword(); // Credentials
Collection<? extends GrantedAuthority> getAuthorities(); // Roles/permissions
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}

Purpose: Represents the user information that Spring Security needs for authentication and authorization.

In Your Code (User.java:15):

@Entity
public class User implements UserDetails {
private Long id;
private String email;
private String password;
private Set<Role> roles;

@Override
public String getUsername() {
return email; // Your email is the username
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.name()))
.collect(Collectors.toList());
}
// ... other UserDetails methods
}

2. Authentication Interfaceโ€‹

Location: org.springframework.security.core.Authentication

public interface Authentication extends Principal {
Collection<? extends GrantedAuthority> getAuthorities(); // User roles
Object getCredentials(); // Password (usually null after auth)
Object getDetails(); // Additional details (IP, session, etc.)
Object getPrincipal(); // The user (UserDetails object)
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated);
}

Purpose: Represents an authentication request or an authenticated user.

Common Implementation: UsernamePasswordAuthenticationToken

In Your Code (JwtAuthenticationFilter.java:64-66):

UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, // โ† Principal (your User object)
null, // โ† Credentials (null after authentication)
userDetails.getAuthorities() // โ† Authorities (roles)
);

3. Principal Interfaceโ€‹

Location: java.security.Principal

public interface Principal {
String getName(); // Returns the name of the principal
}

Purpose: Generic Java interface representing an identity. In Spring Security, the principal is typically the UserDetails object.

Relationship:

Authentication
โ””โ”€ getPrincipal() โ†’ Object (usually UserDetails)
โ””โ”€ In your case: User object (which implements UserDetails)

Extracting Principal:

// From Authentication object
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Object principal = auth.getPrincipal();

// principal can be:
// 1. UserDetails (your User object) - when authenticated
// 2. String (username) - when not fully authenticated
// 3. AnonymousAuthenticationToken - for anonymous users

๐Ÿ”„ Complete Authentication Flowโ€‹

Flow Diagram:โ€‹

1. Client Request
โ””โ”€ Headers: { Authorization: "Bearer eyJhbGc..." }
โ†“
2. Servlet Container
โ””โ”€ Creates HttpServletRequest
โ†“
3. Filter Chain Starts
โ”œโ”€ CORS Filter (handles cross-origin)
โ”œโ”€ YOUR JWT Filter โ† We are here!
โ”‚ โ”‚
โ”‚ โ”œโ”€ Extract JWT from header
โ”‚ โ”œโ”€ Validate token
โ”‚ โ”œโ”€ Extract username from token
โ”‚ โ”œโ”€ Load UserDetails from database
โ”‚ โ””โ”€ Create Authentication object
โ”‚ โ””โ”€ Store in SecurityContext
โ”‚
โ”œโ”€ Other Security Filters
โ””โ”€ Authorization Filter (checks permissions)
โ†“
4. SecurityContext (Thread-Local)
โ””โ”€ Contains Authentication object
โ””โ”€ principal: User object
โ””โ”€ authorities: [ROLE_USER, ROLE_ADMIN]
โ†“
5. Your Controller
โ””โ”€ Can access current user via SecurityContextHolder
โ†“
6. Response

๐Ÿงต Thread-Local Security Contextโ€‹

What is ThreadLocal?โ€‹

Spring Security uses ThreadLocal to store the authentication for each HTTP request thread.

// Simplified version of SecurityContextHolder
public class SecurityContextHolder {
private static ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();

public static SecurityContext getContext() {
return contextHolder.get(); // Gets context for CURRENT thread only
}

public static void setContext(SecurityContext context) {
contextHolder.set(context); // Sets for CURRENT thread only
}
}

Why ThreadLocal?

  • Each HTTP request runs in its own thread
  • ThreadLocal ensures each thread has its own isolated SecurityContext
  • Request A's authentication cannot interfere with Request B's authentication

๐Ÿ“ Step-by-Step: Your JWT Authentication Flowโ€‹

Let's trace a real request through your system:

Request: GET /api/cart with Authorization: Bearer <jwt_token>

Step 1: JwtAuthenticationFilter Interceptsโ€‹

File: JwtAuthenticationFilter.java:34-77

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

// 1. Extract JWT token from header
final String requestTokenHeader = request.getHeader("Authorization");
String jwtToken = null;
String username = null;

if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7); // Remove "Bearer "
username = jwtUtil.getUsernameFromToken(jwtToken); // Extract email from JWT
}

// 2. Check if already authenticated
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

// 3. Load user from database
UserDetails userDetails = userService.loadUserByUsername(username);
// โ†‘ This returns your User object (which implements UserDetails)

// 4. Validate token
if (jwtUtil.validateToken(jwtToken, userDetails)) {

// 5. Create Authentication object
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, // โ† Principal (your User)
null, // โ† Credentials (null for JWT)
userDetails.getAuthorities() // โ† Roles [ROLE_USER]
);

// 6. Add request details (IP, session ID, etc.)
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);

// 7. Store in SecurityContext (ThreadLocal)
SecurityContextHolder.getContext().setAuthentication(authToken);
// โ†‘
// This is now available
// in your controller!
}
}

// 8. Continue filter chain
filterChain.doFilter(request, response);
}

Step 2: Authorization Filter Checks Permissionsโ€‹

File: SecurityConfig.java:77

.requestMatchers(AntPathRequestMatcher.antMatcher("/api/cart/**"))
.hasAnyRole("USER", "ADMIN") // โ† Checks authorities in Authentication

Spring Security automatically:

  1. Gets Authentication from SecurityContext
  2. Calls authentication.getAuthorities()
  3. Checks if authorities contain ROLE_USER or ROLE_ADMIN
  4. Allows/denies access

Step 3: Your Controller Accesses Current Userโ€‹

@GetMapping("/cart")
public ResponseEntity<CartDto> getCart(Authentication authentication) {
// โ†‘ Spring automatically injects this!

// Extract principal
Object principal = authentication.getPrincipal();

if (principal instanceof User user) {
Long userId = user.getId(); // Direct access to your User entity
// ... fetch cart ...
}
}

๐Ÿ—๏ธ Class Hierarchy & Interface Relationshipsโ€‹

java.security.Principal (interface)
โ†‘
org.springframework.security.core.Authentication (interface)
โ†‘
AbstractAuthenticationToken (abstract class)
โ†‘
UsernamePasswordAuthenticationToken (class) โ† You use this
โ””โ”€ principal: Object (your User object)
โ””โ”€ credentials: Object (password or null)
โ””โ”€ authorities: Collection<GrantedAuthority>


org.springframework.security.core.userdetails.UserDetails (interface)
โ†‘
com.automotive.ecommerce.business.entity.User (your class) โ† Your implementation
โ””โ”€ implements all UserDetails methods
โ””โ”€ adds custom fields (id, email, firstName, etc.)

๐ŸŽฏ How UserDetails is Extracted from Authenticationโ€‹

Method 1: Direct Cast (Type-Safe)โ€‹

Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Object principal = auth.getPrincipal();

if (principal instanceof User user) {
// โœ… Type-safe access to your User entity
Long userId = user.getId();
String email = user.getEmail();
}

Method 2: Generic UserDetails Castโ€‹

if (principal instanceof UserDetails userDetails) {
// โœ… Access to UserDetails methods only
String username = userDetails.getUsername();
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();

// โŒ Cannot access User-specific fields like getId()
// Long userId = userDetails.getId(); // Compile error!
}

Method 3: Spring's @AuthenticationPrincipalโ€‹

@GetMapping("/cart")
public ResponseEntity<CartDto> getCart(@AuthenticationPrincipal User user) {
// โ†‘ Spring automatically extracts and casts
Long userId = user.getId(); // Direct access!
}

๐Ÿ” SecurityContext & SecurityContextHolderโ€‹

Structure:โ€‹

SecurityContextHolder (Thread-Local Storage)
โ””โ”€ SecurityContext
โ””โ”€ Authentication
โ”œโ”€ principal: UserDetails (your User)
โ”œโ”€ credentials: null
โ”œโ”€ authorities: [ROLE_USER, ROLE_ADMIN]
โ”œโ”€ details: WebAuthenticationDetails (IP, session)
โ””โ”€ authenticated: true

Accessing Current User Anywhere:โ€‹

// In any service, controller, or component
Authentication auth = SecurityContextHolder.getContext().getAuthentication();

if (auth != null && auth.getPrincipal() instanceof User user) {
System.out.println("Current user: " + user.getEmail());
System.out.println("User ID: " + user.getId());
}

๐Ÿ“Š Summary: The Big Pictureโ€‹

ConceptPurposeExample in Your Code
UserDetailsContract for user information Spring Security needsYour User entity implements this
AuthenticationRepresents an authenticated user with credentials & authoritiesUsernamePasswordAuthenticationToken
PrincipalThe identity (usually UserDetails)Your User object stored in Authentication
SecurityContextContainer for AuthenticationThread-local storage per request
SecurityContextHolderStatic accessor for SecurityContextAccess current user anywhere
Filter ChainIntercepts requests to authenticateYour JwtAuthenticationFilter
AuthenticationManagerValidates credentialsUsed in login (AuthController)
UserDetailsServiceLoads user from databaseYour UserService.loadUserByUsername()

๐Ÿš€ Key Takeawaysโ€‹

  1. UserDetails = Your User entity with Spring Security requirements
  2. Authentication = Container holding the authenticated user (principal) + authorities
  3. Principal = The "who" (your User object) inside Authentication
  4. SecurityContext = Thread-local storage for Authentication (one per request thread)
  5. Filter Chain = Where authentication happens before reaching your controller
  6. JWT Flow = Extract token โ†’ Load user โ†’ Create Authentication โ†’ Store in SecurityContext

Next Steps: Now that you understand Spring Security's architecture, explore how it integrates with your backend scenarios in the Backend Scenarios guide.