Spring Security Fundamentals

Authorization Rules

18 min Lesson 8 of 13

Authorization Rules

Authentication answers who are you?. Authorization answers what are you allowed to do?. In Spring Security 6, authorization rules for HTTP requests are declared fluently inside SecurityFilterChain using the authorizeHttpRequests DSL. Getting these rules right — and understanding their order, precedence, and security implications — is one of the highest-leverage skills you can build as a Spring developer.

The authorizeHttpRequests DSL

When you register a SecurityFilterChain bean you call http.authorizeHttpRequests(auth -> auth...). Each call inside the lambda adds a request matcher rule. Spring Security evaluates them top-to-bottom and stops at the first match, so rule order is critical.

import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth // 1. Public assets — no authentication required .requestMatchers("/", "/login", "/register", "/css/**", "/js/**", "/images/**").permitAll() // 2. Admin area — must have ROLE_ADMIN .requestMatchers("/admin/**").hasRole("ADMIN") // 3. Account management — any authenticated user .requestMatchers("/account/**").authenticated() // 4. Catch-all — deny everything not explicitly permitted .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login").defaultSuccessUrl("/dashboard") ) .logout(logout -> logout.logoutSuccessUrl("/")); return http.build(); } }
Order matters — always put specific rules before broad ones. If you place .anyRequest().authenticated() before your permitAll() rules, every request (including your login page) will require authentication, locking users out permanently. Most security misconfigurations in Spring apps are ordering bugs.

Request Matchers

Spring Security 6 uses requestMatchers() (replacing the deprecated antMatchers() and mvcMatchers()). It accepts Ant-style path patterns, HTTP methods, or combinations of both.

auth // Ant-style wildcards: * matches one path segment, ** matches multiple .requestMatchers("/api/**").hasRole("API_USER") // Restrict by HTTP method AND path .requestMatchers(HttpMethod.POST, "/articles").hasRole("EDITOR") .requestMatchers(HttpMethod.GET, "/articles/**").permitAll() // Multiple paths in one call .requestMatchers("/health", "/actuator/info").permitAll() .requestMatchers("/actuator/**").hasRole("OPS")
Spring Security 6 removed antMatchers(). If you are migrating from Spring Security 5, replace every antMatchers() call with requestMatchers(). The pattern syntax is the same, but the new method also understands Spring MVC route patterns automatically when the MVC dispatcher is on the classpath.

Roles vs Authorities

Spring Security stores both roles and fine-grained authorities as strings on the Authentication object. The distinction is a naming convention: roles are authorities prefixed with ROLE_.

  • hasRole("ADMIN") — checks for the authority ROLE_ADMIN (the prefix is added automatically).
  • hasAuthority("ROLE_ADMIN") — checks for the exact string you supply. No prefix is added.
  • hasAnyRole("USER", "ADMIN") — passes if the principal has any of the listed roles.
  • hasAnyAuthority("read:orders", "write:orders") — useful for OAuth 2 scopes or fine-grained permissions.
auth // Role-based (ROLE_ prefix added for you) .requestMatchers("/reports/**").hasRole("MANAGER") // Authority-based (exact string match — good for OAuth2 scopes) .requestMatchers("/api/orders").hasAuthority("orders:read") // Multiple roles — either is sufficient .requestMatchers("/dashboard").hasAnyRole("USER", "ADMIN", "MANAGER")

Securing URL Patterns — A Realistic Example

Consider a small e-commerce application with three actor types: anonymous visitors, authenticated customers, and admins. Here is how a complete rule set would look in practice:

http.authorizeHttpRequests(auth -> auth // --- Anonymous access --- .requestMatchers(HttpMethod.GET, "/products/**").permitAll() .requestMatchers("/cart/view").permitAll() .requestMatchers("/login", "/register", "/forgot-password").permitAll() .requestMatchers("/error", "/favicon.ico").permitAll() // --- Customer-only --- .requestMatchers("/cart/checkout", "/orders/**", "/account/**").hasRole("CUSTOMER") // --- Admin-only --- .requestMatchers("/admin/**").hasRole("ADMIN") .requestMatchers(HttpMethod.DELETE, "/products/**").hasRole("ADMIN") .requestMatchers(HttpMethod.POST, "/products/**").hasRole("ADMIN") // --- Catch-all: deny unmatched requests --- .anyRequest().denyAll() )

Note the use of denyAll() as the catch-all instead of authenticated(). In a well-scoped API this is the safer default: any endpoint not explicitly whitelisted is denied, so newly added endpoints do not accidentally become public before rules are written for them.

Prefer denyAll() as your catch-all in REST APIs, and authenticated() for traditional web apps. In a REST service every resource should be explicitly permitted; an implicit catch-all of authenticated() can silently expose endpoints you forgot to protect.

Access Decision Expressions with SpEL

For complex conditions that go beyond a single role check you can use Spring Expression Language (SpEL) via access(). This unlocks IP address checks, time-of-day gating, or combining multiple roles with boolean logic:

import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager; auth // Require ROLE_ADMIN and that the request comes from the internal network .requestMatchers("/admin/config") .access(new WebExpressionAuthorizationManager( "hasRole('ADMIN') and hasIpAddress('10.0.0.0/8')" )) // Managers OR Admins can view financial reports .requestMatchers("/finance/reports") .access(new WebExpressionAuthorizationManager( "hasAnyRole('MANAGER','ADMIN')" ));

Security Implications and Common Pitfalls

  • Missing catch-all rule: Without .anyRequest() at the end, any unmatched URL bypasses your security configuration entirely. Always end with .anyRequest().denyAll() or .anyRequest().authenticated().
  • Overly broad wildcards: /api/** matches /api/admin/users as well as /api/products. Make sure your broader rules come after your narrower ones.
  • Relying on URL security alone: If a user with only ROLE_CUSTOMER somehow invokes an admin service method directly, URL rules do not protect you. Combine with method-level security (the next lesson) for defence in depth.
  • Actuator exposure: Spring Boot's /actuator/** endpoints are notoriously easy to forget. Always explicitly restrict them to ROLE_OPS or an IP allowlist.

Summary

Authorization rules in Spring Security 6 are declared with authorizeHttpRequests inside a SecurityFilterChain bean. Rules are matched top-to-bottom — specific paths must come before broad wildcards. Use hasRole() for role-based checks (the ROLE_ prefix is added automatically) and hasAuthority() for exact authority strings such as OAuth 2 scopes. End every rule set with .anyRequest().denyAll() or .anyRequest().authenticated() to prevent accidental exposure of unmatched endpoints. In the next lesson you will push authorization down to the method level using @PreAuthorize and @PostAuthorize.