Spring Security Basics
Spring Security handles authentication (who are you?) and authorization (what may you do?).
Introduction
Spring Security handles authentication (who are you?) and authorization (what may you do?). It works as a chain of filters that sit in front of your controllers and decide whether each request continues.
It has a reputation for being hard, but the mental model is small: a filter chain, an authentication strategy, and a set of access rules. Once those three click, the rest is configuration.
Understanding the topic
The mental model:
- Every request walks a configurable filter chain.
- Filters extract credentials (form login, JWT, OAuth2, API key…) and build an
Authenticationobject. - An
AuthenticationManagervalidates it against aUserDetailsService(or external provider). - An access decision checks roles or expressions before the controller runs.
- If everything passes, the request reaches your controller with the user available via
SecurityContextHolder.
Syntax reference
A minimal HTTP security setup:
@Configuration@EnableWebSecuritypublic class SecurityConfig {@BeanSecurityFilterChain api(HttpSecurity http) throws Exception {return http.authorizeHttpRequests(reg -> reg.requestMatchers("/api/public/**").permitAll().requestMatchers("/api/admin/**").hasRole("ADMIN").anyRequest().authenticated()).httpBasic(Customizer.withDefaults()).csrf(csrf -> csrf.disable()) // safe for stateless JSON APIs.build();}@BeanPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}}
Informative example
Method-level rules for domain-aware authorization:
@Serviceclass DocumentService {private final DocumentRepository repo;@PreAuthorize("hasRole('ADMIN') or #ownerId == authentication.name")public List<Document> listFor(String ownerId) {return repo.findByOwner(ownerId);}@PostAuthorize("returnObject.ownerId == authentication.name")public Document get(Long id) {return repo.findById(id).orElseThrow();}}
Method-level rules are perfect when the authorization decision depends on the data itself, not just on the URL.
Real-world use
Modern Spring apps almost always pair Spring Security with either JWTs (for stateless APIs consumed by SPAs and mobile clients) or OAuth2 / OIDC (for delegated authentication via Google, GitHub, Keycloak, etc.). The filter chain stays the same — only the filter that extracts credentials changes.
Best practices
- Always design authorization alongside authentication — never bolt it on later.
- Use method-level
@PreAuthorizefor rules that depend on the domain object. - Never store passwords in plain text — always go through a
PasswordEncoder. - Disable CSRF only for stateless APIs; keep it enabled for server-rendered pages.
Common mistakes
- Permitting a path too broadly (
/api/**instead of/api/public/**) and exposing private endpoints. - Forgetting to encode passwords before saving — the next login will silently fail.
- Putting business logic inside a filter — keep filters about authentication, not domain rules.
Hands-on exercise
Build it: secure a tiny app with two endpoints — /api/public/ping (anyone) and /api/admin/stats (only users with role ADMIN). Add an in-memory user store with one regular user and one admin. Test both endpoints with HTTP basic auth and confirm the 401/403 behaviour.