URL-Based Rules

authorizeHttpRequests

The simplest place to put rules is the SecurityFilterChain. Each requestMatchers(...) line maps URLs (and optionally an HTTP method) to a rule.

package in.shopease.config;

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.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                // public
                .requestMatchers(HttpMethod.GET, "/api/products/**", "/api/categories/**").permitAll()
                .requestMatchers("/api/auth/**", "/actuator/health").permitAll()
                // sellers and admins manage products
                .requestMatchers(HttpMethod.POST, "/api/products").hasAnyRole("SELLER", "ADMIN")
                .requestMatchers(HttpMethod.PUT, "/api/products/**").hasAnyRole("SELLER", "ADMIN")
                .requestMatchers(HttpMethod.DELETE, "/api/products/**").hasRole("ADMIN")
                // fine-grained permission
                .requestMatchers(HttpMethod.POST, "/api/orders/*/refund").hasAuthority("order:refund")
                // admin area
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                // everything else needs a logged-in user
                .anyRequest().authenticated()
            )
            .httpBasic(Customizer.withDefaults());
        return http.build();
    }
}

Order Matters: First Match Wins

Spring checks the rules from top to bottom and uses the first rule whose matcher matches. Put specific rules first and anyRequest() last (Spring refuses to start if something comes after anyRequest()).

Example

A simulation of first-match-wins. Notice how the wrong order lets a customer delete a product:

import java.util.List;
import java.util.Set;

public class Main {
    record Rule(String method, String prefix, String needs) {
        boolean matches(String m, String path) {
            return (method.equals("*") || method.equals(m)) && path.startsWith(prefix);
        }
    }

    static String decide(List<Rule> rules, String m, String path, Set<String> user) {
        for (Rule r : rules) {
            if (r.matches(m, path)) {
                boolean ok = r.needs().equals("permitAll") || user.contains(r.needs());
                return (ok ? "ALLOW" : "DENY ") + "  (matched " + r.method() + " " + r.prefix() + " -> " + r.needs() + ")";
            }
        }
        return "DENY   (no rule)";
    }

    public static void main(String[] args) {
        Set<String> asha = Set.of("ROLE_CUSTOMER");
        List<Rule> good = List.of(
            new Rule("DELETE", "/api/products", "ROLE_ADMIN"),
            new Rule("*", "/api/products", "permitAll"));
        List<Rule> bad = List.of(
            new Rule("*", "/api/products", "permitAll"),
            new Rule("DELETE", "/api/products", "ROLE_ADMIN"));

        System.out.println("Correct order:");
        System.out.println("  GET    /api/products/7 " + decide(good, "GET", "/api/products/7", asha));
        System.out.println("  DELETE /api/products/7 " + decide(good, "DELETE", "/api/products/7", asha));
        System.out.println("Wrong order:");
        System.out.println("  DELETE /api/products/7 " + decide(bad, "DELETE", "/api/products/7", asha));
    }
}

Output

Correct order:
  GET    /api/products/7 ALLOW  (matched * /api/products -> permitAll)
  DELETE /api/products/7 DENY   (matched DELETE /api/products -> ROLE_ADMIN)
Wrong order:
  DELETE /api/products/7 ALLOW  (matched * /api/products -> permitAll)

Testing the Rules with curl

Example

Asha (customer) can read products but cannot delete one. Meera (admin) can:

curl -i localhost:8080/api/products/7
curl -i -X DELETE -u [email protected]:asha123 localhost:8080/api/products/7
curl -i -X DELETE -u [email protected]:meera123 localhost:8080/api/products/7

Example output

HTTP/1.1 200 OK
{"id":7,"name":"boAt Rockerz 450","price":1499.0,"stock":120,"category":"Audio"}

HTTP/1.1 403 Forbidden

HTTP/1.1 204 No Content

Tip

Tip: In Spring Boot 3 / Security 6, requestMatchers("/api/products/**") uses path patterns and is the only matcher API you need. The old antMatchers and mvcMatchers were removed, and authorizeRequests is replaced by authorizeHttpRequests.