Stateless APIs don't keep server-side sessions. Instead, each request carries a signed JWT that proves who the caller is. Spring Security makes this pattern straightforward once you understand the filter chain.
The security configuration
In modern Spring Security you define a SecurityFilterChain bean rather than
extending a base class.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthFilter jwtAuthFilter;
public SecurityConfig(JwtAuthFilter jwtAuthFilter) {
this.jwtAuthFilter = jwtAuthFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}Validating the token
A custom filter runs once per request. It extracts the bearer token, verifies the signature, and populates the security context.
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtService jwt;
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws ServletException, IOException {
String header = req.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
if (jwt.isValid(token)) {
var auth = jwt.toAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
chain.doFilter(req, res);
}
}Signing keys matter
Use a strong secret (256 bits or more for HS256) and keep it out of source control. Rotate keys periodically and reject tokens whose expiry has passed.
Summary
- Configure a stateless
SecurityFilterChain. - Add a
OncePerRequestFilterto validate JWTs. - Guard your signing key and always check expiration.
This gives you scalable, session-free authentication that works well behind load balancers.