Security Filter Chain¶
Understanding Spring Security's filter chain is crucial for implementing custom authentication flows. This guide explores the filter chain architecture and custom filter integration in our reference project.
🔗 Filter Chain Architecture¶
Spring Security Filter Execution Order¶
graph TD
A[HTTP Request] --> B[SecurityContextPersistenceFilter]
B --> C[JwtAuthenticationFilter - CUSTOM]
C --> D[UsernamePasswordAuthenticationFilter]
D --> E[BasicAuthenticationFilter]
E --> F[AuthorizationFilter]
F --> G[ExceptionTranslationFilter]
G --> H[FilterSecurityInterceptor]
H --> I[Controller]
B -.-> J[Load SecurityContext]
C -.-> K[Validate JWT tokens]
D -.-> L[Process login forms]
E -.-> M[Handle Basic auth headers]
F -.-> N[Check permissions]
G -.-> O[Handle security exceptions]
Filter Chain Configuration¶
Our security configuration in MultiAuthSecurityConfig orchestrates multiple filters to support different authentication methods:
@Bean
@Profile("!oauth2-only & !jdbc-only & !ldap-only")
public SecurityFilterChain defaultFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authz -> authz
// H2 console endpoints (for development)
.requestMatchers(new AntPathRequestMatcher("/h2-console/**")).permitAll()
// Public endpoints
.requestMatchers(new AntPathRequestMatcher("/api/public/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/api/auth/**")).permitAll()
// Role-based endpoints
.requestMatchers(new AntPathRequestMatcher("/api/admin/**")).hasRole("ADMIN")
.requestMatchers(new AntPathRequestMatcher("/api/user/**")).hasAnyRole("USER", "ADMIN")
.requestMatchers(new AntPathRequestMatcher("/api/jdbc/**")).hasAnyRole("USER", "ADMIN")
.requestMatchers(new AntPathRequestMatcher("/api/ldap/**")).hasAnyRole("USER", "ADMIN")
.requestMatchers(new AntPathRequestMatcher("/actuator/health")).permitAll()
.anyRequest().authenticated()
)
// Add authentication providers
.authenticationProvider(customAuthenticationProvider);
// Conditionally add JDBC/LDAP providers if available
if (jdbcAuthenticationProvider != null) {
http.authenticationProvider(jdbcAuthenticationProvider);
}
if (ldapAuthenticationProvider != null) {
http.authenticationProvider(ldapAuthenticationProvider);
}
// JWT Filter positioned BEFORE UsernamePasswordAuthenticationFilter
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
🎯 Custom JWT Authentication Filter¶
Implementation Details¶
The JwtAuthenticationFilter in common-auth module extracts and validates JWT tokens:
package com.example.spring.security.reference.commonauth;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String header = request.getHeader("Authorization");
String jwtToken = null;
String username = null;
// Extract JWT token from Authorization header
if (header != null && header.startsWith("Bearer ")) {
jwtToken = header.substring(7);
try {
Claims claims = jwtTokenUtil.getClaimsFromToken(jwtToken);
username = claims.getSubject();
String role = claims.get("role", String.class);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// Create authorities from role
List<SimpleGrantedAuthority> authorities = List.of(
new SimpleGrantedAuthority(role)
);
// Create authentication token
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(username, null, authorities);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// Set authentication in SecurityContext
SecurityContextHolder.getContext().setAuthentication(authToken);
}
} catch (Exception e) {
// Invalid token - continue without authentication
logger.debug("Invalid JWT token: " + e.getMessage());
}
}
// Continue filter chain
chain.doFilter(request, response);
}
}
Filter Positioning Strategy¶
// JWT filter runs BEFORE form-based authentication
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
Why this positioning?
| Reason | Explanation |
|---|---|
| Priority | JWT tokens should be processed before attempting form-based authentication |
| Stateless | Allows stateless JWT authentication to take precedence |
| Fallback | Enables fallback to other authentication methods if JWT is invalid/missing |
🛡️ Filter Chain Execution Flow¶
Successful JWT Authentication¶
sequenceDiagram
participant C as Client
participant F as JwtAuthenticationFilter
participant S as SecurityContextHolder
participant A as AuthorizationFilter
participant R as ApiController
C->>F: Request with Authorization: Bearer <token>
F->>F: Extract and validate JWT token
F->>F: Extract username and role from claims
F->>S: Set Authentication in SecurityContext
F->>A: Continue filter chain
A->>A: Check user permissions
A->>R: Forward to controller
R->>C: Response
Request Without JWT (Fallback)¶
sequenceDiagram
participant C as Client
participant F as JwtAuthenticationFilter
participant U as UsernamePasswordAuthenticationFilter
participant P as CustomAuthenticationProvider
participant R as ApiController
C->>F: Request without JWT token
F->>F: No Authorization header found
F->>U: Continue to next filter
U->>P: Attempt other authentication
P-->>U: Authentication result
U->>R: Forward if authenticated
R->>C: Response (or 401/403)
🔐 JwtTokenUtil - Token Generation¶
The JwtTokenUtil class uses secure key generation:
@Component
public class JwtTokenUtil {
// Secure 512-bit key for HS512 algorithm
private static final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS512);
private static final long EXPIRATION_TIME = 86400000; // 24 hours
public String generateToken(String username, String role) {
return Jwts.builder()
.setSubject(username)
.claim("role", role)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
}
public Claims getClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
}
}
Key Regeneration
The secret key is regenerated on each application restart, invalidating all previously issued tokens. For production, use a persistent key.
🎓 Learning Points¶
| Concept | Description |
|---|---|
| OncePerRequestFilter | Ensures filter executes only once per request |
| SecurityContextHolder | Thread-local storage for authentication information |
| Filter Order | Critical for correct authentication flow |
| Stateless Sessions | No server-side session storage with JWT |