Securing Actuator, protecting sensitive data across roles, authentication vs authorization, Spring Security 6 setup, securing microservices with OAuth2/JWT, configuring against common threats, JWT done safely, form login, rate limiting with Bucket4j, CORS, and WebSocket security.
Published September 25, 2026
Security answers must use current APIs. Spring Boot 3 ships Spring Security 6, where WebSecurityConfigurerAdapter and authorizeRequests() are gone. Show the SecurityFilterChain bean style, and pair each mechanism with the threat it stops.
Short answer:
health is exposed over HTTP by default. Add endpoints deliberately (management.endpoints.web.exposure.include=health,info,prometheus).management.server.port, bound to an internal network or blocked at the ingress.management.endpoint.health.show-details=when-authorized, and keep the default secret masking.heapdump or env publicly, because they leak secrets.@Bean
@Order(1)
SecurityFilterChain actuatorSecurity(HttpSecurity http) throws Exception {
return http.securityMatcher(EndpointRequest.toAnyEndpoint())
.authorizeHttpRequests(a -> a
.requestMatchers(EndpointRequest.to(HealthEndpoint.class, InfoEndpoint.class)).permitAll()
.anyRequest().hasRole("OPS"))
.httpBasic(Customizer.withDefaults())
.build();
}
Learn it in depth → Health Checks
Short answer: Layer the defences:
@PreAuthorize, including ownership checks ("users can read only their own records"), not just roles.@PreAuthorize("hasRole('SUPPORT') or #customerId == authentication.principal.customerId")
public CustomerProfile profile(long customerId) { … }
Short answer:
AttributeConverters and keys held in a KMS), and use row-level security or tenant filters where appropriate. Give the application a least-privilege DB user.Learn it in depth → Payment Security
Short answer: Authentication establishes who the caller is (a password, token or certificate). It produces an Authentication in the SecurityContext. Authorization decides what that caller may do, based on their granted authorities, and on rules evaluated by the AuthorizationManager at URL, method or domain-object level.
Key points to cover:
Learn it in depth → Authentication Mechanics
Short answer:
spring-boot-starter-security. Every endpoint is secured by default, with a generated password.SecurityFilterChain bean that sets the URL rules and the authentication mechanism.UserDetailsService (a database) or an external identity provider (OAuth2/OIDC).PasswordEncoder (BCrypt or Argon2, through DelegatingPasswordEncoder).@EnableMethodSecurity.@Configuration
@EnableMethodSecurity
class SecurityConfig {
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(a -> a
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.build();
}
@Bean PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); }
}
Common trap: describing extends WebSecurityConfigurerAdapter and configure(HttpSecurity). It was removed in Spring Security 6.
Learn it in depth → Spring Security Overview
Short answer:
Common trap: a single custom "auth service" that hand-rolls JWT signing. Use a standards-based identity provider.
Learn it in depth → Spring OAuth2 Basics
Short answer: Map each threat to a control:
| Threat | Control |
|---|---|
| Credential stuffing, brute force | Rate limiting on login, account lockout or backoff, MFA |
| Weak password storage | BCrypt or Argon2 via DelegatingPasswordEncoder |
| CSRF (cookie sessions) | Spring's CSRF protection, on by default (disable only for stateless token APIs) |
| Session fixation / hijacking | Session-ID rotation on login (the default), HttpOnly/Secure/SameSite cookies, timeouts |
| XSS, clickjacking | Security headers: Content-Security-Policy, X-Frame-Options, HSTS (http.headers(...)) |
| Broken access control | Deny by default (anyRequest().authenticated()), method-level checks, ownership checks |
| Sensitive data exposure | TLS, masking, no stack traces in responses |
Short answer: Let an identity provider issue the tokens, and configure the app as an OAuth2 resource server. Spring validates the signature (fetching keys from the issuer's JWKS endpoint), expiry, issuer and audience on every request, and maps scopes or roles to authorities. The service stays stateless.
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.com/realms/shop # discovery + JWKS
audiences: orders-api
Key points to cover:
localStorage in browser apps, because of XSS exposure. Prefer the backend-for-frontend pattern with HttpOnly session cookies.alg: none, and pin the expected algorithms.Learn it in depth → Spring JWT Authentication
Short answer (Spring Security 6):
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(a -> a
.requestMatchers("/", "/login", "/css/**").permitAll()
.requestMatchers("/account/**").authenticated()
.anyRequest().authenticated())
.formLogin(f -> f.loginPage("/login").defaultSuccessUrl("/account", true))
.logout(l -> l.logoutSuccessUrl("/"))
.build();
}
@Bean
UserDetailsService users(UserRepository repo) { // load users from the database
return username -> repo.findByEmail(username)
.map(u -> User.withUsername(u.getEmail()).password(u.getPasswordHash()).roles(u.getRole()).build())
.orElseThrow(() -> new UsernameNotFoundException(username));
}
Common trap: configure(AuthenticationManagerBuilder auth) and http.authorizeRequests(). Those are the removed Spring Security 5 API.
Short answer:
RequestRateLimiter (a Redis token bucket), NGINX/Envoy, or an API gateway service.Retry-After header.@Component
class RateLimitFilter extends OncePerRequestFilter {
private final ProxyManager<String> buckets; // Bucket4j + Redis
private final Supplier<BucketConfiguration> config = () -> BucketConfiguration.builder()
.addLimit(Bandwidth.builder().capacity(100).refillGreedy(100, Duration.ofMinutes(1)).build())
.build();
@Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws ServletException, IOException {
String key = "rl:" + apiKeyOf(req);
if (buckets.builder().build(key, config).tryConsume(1)) {
chain.doFilter(req, res);
} else {
res.setStatus(429);
res.setHeader("Retry-After", "60");
}
}
}
Learn it in depth → Design a Rate Limiter
Short answer: Allow exactly that origin, the methods and headers it needs, and credentials only if required. Configure it globally, and make sure Spring Security applies it too. Otherwise, preflight OPTIONS requests are rejected before MVC ever sees them.
@Bean
CorsConfigurationSource corsConfigurationSource() {
var cfg = new CorsConfiguration();
cfg.setAllowedOrigins(List.of("https://shop.example.com"));
cfg.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
cfg.setAllowedHeaders(List.of("Authorization", "Content-Type"));
cfg.setAllowCredentials(true);
cfg.setMaxAge(Duration.ofHours(1));
var source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", cfg);
return source;
}
// and in the SecurityFilterChain: http.cors(Customizer.withDefaults())
Key points to cover:
@CrossOrigin on controllers works for small cases.allowCredentials(true) with a wildcard origin (Spring rejects it). CORS is a browser protection, not a server-side access control.Short answer:
AuthenticationSuccessEvent and AbstractAuthenticationFailureEvent for you to record.Short answer:
Origin header (setAllowedOrigins).SUBSCRIBE and SEND destinations (Spring Security's message authorisation), not just the connection.wss:// only.@Bean
AuthorizationManager<Message<?>> messageAuthorization(MessageMatcherDelegatingAuthorizationManager.Builder m) {
return m.simpDestMatchers("/app/admin/**").hasRole("ADMIN")
.simpSubscribeDestMatchers("/user/queue/**").authenticated()
.anyMessage().denyAll()
.build();
}
Q: Why is CSRF protection usually disabled for REST APIs?
A: CSRF exploits the browser automatically attaching cookies. APIs authenticated with a bearer token in the Authorization header aren't vulnerable. Keep CSRF enabled for any cookie-based session authentication, including BFFs.
Q: hasRole('ADMIN') vs hasAuthority('ROLE_ADMIN')?
A: They're equivalent. hasRole adds the ROLE_ prefix automatically. Scopes from JWTs map to authorities such as SCOPE_orders.read, which you check with hasAuthority.
Q: How do you test security rules?
A: With spring-security-test: @WithMockUser, or jwt() request post-processors in MockMvc. Assert 401 for anonymous callers, 403 for wrong roles, and 200 for the right ones.
Q: How do you rotate JWT signing keys without downtime?
A: Publish the new key in the JWKS alongside the old one, start signing with the new key, and remove the old key after the longest token lifetime has passed. Resource servers pick up keys by kid automatically.