Intermediate · Secure
Production Implementation: Protected Task API
Ship the security slice end to end: stateless filter chain, bcrypt hashing, scoped authorities, and tests that pin every denied path.
Primary source: Spring Security 6.5 Reference Architecture
Lesson 5 of 5
1d streakThe Architectural Question
Security assembled from blog snippets fails closed unpredictably: wrong matcher order, sessions left on, and untested deny paths that regress the first time someone refactors.
What this guide proves
- Declare one lambda-style SecurityFilterChain with ordered matchers
- Hash passwords with a delegating bcrypt encoder
- Prove 401 versus 403 paths in MockMvc
Defense-in-Depth Architecture: Multi-Layered API Security
This is the security integration capstone. Apply the earlier lessons on filter-chain ordering, authentication, CORS, CSRF, OAuth2, and method authorization to one production boundary. The goal is to verify that the controls compose correctly across layers, not to redefine each mechanism.
At the servlet boundary, DelegatingFilterProxy delegates to the Spring-managed FilterChainProxy. FilterChainProxy selects the matching SecurityFilterChain, which runs the authentication and authorization filters used by this configuration.
http
.requiresChannel(channel -> channel.anyRequest().requiresSecure()) // Enforce HTTPS
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'"))
.httpStrictTransportSecurity(hsts -> hsts.includeSubDomains(true).maxAgeInSeconds(31536000))
.frameOptions(frame -> frame.deny())
)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// Safe only when the API accepts credentials exclusively in Authorization headers,
// never from cookies or other browser-attached credentials.
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health/**").permitAll()
.requestMatchers("/api/v1/public/**").permitAll()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()));Stateless does not decide CSRF by itself
Active Recall Checkpoint
What security attack does frameOptions(frame -> frame.deny()) prevent?
Hint: What attack involves rendering your website inside a transparent iframe?
Show answer & explanation
Clickjacking attacks. Denying frames ensures that malicious external websites cannot embed your application inside an invisible iframe to trick users into clicking buttons.
Audit Logging & Tenancy Injection via SecurityContextHolder
Security auditing requires recording the authenticated user ID and tenant ID on every database modification. Spring Data JPA provides auditing integration via AuditorAware.
package com.example.security;
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class SpringSecurityAuditorAware implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !auth.isAuthenticated() || "anonymousUser".equals(auth.getPrincipal())) {
return Optional.of("system");
}
return Optional.ofNullable(auth.getName());
}
}Active Recall Checkpoint
Why should @EnableJpaAuditing be used with AuditorAware in enterprise applications?
Hint: How does it automate populating @CreatedBy and @LastModifiedBy columns?
Show answer & explanation
It automatically populates @CreatedBy and @LastModifiedBy audit fields on JPA entities using the authenticated principal from SecurityContextHolder, eliminating manual boilerplate and preventing audit tampering.
The Automated Security Test Matrix
A production security implementation is incomplete without an automated test matrix that proves every route enforces authentication, roles, and boundaries.
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
@SpringBootTest
@AutoConfigureMockMvc
class TaskSecurityIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void unauthenticatedRequest_Returns401() throws Exception {
mockMvc.perform(get("/api/v1/tasks/123"))
.andExpect(status().isUnauthorized());
}
@Test
void userRole_CanAccessTasks() throws Exception {
mockMvc.perform(get("/api/v1/tasks/123")
.with(jwt().authorities(new SimpleGrantedAuthority("ROLE_USER"))))
.andExpect(status().isOk());
}
@Test
void userRole_CannotAccessAdminEndpoint() throws Exception {
mockMvc.perform(delete("/api/v1/admin/tasks/123")
.with(jwt().authorities(new SimpleGrantedAuthority("ROLE_USER"))))
.andExpect(status().isForbidden());
}
}Active Recall Checkpoint
What is the semantic HTTP difference between 401 Unauthorized and 403 Forbidden in Spring Security?
Hint: Is the client authenticated, or do they lack permissions?
Show answer & explanation
401 Unauthorized indicates unauthenticated access (no credentials or invalid token supplied). 403 Forbidden indicates the client is authenticated, but lacks the necessary permissions or roles to execute the operation.
Deliberate Failure Lab: Detecting Silent Authorization Leaks
A critical vulnerability occurs when a new controller endpoint is added without updating the security filter chain or method security annotations. If .anyRequest().permitAll() was mistakenly used instead of .anyRequest().authenticated(), the new endpoint is silently open to the public internet.
The Strict Security Posture
.anyRequest().denyAll() or .anyRequest().authenticated(). Any unmapped route must fail closed rather than failing open.Active Recall Checkpoint
Why is "Default Deny" a safer baseline posture than "Default Allow"?
Hint: What happens when a developer adds a new internal API endpoint and forgets to configure authorization?
Show answer & explanation
Under "Default Deny", a forgotten configuration blocks access until explicitly permitted. Under "Default Allow", a forgotten configuration exposes sensitive internal endpoints to the entire internet.
Working Implementation
Code Walkthrough
Secure the task API with JWT resource-server validation and an authorization matrix no test can silently regress.
package com.example.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter;
@Configuration
@EnableMethodSecurity
public class ProtectedTaskApiSecurityLab {
@Bean
SecurityFilterChain productionFilterChain(HttpSecurity http) throws Exception {
return http
.requiresChannel(channel -> channel.anyRequest().requiresSecure()) // Enforce HTTPS
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'"))
.frameOptions(frame -> frame.deny())
.referrerPolicy(ref -> ref.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN))
)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.csrf(csrf -> csrf.disable())
.cors(Customizer.withDefaults())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/tasks/**").hasAnyRole("USER", "ADMIN")
.requestMatchers(HttpMethod.POST, "/api/v1/tasks/**").hasRole("ADMIN")
.anyRequest().denyAll()
)
.oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()))
.build();
}
}Core Spring Implementation
Protected Task API security slice
Ship JWT-backed API security: a stateless SecurityFilterChain, bcrypt password hashing, scoped authorization rules, and tests that pin every denied path.
Component Boundaries
- 01SecurityFilterChain filters run before DispatcherServlet, so rejected requests never reach a controller.
- 02The oauth2-resource-server starter validates JWT signature, issuer, audience, and exp on every request; HTTP sessions stay off.
- 03PasswordEncoderFactories emits {bcrypt} hashes with per-hash salts; raw passwords are compared once and never stored or logged.
Required Evidence
- Token contract recorded: issuer, audience, clock skew, and required authorities such as task:create.
- 401 and 403 are asserted as distinct test outcomes, never merged into one catch-all case.
- Database audit shows every credential column holding a {bcrypt}-prefixed hash and no reversible values.
Implementation sequence
Step-by-Step Integration
Follow these structured steps to inject Core Spring functionality cleanly across your boundaries.
Declare a stateless SecurityFilterChain
SecurityConfiguration.java
WebSecurityConfigurerAdapter is gone in Spring Security 6; the lambda-style SecurityFilterChain bean is the supported configuration, and disabling CSRF fits token-bearing APIs that accept no cookies.
@Configuration
@EnableMethodSecurity
class SecurityConfiguration {
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.requestMatchers(HttpMethod.POST, "/api/tasks").hasAuthority("task:create")
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.build();
}
}Verify Configuration
curl -i -X POST http://localhost:8080/api/tasks with no token prints HTTP/1.1 401; the same call with a token lacking task:create prints 403; GET http://localhost:8080/actuator/health without a token still returns 200 {"status":"UP"}.
Store passwords behind the delegating bcrypt encoder
PasswordConfiguration.java
Encryption is reversible and unsalted MD5 or SHA-256 falls to commodity GPUs; bcrypt carries a salt and cost factor per hash, and the delegating encoder lets you migrate algorithms without breaking stored values.
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}Verify Configuration
SELECT password FROM users LIMIT 1 returns a value beginning with {bcrypt}$2a$; searching application logs for the submitted plaintext password returns no matches.
Pin the authorization matrix in MockMvc
TaskSecurityTest.java
Happy-path-only suites hide broken deny rules; unauthenticated, unauthorized, and permitted requests must each assert their own status code.
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
@SpringBootTest
@AutoConfigureMockMvc
class TaskSecurityTest {
@Autowired MockMvc mvc;
@Test
void rejects_missing_token() throws Exception {
mvc.perform(post("/api/tasks")).andExpect(status().isUnauthorized());
}
@Test
void rejects_wrong_authority() throws Exception {
mvc.perform(post("/api/tasks").with(jwt().authorities(new SimpleGrantedAuthority("task:read"))))
.andExpect(status().isForbidden());
}
@Test
void permits_creator() throws Exception {
mvc.perform(post("/api/tasks").with(jwt().authorities(new SimpleGrantedAuthority("task:create"))))
.andExpect(status().isCreated());
}
}Verify Configuration
./gradlew test --tests TaskSecurityTest reports separate green cases for 401 missing token, 403 wrong authority, and 201 permitted creator.
Synthesis & Production Verification
Production Acceptance Criteria
- Unauthenticated calls get 401, insufficient scope gets 403
- Passwords verify against {bcrypt} hashes and never log raw values
- Expired and wrong-audience tokens are rejected with distinct evidence
Key Takeaway
A production security implementation combines transport security, secure headers, stateless token validation, audit logging, and automated test matrices verifying both allowed and forbidden paths.
Architectural & Technical Review
Core Interview Questions
Q01What HTTP security headers should be enforced on production API responses?Show answer
Content-Security-Policy (CSP), Strict-Transport-Security (HSTS), X-Content-Type-Options (nosniff), X-Frame-Options (DENY), and Referrer-Policy to prevent clickjacking and MIME-confusion attacks.
Q02How does AuditorAware integrate with Spring Security for entity auditing?Show answer
AuditorAware<String> extracts the authenticated username from SecurityContextHolder, automatically populating @CreatedBy and @LastModifiedBy fields on JPA entities.
Q03How should an automated security test matrix be structured in Spring Boot?Show answer
Use MockMvc with SecurityMockMvcRequestPostProcessors.jwt() to systematically verify that unauthenticated calls return 401, unauthorized roles return 403, and authorized roles return 200.
Production Pitfalls
Critical Anti-Patterns
- Pitfall: Relying solely on transport TLS without application-level method security — TLS encrypts the channel, but does not enforce principal permissions or tenancy boundaries.
- Pitfall: Failing to verify 401 and 403 authorization matrix paths in automated tests — Testing only happy-path 200 responses risks leaving authorization holes completely undetected.
Module Certification Quiz
Prove Module Mastery
Comprehensive question set covering all lessons in this module.
Primary Documentation & Specifications
Lesson Primary References
Supplementary Module Resources