By WebNest Studio Engineering Team
Spring Boot Tutorial
Spring Security Basics
Spring Security is the standard framework for securing Spring Boot applications. It's built around a chain of servlet filters that intercept every incoming request before it reaches your controllers, deciding whether the request is allowed to proceed. Understanding the filter chain, and the distinction between authentication and authorization, is the foundation for everything else in Spring Security.
The Security Filter Chain
When spring-boot-starter-security is on the classpath, Spring Boot registers a chain of servlet filters in front of your application (via DelegatingFilterProxy and FilterChainProxy). Each filter has one responsibility — one checks for a session, one checks for HTTP Basic credentials, one handles CSRF, one handles authorization decisions — and a request must pass through the whole chain before reaching a controller. By default, adding the security starter alone makes every endpoint require authentication with a generated login form and a random password printed to the console at startup.
Authentication vs Authorization
These two terms are often used loosely but mean distinctly different things. Authentication answers "who are you?" — verifying credentials (username/password, a token) and establishing an identity. Authorization answers "what are you allowed to do?" — given a known identity, deciding whether that identity may access a specific resource or perform a specific action, typically based on roles or authorities.
Configuring Access Rules
Modern Spring Security (5.7+) configures access rules through a SecurityFilterChain @Bean rather than extending WebSecurityConfigurerAdapter (deprecated and removed in newer versions). Inside it, authorizeHttpRequests() declares which paths require authentication, which require a specific role, and which are open to everyone.
Rules Are Checked in Order
Spring Security checks the requestMatchers rules from top to bottom and uses the first one that matches. Put specific rules first and the catch-all anyRequest() last. If .requestMatchers("/api/**").authenticated() came before .requestMatchers("/api/admin/**").hasRole("ADMIN"), every signed-in user could reach the admin pages, because the broader rule would match first.
Rules Need a Way to Log In
Access rules only say who may do what. The filter chain also needs at least one authentication mechanism, such as httpBasic(), formLogin() or oauth2ResourceServer() for JWTs, plus a source of users such as a UserDetailsService. Without a mechanism there is no way to sign in, and every protected URL answers 403 Forbidden, even for requests that carry a correct username and password. The mechanism also decides what "not logged in" looks like: HTTP Basic answers 401 with a WWW-Authenticate header, and form login redirects to the login page.
401 or 403?
- 401 Unauthorized means "I do not know who you are": no credentials, wrong password or an expired token. The client should sign in, or sign in again.
- 403 Forbidden means "I know who you are, and you may not do this": a signed-in user without the required role. Signing in again will not help.
- Roles are stored as authorities with a
ROLE_prefix..roles("ADMIN")creates the authorityROLE_ADMIN, andhasRole("ADMIN")checks for it.
Choosing an Authentication Mechanism
- Form login with a session cookie: server-rendered web applications. Keep CSRF protection on.
- HTTP Basic: internal tools, scripts and quick tests. Only over HTTPS, because the password travels with every request.
- JWT bearer tokens (OAuth2 resource server): APIs used by single-page apps, mobile apps and other services.
- OAuth2 login ("Sign in with Google"): when users should not create a password on your site at all.
Example
Role-based rules with HTTP Basic and two in-memory users (tested on Spring Boot 4.1.1)
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults()) // how clients prove who they are
.csrf(csrf -> csrf.disable()) // no browser session, so no CSRF risk
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
// Demo users. A real application loads users from a database instead.
@Bean
public UserDetailsService users(PasswordEncoder encoder) {
return new InMemoryUserDetailsManager(
User.withUsername("asha").password(encoder.encode("asha-pass")).roles("USER").build(),
User.withUsername("ravi").password(encoder.encode("ravi-pass")).roles("USER", "ADMIN").build());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@RestController
public class Endpoints {
@GetMapping("/api/public/info") public String info() { return "public info"; }
@GetMapping("/api/products/{id}") public String product(@PathVariable long id) { return "product " + id; }
@GetMapping("/api/admin/reports") public String reports() { return "admin reports"; }
@GetMapping("/api/orders") public String orders(Principal user) { return "orders for " + user.getName(); }
}
GET /api/public/info -> 200 public info (no login needed)
GET /api/products/5 -> 200 product 5 (no login needed)
GET /api/orders -> 401 (no credentials)
GET /api/orders -u asha:asha-pass -> 200 orders for asha
GET /api/orders -u asha:wrong -> 401 (wrong password)
GET /api/admin/reports -u asha:asha-pass -> 403 Forbidden (signed in, but no ADMIN role)
GET /api/admin/reports -u ravi:ravi-pass -> 200 admin reports
Without the .httpBasic(...) line, every request to /api/orders and
/api/admin/reports returns 403, with or without a correct password.
Common Mistakes
- Writing authorizeHttpRequests rules without any authentication mechanism (httpBasic, formLogin or oauth2ResourceServer). Nobody can sign in, and every protected URL returns 403.
- Placing a broad rule such as requestMatchers("/api/**").authenticated() above a narrower admin rule, so the admin rule is never reached.
- Writing hasRole("ROLE_ADMIN"). hasRole adds the ROLE_ prefix itself, and the application fails at startup with "ROLE_ADMIN should not start with ROLE_ since ROLE_ is automatically prepended". Use hasRole("ADMIN") or hasAuthority("ROLE_ADMIN").
- Confusing 401 Unauthorized (authentication is missing or invalid) with 403 Forbidden (authenticated, but not authorized for this resource).
- Storing plaintext passwords instead of hashing them with a PasswordEncoder like BCryptPasswordEncoder.
- Leaving CSRF protection enabled on a stateless, token-based REST API where it provides no benefit and only blocks legitimate non-browser clients.
- Copying old tutorials that extend WebSecurityConfigurerAdapter — it was removed in Spring Security 6; use SecurityFilterChain beans.
Exercise
Using the lesson's Role-based rules with HTTP Basic and two in-memory users (tested on Spring Boot 4.1.1) example, create a small implementation or design sketch that demonstrates Spring Security enforces access control through a chain of servlet filters that run before your controllers.. Add a test case or scenario that would catch Writing authorizeHttpRequests rules without any authentication mechanism (httpBasic, formLogin or oauth2ResourceServer). Nobody can sign in, and every protected URL returns 403., and describe the expected result.
Show hints
- Start with The Security Filter Chain and compare your approach with Role-based rules with HTTP Basic and two in-memory users (tested on Spring Boot 4.1.1). Identify the decision or behavior the lesson recommends.
- Walk through your scenario step by step. Confirm that it follows Spring Security enforces access control through a chain of servlet filters that run before your controllers. and that it would catch the failure described by Writing authorizeHttpRequests rules without any authentication mechanism (httpBasic, formLogin or oauth2ResourceServer). Nobody can sign in, and every protected URL returns 403..
Quick quiz
1. Which choice is a mistake to avoid in Spring Security Basics?
- A. Placing a broad rule such as requestMatchers("/api/**").authenticated() above a narrower admin rule, so the admin rule is never reached.
- B. Spring Security enforces access control through a chain of servlet filters that run before your controllers.
- C. Writing authorizeHttpRequests rules without any authentication mechanism (httpBasic, formLogin or oauth2ResourceServer). Nobody can sign in, and every protected URL returns 403.
Show answer
C. Avoid this approach: Writing authorizeHttpRequests rules without any authentication mechanism (httpBasic, formLogin or oauth2ResourceServer). Nobody can sign in, and every protected URL returns 403. The lesson recommends: Spring Security enforces access control through a chain of servlet filters that run before your controllers.
Interview questions
A teammate is about to make this mistake: Writing authorizeHttpRequests rules without any authentication mechanism (httpBasic, formLogin or oauth2ResourceServer). Nobody can sign in, and every protected URL returns 403. How would you diagnose and correct it using the lesson's guidance?
Show answer
Spring Security enforces access control through a chain of servlet filters that run before your controllers. Apply this in The Security Filter Chain; the example Role-based rules with HTTP Basic and two in-memory users (tested on Spring Boot 4.1.1) shows one way to do it. Avoid Writing authorizeHttpRequests rules without any authentication mechanism (httpBasic, formLogin or oauth2ResourceServer). Nobody can sign in, and every protected URL returns 403. and explain how your implementation or design prevents that failure.
Key Points to Remember
- Spring Security enforces access control through a chain of servlet filters that run before your controllers.
- Authentication establishes identity ("who are you"); authorization decides access ("what are you allowed to do").
- SecurityFilterChain with authorizeHttpRequests() is the current, non-deprecated way to configure access rules.
- Passwords must always be hashed (e.g. with BCryptPasswordEncoder), never stored or compared as plaintext.
- Rules are matched top to bottom, first match wins, so specific rules go before anyRequest().
- Access rules need an authentication mechanism; 401 means "sign in", 403 means "signed in but not allowed".
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
These examples need a JDK project with their libraries on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering