Most access control bugs in Spring applications are not flaws in Spring Security but configuration mistakes: a filter chain that never matches, a permitAll() rule placed above a stricter one, a controller method nobody annotated, or CSRF switched off to make a test pass. Spring Security is the default security framework for enterprise Java, and its behaviour is precise but unforgiving: chains and rules are evaluated in order and only the first match applies. This guide is a configuration reference for engineers and reviewers working with Spring Security 6.x and 7.x on servlet (Spring MVC) and reactive (WebFlux) stacks. It covers how filter chains and authorisation decisions work, method security, OAuth2 resource server settings including audience validation and DPoP, CSRF, logging and a hardening baseline. Language-level Java risks are covered in the Java and Spring Security Guide; this page goes deeper on the framework itself.
Key takeaways
- First match wins, twice.
FilterChainProxyinvokes only the firstSecurityFilterChainwhose matcher fits the request, andAuthorizationFilterapplies only the first matching rule inside it; order is security policy. - Deny by default. End every chain with
anyRequest().denyAll()oranyRequest().authenticated(), and add one catch-all chain with nosecurityMatcheras the last bean. - Unannotated methods are unsecured. Method security with
@PreAuthorizeprotects only what is annotated and called through the Spring proxy, so keep request-level rules as the backstop. - Resource servers must check audience.
issuer-urivalidatesiss,expandnbf, but notaud; setaudiencesor a custom validator, and consider DPoP for high-value APIs. - Keep CSRF on for browser sessions. Disable it only for stateless APIs that do not use cookies for authentication, and prefer
ignoringRequestMatchersfor specific endpoints.
How identity and access work in Spring Security
Servlet filter chains
Spring Security plugs into the servlet container through DelegatingFilterProxy, which hands requests to the FilterChainProxy bean. FilterChainProxy applies the HttpFirewall and then selects a SecurityFilterChain. The reference documentation is explicit that only the first chain that matches is invoked: if /api/** is matched by the first chain, a later chain that also matches is never consulted. Chain priority is set with @Order on each SecurityFilterChain bean, and a bean without @Order is considered last.
Within a chain, filters run in a fixed order: CsrfFilter, then authentication filters, then AuthorizationFilter, which is last by default. You can print the filter list for a request to confirm what is actually running.
Authorisation rules
- Default: with Spring Boot and no custom chain, every request must be authenticated.
- Ordering:
authorizeHttpRequestsrules are processed in the order listed and only the first match applies. Put specific rules first and the catch-all last. - Matchers: Ant-style path patterns by default, with
RegexRequestMatcheravailable; anchor regular expressions and test them. - Dispatch types: authorisation applies to
FORWARD,ERRORandINCLUDEdispatches as well as the original request, so error pages and view forwards may need explicit rules; permit those dispatcher types deliberately rather than broadening path rules. - Firewall: the default
StrictHttpFirewallrejects un-normalised requests and strips path parameters and duplicate slashes for matching. Relaxing it to allow matrix variables or encoded characters should be a reviewed decision.
Servlet versus reactive
| Concern | Servlet (Spring MVC) | Reactive (WebFlux) |
|---|---|---|
| Enable | @EnableWebSecurity | @EnableWebFluxSecurity |
| Chain type | SecurityFilterChain built from HttpSecurity | SecurityWebFilterChain built from ServerHttpSecurity |
| Request rules | authorizeHttpRequests with requestMatchers | authorizeExchange with pathMatchers |
| Method security | @EnableMethodSecurity | @EnableReactiveMethodSecurity; methods return Mono or Flux |
| Security context | Thread-bound SecurityContextHolder | Reactor context; thread-local lookups do not work |
| Firewall | StrictHttpFirewall | StrictServerWebExchangeFirewall |
Mixing the two models (for example calling servlet-style SecurityContextHolder in WebFlux code) is a common source of "anonymous" principals in production.
Admin roles and privileged access
Spring Security has no admin console: privileged access is whatever your rules grant to administrative authorities and management endpoints.
- Authorities over roles in expressions:
hasRole('ADMIN')checks forROLE_ADMIN;hasAuthoritychecks the exact string. The method security documentation advises favouring granted authorities over complicated SpEL expressions, which are hard to review. - Separate admin chain: put admin paths in their own
SecurityFilterChainwith@Orderahead of the general chain, stronger authentication (for example MFA at the identity provider) and adenyAll()catch-all. - Method security for business operations: enable
@EnableMethodSecurity, which activates@PreAuthorize,@PostAuthorize,@PreFilterand@PostFilter.@Securedis legacy;@PreAuthorizeis recommended. Use@PostAuthorize("returnObject.owner == authentication.name")style checks for object-level ownership, or@AuthorizeReturnObjectto secure returned domain objects. - Proxy limits: method security is built on Spring AOP, so calls from one method to another inside the same bean bypass the proxy and its checks. Put the annotation on the entry point or move the call to another bean.
- Management endpoints: Actuator endpoints need their own chain restricted to an operations authority or a separate management port. See the Spring Boot Actuator and Management Endpoint Security Guide.
- Migration: replace
@EnableGlobalMethodSecuritywith@EnableMethodSecurity; the reference has a migration section.
| Request-level rules | Method-level rules |
|---|---|
| Coarse-grained, declared in one configuration class (DSL) | Fine-grained, declared on each method (annotations and SpEL) |
| Cover every request, including endpoints nobody remembered | Cover only annotated methods reached through the proxy |
| Best for authentication requirements, admin areas, deny-by-default | Best for ownership and business permission checks |
The reference warns that with annotation-based method security, unannotated methods are not secured, and recommends a catch-all request rule to protect against that. Use both layers.
Integrations, API tokens and service accounts
OAuth2 resource server (JWT)
- Issuer:
spring.security.oauth2.resourceserver.jwt.issuer-urimust equal theissclaim and point at a discovery document; the resource server then validatesexp,nbfandiss. Supplyingjwk-set-urias well lets the server start without contacting the issuer while still validatingiss. - Audience: set
spring.security.oauth2.resourceserver.jwt.audiences(or add an audience validator) so tokens minted for another API are rejected. OWASP ASVS 5.0 V10.3.1 requires resource servers to accept only tokens intended for them. - Scopes and authorities: scopes become authorities with the
SCOPE_prefix by default; useaccess(hasScope("message:read"))orhasAuthority("SCOPE_message:read"), and a customJwtAuthenticationConverterif roles come from another claim. - Sender-constrained tokens: the resource server reference documents DPoP-bound access tokens (RFC 9449): the client sends
Authorization: DPoP <token>with aDPoPproof header, and the server checks that the key thumbprint in the token'scnf.jktclaim matches the proof key and that the proof'sathclaim matches the token hash. ASVS V10.3.5 (Level 3) calls for sender-constrained tokens. See the DPoP Guide. - Opaque tokens: introspection clients authenticate to the authorisation server with a client secret or key; store that credential in a secrets manager, not
application.yml.
Service accounts and outbound calls
- Machine clients: services calling other APIs should use the OAuth2 client support with the client credentials grant and short-lived tokens, not shared API keys in headers. See the Machine-to-Machine OAuth (Client Credentials) Guide.
- Separate chains for machine APIs: give machine-to-machine endpoints their own stateless chain that accepts only bearer or DPoP tokens and no form login or sessions.
- Client secrets: prefer
private_key_jwtor mTLS client authentication over shared secrets where the authorisation server supports them, and keep keys out of source control.
CSRF
- Default: CSRF protection is on for unsafe methods such as POST. Tokens are stored in the
HttpSessionbyHttpSessionCsrfTokenRepository; since Spring Security 6 token loading is deferred and each token is randomised per request to resist BREACH. - Single-page applications:
CookieCsrfTokenRepositorywrites anXSRF-TOKENcookie and readsX-XSRF-TOKEN; usewithHttpOnlyFalse()only when JavaScript must read the cookie, as the reference recommends. Spring Security 6 changed SPA handling, so follow the SPA section rather than old examples. - Narrow exceptions: before disabling CSRF, the reference suggests ignoring only specific endpoints with
ignoringRequestMatchers, for example a webhook authenticated by signature. - When disabling is correct: stateless APIs that authenticate every request with a bearer or DPoP token, and never with cookies, do not need CSRF tokens. See the CSRF Prevention Guide.
Decision guidance
- Session login or token resource server: server-rendered applications suit form or OIDC login with sessions and CSRF; APIs consumed by other services or SPAs through a backend suit stateless resource server chains. Many applications need both, in separate chains.
- Bearer or DPoP: bearer tokens are simpler and fine for internal low-risk APIs with short lifetimes; DPoP is worth the client work for payment, admin and partner APIs where token theft is a realistic threat.
- Small teams: one browser chain, one API chain, deny-by-default and audience validation cover most risk. Large estates should publish a shared configuration library with these defaults and a test kit, so each service does not reinvent ordering.
Logging and detection
- Authentication events: register a
DefaultAuthenticationEventPublisherand listen forAuthenticationSuccessEventandAuthenticationFailureEventsubclasses; forward them to your SIEM with principal, client IP and outcome. - Authorisation events: publish
AuthorizationDeniedEventwithSpringAuthorizationEventPublisher;AuthorizationGrantedEventis optional and noisy, so enable it only for sensitive operations. - Firewall rejections: log
RequestRejectedExceptionfrom the HTTP firewall; spikes indicate path traversal or matcher bypass probing. - Token validation failures: log JWT rejections by reason (issuer, audience, expiry, signature) without logging the token. See the JWT and Session Tokens in Code and Logs Guide.
- Debug output: security debug and TRACE logging print filter chains and request details; never leave them on in production.
Metrics:
- Authorisation denials per endpoint per day, with alerts on sudden changes.
- Percentage of controller endpoints covered by an explicit request rule or method annotation (from a build-time test).
- JWT rejections for wrong audience, a sign of token misuse across services.
- Authentication failures per account and per source address.
- Number of chains or rules containing
permitAll(), reviewed each release.
Hardening baseline
| Area | Setting | Check |
|---|---|---|
| Chain ordering | Specific chains with securityMatcher and @Order; one final catch-all chain | Test that unknown paths are denied |
| Rule ordering | Specific requestMatchers first; anyRequest().denyAll() or authenticated() last | Unit tests with MockMvc for each role and an anonymous user |
| Method security | @EnableMethodSecurity, @PreAuthorize on service entry points | Architecture test that service methods carry an annotation |
| Firewall | Default StrictHttpFirewall | Review any relaxations |
| Resource server | issuer-uri plus audiences; DPoP for high-value APIs | Reject a valid token minted for another audience |
| Sessions | Stateless (SessionCreationPolicy.STATELESS) for token APIs; session fixation protection left on for browser apps | No JSESSIONID issued on API chains |
| CSRF | On for cookie-authenticated apps; narrow ignores only | POST without token is rejected |
| Headers | Defaults kept: Cache-Control: no-cache, no-store, max-age=0, must-revalidate, X-Content-Type-Options: nosniff, HSTS max-age=31536000 ; includeSubDomains on HTTPS, X-Frame-Options: DENY, X-XSS-Protection: 0; add a CSP | Response header scan. See the Security Headers Guide. |
| Passwords | DelegatingPasswordEncoder with a modern adaptive algorithm | No NoOpPasswordEncoder anywhere |
| Static resources | Use permitAll() rules rather than removing paths from Spring Security, so headers still apply | Headers present on static responses |
Rollout and testing
- Print the filter chains for representative URLs and draw the chain map: which chain handles which paths.
- Write MockMvc or WebTestClient tests for every endpoint and role, including anonymous access and wrong-audience tokens, before changing rules.
- Refactor to deny-by-default in a feature branch; run the test suite and a dynamic scan against a staging environment.
- Release behind a canary instance and watch authorisation denial metrics; rollback is redeploying the previous configuration, so keep security config changes in their own commits.
- Add the tests to CI so a new controller without a rule fails the build.
Framework mapping
| Framework | Reference | Spring Security control |
|---|---|---|
| OWASP ASVS 5.0 | V8.2.1, V10.3.1, V10.3.5, V3.5.1 | Function-level access, audience validation, sender-constrained tokens, CSRF defences |
| NIST SP 800-53 Rev. 5 | AC-3, AC-6, IA-2, SC-23, AU-2 | Access enforcement, least privilege, authentication, session authenticity, event logging |
| NIST CSF 2.0 | PR.AA-05 | Access permissions defined, enforced and reviewed |
| MITRE ATT&CK | T1078 Valid Accounts | Strong authentication and audience checks limit reuse of stolen credentials and tokens |
Common misconfigurations
- Broad
permitAll()above specific rules:requestMatchers("/api/**").permitAll()listed beforerequestMatchers("/api/admin/**").hasRole("ADMIN")makes the admin rule unreachable. - Chain that never matches: two chains without distinct
securityMatchervalues, or a general chain ordered first, so the stricter API chain is never used. - No catch-all: rules list known endpoints and stop, so new endpoints are reachable by any authenticated or anonymous user depending on defaults.
- Method security assumed everywhere: missing annotations, or internal calls that bypass the proxy, leave business operations unprotected. See the Broken Access Control Prevention Guide.
- No audience validation: any token from the shared identity provider works on every API, so one compromised low-value client opens all services.
- CSRF disabled globally on a session-based app: copied from an API tutorial, it reopens cross-site request forgery on every form.
- Error and forward dispatches overlooked: broad permits added to fix error page access instead of permitting the
ERRORdispatcher type.
Practitioner checklist
- Map every
SecurityFilterChain, itssecurityMatcherand@Order, and keep one final catch-all chain. - Order rules from specific to general and end each chain with
denyAll()orauthenticated(). - Enable
@EnableMethodSecurityand put@PreAuthorizeon service entry points, keeping request rules as the backstop. - Configure resource servers with
issuer-uriandaudiences, map scopes deliberately, and use DPoP for high-value APIs. - Keep CSRF enabled for cookie-based sessions and use
ignoringRequestMatchersfor narrow exceptions. - Keep the default
StrictHttpFirewalland security headers, and add CSP. - Isolate Actuator and admin paths in their own chains with an operations authority.
- Publish authentication and authorisation denied events to the SIEM.
- Test every endpoint for anonymous, wrong-role and wrong-audience access in CI.
Standards and references
- Spring Security Reference: Servlet Architecture
- Spring Security Reference: Authorize HttpServletRequests
- Spring Security Reference: Method Security
- Spring Security Reference: OAuth 2.0 Resource Server JWT
- Spring Security Reference: OAuth 2.0 DPoP-bound Access Tokens
- Spring Security Reference: Cross Site Request Forgery (CSRF)
- IETF: RFC 9449 OAuth 2.0 Demonstrating Proof of Possession (DPoP)
- OWASP: Application Security Verification Standard 5.0
Related NHI Mgmt Group resources: Java and Spring Security Guide · Spring Boot Actuator and Management Endpoint Security Guide · Broken Access Control Prevention Guide · DPoP Guide · JWT Security Guide