Spring Security: the filter chain, 401 and 403
Security runs as servlet filters before any controller; authorization rules are checked in order and the first match decides, and the response says whether you are unknown or merely not allowed.
After this lesson you can
- Place Spring Security in the request flow and say what Boot configures by default
- Order authorization rules so a broad rule does not shadow a specific one
- Distinguish 401 from 403, and hasRole from hasAuthority
Spring Security is a chain of servlet filters that every request
passes through before it reaches the DispatcherServlet. One filter
registered with the servlet container (DelegatingFilterProxy) hands
the request to Spring's FilterChainProxy, which picks the matching
SecurityFilterChain and runs its filters: one reads credentials and
authenticates, one translates security exceptions into responses, and
the last one checks authorization. A request rejected there never
reaches a controller.
Add spring-boot-starter-security and, until you declare your own
SecurityFilterChain, Boot secures every endpoint, enables form
login and HTTP Basic, and logs a generated password for a user named
user at startup. Your own configuration replaces that:
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**", "/actuator/health").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.POST, "/orders/**").hasAuthority("SCOPE_orders.write")
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
return http.build();
}
First match wins
The rules are checked in the order declared, and the first matcher
that fits the request decides. A broad rule placed early shadows every
narrower rule after it; Spring Security only guards one case for you
and refuses to start if anything is declared after anyRequest().
Order rules from most specific to least specific.
import java.util.*; public class Solution { record Rule(String prefix, String requirement) {} static String decide(List<Rule> rules, String path, Set<String> authorities) { for (Rule r : rules) { if (path.startsWith(r.prefix())) { // first matching rule decides boolean ok = switch (r.requirement()) { case "authenticated" -> true; // this user is logged in default -> authorities.contains(r.requirement()); }; return path + " matched " + r.prefix() + "** -> " + (ok ? "200" : "403"); } } return path + " -> no rule"; } public static List<String> run() { Set<String> user = Set.of("ROLE_USER"); List<Rule> broadFirst = List.of(new Rule("/api/", "authenticated"), new Rule("/api/admin/", "ROLE_ADMIN")); List<Rule> specificFirst = List.of(new Rule("/api/admin/", "ROLE_ADMIN"), new Rule("/api/", "authenticated")); return List.of( "broad first: " + decide(broadFirst, "/api/admin/purge", user), "specific first: " + decide(specificFirst, "/api/admin/purge", user), "specific first: " + decide(specificFirst, "/api/orders", user)); }}401 or 403
- 401 Unauthorized really means unauthenticated: the request
carries no valid identity, and the response invites the client to
authenticate (for HTTP Basic, with a
WWW-Authenticateheader). - 403 Forbidden means the user is known but lacks the required authority. Logging in again will not help.
When an anonymous request is denied, Spring Security starts authentication (401, or a redirect to a login page for form login); when an authenticated user is denied, it answers 403.
Roles are authorities with a prefix
A user's permissions are a list of GrantedAuthority strings.
hasAuthority("SCOPE_orders.write") compares the string exactly.
hasRole("ADMIN") is shorthand for hasAuthority("ROLE_ADMIN") — the
ROLE_ prefix is added for you, so a user whose authority is the bare
string ADMIN does not pass hasRole("ADMIN").
Beyond URL rules, @EnableMethodSecurity enables
@PreAuthorize("hasRole('ADMIN')") on service methods — implemented by
a proxy, so self-invocation skips it just as it skips @Transactional.
Two more defaults worth knowing: CSRF protection is on, which matters
for cookie-based sessions and is commonly disabled for a stateless API
authenticated by bearer tokens; and passwords are stored through a
PasswordEncoder such as BCrypt, never in plain text.
Exercise · Pro
Which filter chain handles the request
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade