Spring Security
Authentication, authorisation, JWT, OAuth2, method-level security
Spring Security is a powerful, highly customisable authentication and authorisation framework. It integrates with Spring Boot via auto-configuration and supports HTTP Basic, form login, JWT, OAuth2, SAML, and more. Understanding the security filter chain is key to configuring and debugging Spring Security correctly.
Key Points
- Security Filter Chain: Spring Security installs a chain of servlet filters — each request passes through them in order
- DelegatingFilterProxy → FilterChainProxy → SecurityFilterChain: the entry points from servlet container to Spring Security
- Authentication: verifying who you are — UsernamePasswordAuthenticationToken, JwtAuthenticationToken
- AuthenticationManager → AuthenticationProvider: pluggable authentication — DaoAuthenticationProvider for DB, JwtDecoder for tokens
- SecurityContextHolder: ThreadLocal that holds the Authentication for the current request — clear after use
- Authorization: what you can do — @PreAuthorize("hasRole('ADMIN')"), @Secured, or HttpSecurity.authorizeHttpRequests()
- JWT flow: client sends Bearer token → JwtAuthenticationFilter extracts and validates → Spring sets Authentication in context
- OAuth2 Resource Server: spring-boot-starter-oauth2-resource-server + .oauth2ResourceServer(oauth2 -> oauth2.jwt()) validates JWTs automatically
- CSRF: enabled by default for browser clients; disable for stateless REST APIs (JWT) — http.csrf(AbstractHttpConfigurer::disable)
Spring Security: stateless JWT config, oauth2ResourceServer, @PreAuthorize method security, BCrypt password encoder
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // enables @PreAuthorize
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable) // stateless API
.sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // public
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtConverter()))
)
.build();
}
// Extract roles from JWT claim "roles"
@Bean
public JwtAuthenticationConverter jwtConverter() {
var converter = new JwtGrantedAuthoritiesConverter();
converter.setAuthoritiesClaimName("roles");
converter.setAuthorityPrefix("ROLE_");
var jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(converter);
return jwtConverter;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
}
// Method-level security
@Service
public class ReportService {
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.name")
public Report getReport(String userId) { ... }
@PreAuthorize("hasAuthority('report:write')")
public void publishReport(Report r) { ... }
}
// Custom JWT claim extraction
@Component
public class JwtService {
public String generateToken(UserDetails user) {
return Jwts.builder()
.subject(user.getUsername())
.claim("roles", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + 86_400_000))
.signWith(signingKey)
.compact();
}
}Real-World Example
The most common Spring Security mistake is accidentally exposing all endpoints. The default HttpSecurity in Spring Boot 3 requires authentication for all endpoints — good. But adding .authorizeHttpRequests().anyRequest().permitAll() during development and forgetting to remove it is a critical security gap. Always review SecurityFilterChain configuration before deployment, and write integration tests for secured endpoints.