skip to content

Explain the first-match-wins ordering rule in authorizeHttpRequests. How does rule ordering affect security?

level: middleimportance: must knowfreq 80%

answer

  1. top-to-bottom, first match wins
  2. specific before broad
  3. broad rule shadows specific
  4. anyRequest last — else startup error
  5. not most-specific-wins

basics

~10 s

Rules are evaluated top to bottom and the first matcher that matches a request decides access; later rules are ignored. So specific rules must come before broad ones, and anyRequest goes last.

solid answer

~40 s

authorizeHttpRequests evaluates rules in declaration order and applies the first matcher that matches — first-match-wins, not most-specific-wins. Once a rule matches, its decision (permitAll, authenticated, hasRole, etc.) is final and remaining rules are skipped. This means ordering is a security property: a broad matcher placed before a specific one shadows it. For example, putting anyRequest().permitAll() first, or /api/** before /api/admin/**, would let requests through the broad rule and never reach the stricter one. The safe pattern is to list the most specific and most permissive-but-narrow rules first, then progressively broader ones, and finish with anyRequest() as the catch-all default. Spring Security 6 even fails fast at startup if you declare a rule after anyRequest(), since anyRequest() must be last. Internally the RequestMatcherDelegatingAuthorizationManager iterates the ordered list and returns on the first match.

code

java · 11 lines
java
// WRONG — /api/** shadows the admin rule, admin check never runs
http.authorizeHttpRequests(auth -> auth
    .requestMatchers("/api/**").authenticated()
    .requestMatchers("/api/admin/**").hasRole("ADMIN")
    .anyRequest().denyAll());

// RIGHT — specific admin rule first, broad rule after
http.authorizeHttpRequests(auth -> auth
    .requestMatchers("/api/admin/**").hasRole("ADMIN")
    .requestMatchers("/api/**").authenticated()
    .anyRequest().denyAll());

go deeper

for a junior

Knows rules go top-to-bottom and anyRequest is last.

for a middle

Must be able to spot a shadowing bug and reorder specific-before-broad.

for a senior

Explains the startup guardrail and the no-match-defaults-open hazard.

for a principal

Treats rule ordering as a reviewable security invariant and enforces default-deny via anyRequest().denyAll().

## The rule Inside `authorizeHttpRequests`, every `requestMatchers(...).someRule()` pair is stored in an **ordered list**. When a request arrives, the `AuthorizationFilter` (via `RequestMatcherDelegatingAuthorizationManager`) walks that list **top to bottom** and stops at the **first matcher that matches**. That matched rule's `AuthorizationManager` makes the allow/deny decision, and **all later rules are ignored**. This is *first-match-wins*, **not** *most-specific-wins* (unlike, say, some routing frameworks). ## Why ordering is a security control Because the first match is final, a broad matcher placed early can **shadow** a stricter rule placed later. Consider: ```java .requestMatchers("/api/**").authenticated() // matches everything under /api .requestMatchers("/api/admin/**").hasRole("ADMIN") // NEVER reached ``` A request to `/api/admin/users` matches the first rule (`/api/**`) and is allowed for *any* authenticated user — the `hasRole("ADMIN")` rule is dead code. The fix is to order specific-before-broad: ```java .requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers("/api/**").authenticated() ``` ## anyRequest() must be last `anyRequest()` matches everything, so anything after it is unreachable. Spring Security 6 **throws an exception at startup** (`IllegalStateException: Can't configure requestMatchers after anyRequest`) if you add rules after `anyRequest()` — a helpful guardrail. Always end with it as your default (`anyRequest().authenticated()` or `.denyAll()` are the secure defaults; ending with `permitAll()` is rarely what you want). ## Method + path matchers and ordering Method-scoped matchers interact with ordering too: ```java .requestMatchers(HttpMethod.GET, "/api/**").permitAll() .requestMatchers(HttpMethod.POST, "/api/**").authenticated() ``` A GET matches the first rule; a POST falls through to the second. If you reversed a broad `requestMatchers("/api/**").permitAll()` to the top with no method, it would swallow both. ## Internals The DSL compiles to a `RequestMatcherDelegatingAuthorizationManager` holding a `List<RequestMatcherEntry<AuthorizationManager>>`. Its `check` method iterates entries, calls `matcher.matches(request)`, and delegates to the first match; if none match it returns null → historically treated as access granted (another reason `anyRequest()` matters). ## Gotchas - First-match-wins, not longest-prefix — reviewers must read top-down. - A stray early `permitAll()` is a classic vulnerability. - Overlapping Ant patterns (`/api/**` vs `/api/admin/**`) require deliberate ordering. - `anyRequest()` after other rules is fine; other rules after `anyRequest()` fail at startup.

  • What happens if you declare a requestMatchers rule after anyRequest()?
    Spring Security 6 fails at startup with an IllegalStateException — anyRequest() must be the last rule because it matches everything and would make subsequent rules unreachable.
  • If no rule matches a request and there is no anyRequest(), what happens?
    Historically the delegating manager returns no decision, which the filter treats as granted — leaving the endpoint effectively open. That is exactly why you must always add anyRequest() as an explicit default.
  • Is the evaluation most-specific-wins like URL routing?
    No. It is strictly declaration-order first-match-wins. A broader pattern listed earlier will win over a more specific one listed later.

context