JwtFilter.java
package com.stocks.stockease.security;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* JWT token extraction and validation filter.
*
* Spring Security filter that runs once per request. Extracts JWT token from
* Authorization header, validates signature/expiration, loads user details,
* and populates SecurityContext for authorization checks on protected endpoints.
*
* Filter chain position: Early in chain (before controller execution).
* Bypass: Public endpoints (login, health checks) via SecurityConfig.
* Disabled in 'docs' profile for CI/CD documentation generation.
*
* @author Team StockEase
* @version 1.0
* @since 2025-01-01
*/
@Component
@org.springframework.context.annotation.Profile("!docs")
public class JwtFilter extends OncePerRequestFilter {
/**
* JWT utility for token validation and claims extraction.
*/
private final JwtUtil jwtUtil;
/**
* Spring Security UserDetailsService for loading user authorities.
* Implementation: CustomUserDetailsService (loads from UserRepository).
*/
private final UserDetailsService userDetailsService;
/**
* Constructs filter with dependencies.
*
* @param jwtUtil JWT operations utility
* @param userDetailsService loads user details for authentication
*/
public JwtFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}
/**
* Processes request to validate JWT and populate security context.
*
* Flow:
* 1. Extract "Authorization: Bearer <token>" header
* 2. Validate token signature and expiration
* 3. Extract username and role from token claims
* 4. Load user details (authorities/roles) from database
* 5. Create UsernamePasswordAuthenticationToken
* 6. Store in SecurityContext for downstream @PreAuthorize checks
* 7. Continue filter chain
*
* If no token or validation fails, request continues without authentication
* (handled by explicit @PreAuthorize or global security config).
*
* @param request HTTP request with optional Authorization header
* @param response HTTP response
* @param filterChain next filter in chain
* @throws java.io.IOException if I/O error
* @throws jakarta.servlet.ServletException if servlet error
*/
@Override
protected void doFilterInternal(
@NonNull jakarta.servlet.http.HttpServletRequest request,
@NonNull jakarta.servlet.http.HttpServletResponse response,
@NonNull jakarta.servlet.FilterChain filterChain
) throws java.io.IOException, jakarta.servlet.ServletException {
// Extract Authorization header (format: "Bearer <token>")
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
// Extract token by removing "Bearer " prefix (7 characters)
String token = authHeader.substring(7);
// Validate token: signature verification and expiration check
if (jwtUtil.validateToken(token)) {
// Extract username from JWT "sub" claim
String username = jwtUtil.extractUsername(token);
// Extract role from JWT custom "role" claim
String role = jwtUtil.extractRole(token);
// DEBUG: Log extracted role for troubleshooting (remove in production for performance)
System.out.println("Token role: " + role);
// Load user details from database (including authorities for @PreAuthorize)
// CustomUserDetailsService maps role to Spring Security authorities (ROLE_ADMIN, ROLE_USER)
var userDetails = userDetailsService.loadUserByUsername(username);
// Create authentication token with user details and authorities
// No credentials in token (already authenticated by JWT signature)
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
// Store authentication in SecurityContext (available to @PreAuthorize, etc.)
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
// Continue filter chain regardless of authentication success
// Unauthenticated requests will be rejected by @PreAuthorize on protected endpoints
filterChain.doFilter(request, response);
}
}