Quick answer
Spring Security is most reliable when authentication establishes a verified principal, an ordered SecurityFilterChain selects the right policy for the request, and authorization starts from deny by default. A request rule decides whether this endpoint class can be entered; method security and a service-level ownership decision decide whether this caller may perform this operation on this resource. These layers overlap deliberately, but none should be mistaken for the others.
For the order service, a bearer token or session does not grant permission merely because it contains a customer-looking identifier. The server derives identity and tenant scope from verified credentials, then checks ownership against current order state. A customer may read only their order, a support role may have a constrained audited capability, and a background repair job needs its own service identity. The request body never supplies authority.
This article uses Java 17 and Spring Boot 4.1 as the mainline with imperative Spring MVC. Spring Boot 3.5 applications can apply the same boundary model, but should confirm their selected Spring Security version, matchers, authorization manager APIs, and default filter configuration before copying configuration. In particular, defining a custom chain changes Boot auto-configuration behavior; inspect the resulting chains rather than assuming both default and custom policies are active.
Shared order-service incident stage
In the flash-sale incident, the order service had already rejected malformed requests at the MVC boundary and begun short inventory reservations in its application service. A second failure appeared: an internal support endpoint was reachable by a broad authenticated-user rule, and several clients retried a stale order URL. No customer data was intentionally returned, but ambiguous authorization increased load and risk while database pool wait was growing.
Responders first identified which filter chain selected /api/orders/**, which authentication was accepted, and which authorization decision denied or allowed each route. They did not use a controller parameter such as customerId as identity. For a GET /api/orders/{orderId}, the service loaded the order in the tenant derived from the principal and tested ownership. A support path used a separate role and a narrowly scoped audit trail; it was not accidentally opened by the customer chain.
The incident therefore moves from transport validation to identity, request authorization, ownership, and method decisions before the transactional reservation path. When traffic is expensive, a clear 401 or 403 at the earliest safe point is cheaper than a query that will later fail. Yet authorization must remain correct for non-HTTP callers, cached references, and methods invoked by workers. The recovery goal is not “more rules”; it is one explainable policy from request selection through resource access.
Core mechanism and evidence boundary
Servlet security runs as a filter chain before an MVC controller. Spring Security selects the first SecurityFilterChain whose matcher applies, then invokes its filters in their configured order. Multiple chains are useful when an actuator management port, a machine-to-machine API, and a browser application have genuinely different authentication and CSRF needs. They are dangerous when broad early matchers shadow a narrower later chain. Give every chain an explicit securityMatcher, declare @Order, and test representative URLs.
Authentication answers who or what presented usable credentials. Authorization answers whether that identity may take an action. At the HTTP level, map public routes deliberately, require authority for every remaining request, and use anyRequest().denyAll() when the application’s route inventory is explicit. Deny by default makes a newly added endpoint unavailable until someone consciously gives it a policy. It is a safer failure than silently inheriting a broad authenticated() rule.
Request authorization is coarse and useful: require a staff authority before reaching an administrative controller, or an order-write authority before accepting checkout. It cannot normally decide whether the current customer owns an arbitrary order ID without loading resource state. Put that object-level authorization in an application service or a method-security policy that receives trusted identity and resource state. @PreAuthorize can protect service entry points, but a role string alone does not prove ownership; query through the tenant boundary and compare server-side identifiers.
Method security is independently enabled and protects call paths that do not pass through MVC. That matters for message consumers, scheduled reconciliation, and code reused by a repair endpoint. It does not make direct object construction secure, and it does not eliminate clear request rules. Keep expressions small and testable, prefer a named authorization component for nontrivial logic, and never place raw SpEL based on client-controlled data where a service method could make the decision transparently.
Evidence must be bounded. Count authentication failures, access denials, selected policy family, and ownership-denied outcomes with small enumerations. A protected audit event can contain a correlation ID and actor type. Do not tag metrics with an order ID, token subject, email, raw URI, authorization expression, or exception text. A 403 proves only that this policy denied the presented request; it is not proof that all endpoints, all method calls, or all historical resource permissions are correct.
Token-to-authority mapping needs the same review as a database schema. Validate issuer, audience, signature, algorithm policy, expiry, and key rotation at the resource-server boundary, then translate only documented claims to a small authority vocabulary. Do not grant an application role merely because an unverified or ambiguously named claim is present. Clock skew, key rotation, disabled customers, and revoked privileged access are operational cases with an explicit failure posture. A locally valid signed token may still need a current account-status or tenant-membership decision for sensitive operations.
Error semantics should also avoid resource discovery. Choose deliberately whether an unauthenticated request gets 401 with the proper challenge, a known-but-forbidden resource gets 403, or an object outside the caller’s visible tenant is represented as not found. Consistency matters more than a universal rule: document the public contract, make support lookup privileged and audited, and do not leak existence through differing body detail, headers, timing, or cache behavior. Authorization failures should be observable to operators without explaining policy internals to an attacker.
Minimal reproducible Spring example
This Java 17 configuration makes selection and deny-by-default visible. The first chain handles only /api/**; a browser application or an intentionally exposed management surface would need its own explicit, earlier chain with the correct authentication and CSRF policy. The last chain has no securityMatcher, so it catches every request that no earlier chain selected and denies it. That includes a newly added controller, an error dispatch, a browser path, or a same-context management path until a reviewed rule permits it. The custom configuration is intentional: Boot’s web-security auto-configuration backs off when application-defined SecurityFilterChain beans take responsibility, so the application must own the complete effective policy rather than expecting an auto-configured fallback to remain.
package com.example.orders;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableMethodSecurity
class SecurityConfiguration {
@Bean
@Order(1)
SecurityFilterChain api(HttpSecurity http) throws Exception {
return http
.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable()) // Bearer-token API; browser chain differs.
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/public/status").permitAll()
.requestMatchers("/api/orders/**").hasAuthority("SCOPE_orders")
.anyRequest().denyAll())
.oauth2ResourceServer(oauth2 -> oauth2.jwt())
.build();
}
@Bean
@Order(99)
SecurityFilterChain fallback(HttpSecurity http) throws Exception {
return http
// No securityMatcher: this is the final otherwise-unmatched chain.
.authorizeHttpRequests(authorize -> authorize
.anyRequest().denyAll())
.build();
}
}
The fallback intentionally makes browser and management traffic unavailable, rather than accidentally unauthenticated. If the application later serves a cookie-authenticated browser UI, add a specifically matched chain before the fallback and keep CSRF enabled. If Actuator endpoints are deliberately exposed, give their application or management context a narrow endpoint-aware policy, network boundary, and operator identity, then test the real port and paths. A separate management child context may have its own security configuration, so deployment tests—not the presence of this main-context bean—must prove its result.
The service receives the authenticated subject from a trusted adapter and enforces ownership after loading a tenant-scoped record. The repository query itself limits candidate data to the actor’s tenant; comparison is defense in depth, not an excuse to fetch every order and filter it in memory.
@Service
public class OrderReadService {
private final OrderRepository orders;
public OrderReadService(OrderRepository orders) { this.orders = orders; }
@PreAuthorize("hasAuthority('SCOPE_orders')")
public OrderView findForCustomer(Actor actor, UUID orderId) {
Order order = orders.findByTenantIdAndId(actor.tenantId(), orderId)
.orElseThrow(OrderNotFoundException::new);
if (!order.customerId().equals(actor.customerId())) {
throw new AccessDeniedException("order is not owned by actor");
}
return OrderView.from(order);
}
}
The class and security-protected entry point are deliberately non-final and public because this sketch uses class-based proxying without an interface. Spring can therefore create the method-security proxy when this service is obtained from the container. A real application may instead inject an interface and use an interface proxy, but it must not make a final, non-interceptable class look protected merely because @PreAuthorize appears in source. The tenant-scoped query and explicit customer comparison remain necessary after method authorization: SCOPE_orders permits the operation family, not every order in that family.
For a browser application authenticated by cookies, CSRF is a separate concern. Same-origin cookies are automatically attached by a browser, so a state-changing request can be forged from another site unless an appropriate CSRF token or equivalent protected mechanism is required. Do not copy the API chain’s CSRF disablement into a cookie-session chain. CORS is also not authorization: it tells browsers which cross-origin scripts may read a response, while non-browser clients can still send requests. Configure exact trusted origins, methods, and headers; do not use a wildcard with credentials.
Failure modes and dangerous misconceptions
“Authenticated means authorized.” Authentication only establishes a principal under the selected mechanism. A valid customer token should not reach support, operations, or another customer’s resource. Define authorities, tenant scope, and ownership separately.
“The last chain is the fallback.” Chain matching is first applicable chain wins. A broad /** chain with earlier order can prevent a later /api/** chain from ever running. Log or inspect the configured chains in a safe environment and test actual URLs, including error and actuator paths.
“A request matcher secures ownership.” It cannot decide ownership without trustworthy state. A pattern such as /orders/{id} says nothing about whether the caller owns {id}. Resolve identity server-side and perform an explicit resource decision in a service or authorization component.
“Disable CSRF because CORS exists.” CORS and CSRF address different browser behaviors. A bearer-token API that browsers do not automatically authenticate may choose a different CSRF posture than a cookie session application. Document the credential transport before changing the default.
“Adding a custom chain adds one rule to Boot defaults.” A custom-chain configuration can cause Spring Boot auto-configuration backoff. Treat it as owning the policy: include authentication, authorization, exception handling, and any other desired defaults explicitly, then verify the effective result.
Security, privacy, transaction, capacity, and cost implications
Security policy should reduce work before scarce resources are acquired. Reject absent or invalid credentials before a database lookup where possible, but do not turn a timing difference into an account-enumeration oracle. Use generic public responses where disclosure matters, preserve detailed protected diagnostics, and rate-limit authentication and authorization failures separately from successful checkout traffic.
Authorization data is sensitive. Avoid returning “this order belongs to customer X,” and avoid storing raw tokens, claims, addresses, or authorization headers in logs. A support role must have a documented reason, minimal scope, audit retention policy, and revocation path. Role names alone are not a permission review; review how a privilege is assigned and how it is constrained by tenant and operation.
Keep authorization outside long database transactions when a stable preliminary decision is possible, then re-check facts that can change at the write boundary. A previously authorized customer may lose access or an order may transition while a request waits. The transaction must preserve the final inventory and order invariants; security checks do not make a race-safe state change.
Security controls consume capacity too. Token signature verification, remote key retrieval, password hashing, policy queries, and audit delivery require bounded timeouts and caching rules. Cache public keys safely with rotation behavior, not individual permission decisions without invalidation. A denial spike can be an attack, a broken rollout, or a clock/configuration issue; distinguish it with bounded metrics rather than increasing database pools.
Testing and production validation
Use MVC security tests for no credential, invalid credential, customer credential, support credential, and an endpoint missing an explicit request rule. Assert 401 versus 403 according to the documented API contract, and assert that denied requests do not call the expensive service path. Test chain selection for an unmatched route, an error dispatch, a browser URL, and a management URL as well as representative /api and public mappings. The fallback cases should be denied unless an explicit earlier chain owns them.
Use service integration tests for tenant scoping and ownership. Create two customers in two tenants, then prove that each cannot read or mutate the other’s order even when the UUID is known. Invoke protected service methods through the Spring context to confirm method security is enabled. A unit test of a policy helper is useful, but it is not proof that the filter chain or method interceptor is installed.
In staging, exercise one approved machine identity and one customer identity through the deployed gateway. Verify structured denial counts, selected deployment version, and absence of secrets in logs. During the flash-sale rehearsal, load unauthorized traffic as well as valid traffic and confirm it is rejected without exhausting connection pools. Review policy changes with threat modeling and a rollback plan; restoring an earlier configuration is safe only if its credential and route assumptions still hold.
Include upgrade tests that make auto-configuration changes visible. Print or inspect a sanitized listing of intended filter-chain matchers in non-production diagnostics, assert the management and error routes select their designed policy, and test a token signed by both a current and a rotated key where rotation is supported. The valuable assertion is not the precise internal filter list, which can legitimately change across versions, but the externally observable authentication, authorization, CSRF, CORS, challenge, and ownership contract.
Operations checklist
- Define explicit, ordered security matchers and test which chain selects every route family.
- Authenticate from verified credentials; never trust account or tenant fields in a request body.
- Require explicit request rules and use deny by default for unknown endpoints.
- Apply method security to reusable service entry points and keep ownership checks state-aware.
- Scope repository access by tenant before comparing resource ownership.
- Treat CSRF as mandatory for relevant cookie-authenticated browser flows.
- Configure CORS as a narrow browser-read policy, not as authentication or authorization.
- Review custom-chain auto-configuration backoff and the effective filter chain after upgrades.
- Record bounded denial and authentication evidence without token, customer, or raw-URI labels.
- Rehearse privilege revocation, policy rollback, and an authorization-failure traffic spike.
Official sources
- Spring Security servlet architecture — accessed 2026-08-05.
- Spring Security authorization — accessed 2026-08-05.
- Spring Security method authorization — accessed 2026-08-05.
- Spring Boot web security — accessed 2026-08-05.
Continue the learning path
Follow the recovery signals in Spring Boot Actuator, Health, and Metrics Explained, then prove protected workflows with Spring Boot Integration Testing with Testcontainers Explained. Return to the Spring Backend course, browse Topics, and revisit Spring Backend Engineering.