Spring Security
|
This section documents Spring Boot 4.1.x and Spring Framework 7.0.x on the Java 17/21+ baseline, as described by the official Spring Boot reference documentation and each sub-project’s own site (Spring Data, Spring for Apache Kafka, Micrometer, Project Reactor, springdoc-openapi, Spring gRPC, and the others listed in this section’s Bibliography) — which are the references these pages are written and verified against. This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production. Spring Boot and its ecosystem continue to evolve; the examples here target the current 4.1.x / 7.0.x releases. This section’s bibliography lists the reference material consulted while preparing these pages. |
Spring Security is the authentication and authorization layer for a Spring Boot service. This page covers the
model it is built on — a chain of servlet filters — the annotations you use day to day, how the
SecurityContext is stored on the servlet and reactive stacks, and the two most common ways the context gets
populated: from a username/password credential and from a bearer JWT validated as an OAuth2 Resource Server.
It is what secures the endpoints built in REST APIs and
gRPC APIs. Issuing those tokens yourself, and "Sign in with Google /
Microsoft / Apple", are covered on the companion page
Authorization Server & Social Login.
The annotation-driven parts of this page assume familiarity with Spring’s core annotations, and every YAML block here is bound the usual way — see Configuration & Profiles for how to keep secrets and issuer URLs per-environment.
The model
Authentication answers "who is the caller?"; authorization answers "is this caller allowed to do this?". Spring Security does both, and it does them for a request before it reaches your controller.
On the servlet stack it is a chain of jakarta.servlet.Filter instances placed in front of the application.
Spring registers a single DelegatingFilterProxy in the servlet container; that proxy delegates to a
FilterChainProxy bean, which owns one or more SecurityFilterChain instances. Each SecurityFilterChain
pairs a request matcher with an ordered list of security filters (context, CSRF, authentication, exception
translation, authorization, …). The first chain whose matcher accepts the request handles it; the rest are
skipped.
The Spring Security 7 configuration style is a SecurityFilterChain @Bean built with the lambda DSL. The
removed WebSecurityConfigurerAdapter base class, the .and() chaining style, authorizeRequests(…), and
antMatchers(…) are all gone — use authorizeHttpRequests(…) with requestMatchers(…) instead.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**", "/actuator/health").permitAll()
.anyRequest().authenticated())
.formLogin(withDefaults())
.httpBasic(withDefaults());
return http.build();
}
}
withDefaults() is org.springframework.security.config.Customizer.withDefaults(). With no
SecurityFilterChain bean at all, Spring Boot still auto-configures one that authenticates every request with a
generated password — defining the bean replaces that default.
Typical annotations
Enabling method security
Rules on the URL are coarse; rules on a service method are where most authorization actually lives. Enable it
with one annotation on a @Configuration class:
-
@EnableMethodSecurity— the servlet/MVC stack.prePostEnabledis on by default, so@PreAuthorize/@PostAuthorize/@PreFilter/@PostFilterwork immediately.securedEnabled = trueturns on@Secured;jsr250Enabled = trueturns on@RolesAllowed/@PermitAll/@DenyAll. -
@EnableReactiveMethodSecurity— the WebFlux stack, for methods that returnMono/Flux.
Both are backed by AuthorizationManager-based method interceptors. The old
@EnableGlobalMethodSecurity (and its GlobalMethodSecurityConfiguration) has been removed in Spring
Security 7 — use @EnableMethodSecurity.
@PreAuthorize / @PostAuthorize
Both take a SpEL expression. @PreAuthorize runs before the method; @PostAuthorize runs after and can inspect
the return value via returnObject. Useful expression elements: hasRole('ADMIN'), hasAuthority('SCOPE_read'),
hasAnyRole('ADMIN','OPS'), authentication, principal, method arguments as #argName, and calls to other
beans as @beanName.method(#x).
@Service
public class DocumentService {
@PreAuthorize("hasRole('EDITOR') and #ownerId == authentication.name")
public void update(String ownerId, DocumentPatch patch) { /* ... */ }
@PostAuthorize("returnObject.ownerId == authentication.name or hasRole('ADMIN')")
public Document findById(String id) { /* ... */ }
@PreAuthorize("@documentPermissions.canDelete(#id, authentication)")
public void delete(String id) { /* ... */ }
}
@PreFilter / @PostFilter
Filter a collection, array, or Stream in place rather than authorizing the call as a whole. @PreFilter
trims an argument before the method sees it; @PostFilter trims the return value. The element under test is
filterObject.
@PostFilter("filterObject.ownerId == authentication.name or hasRole('ADMIN')")
public List<Document> findAll() { /* ... */ }
@PreFilter("filterObject.status != 'LOCKED'")
public void saveAll(List<Document> documents) { /* ... */ }
@Secured and JSR-250
-
@Secured("ROLE_ADMIN")— a plain authority check, no SpEL; needssecuredEnabled = true. -
@RolesAllowed("ADMIN")— the JSR-250 equivalent; needsjsr250Enabled = true. -
@PermitAll/@DenyAll— JSR-250 unconditional allow / deny; same opt-in flag.
@AuthorizeReturnObject and meta-annotations
@AuthorizeReturnObject on a method (or type) makes Spring Security proxy the returned object so that
@PreAuthorize / @PostAuthorize on the returned type’s own getters are enforced when the caller reads them — handy for masking fields on a DTO.
Any of these annotations can be composed into a project-specific meta-annotation:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
public @interface IsAdmin {
}
Controller and argument annotations
-
@AuthenticationPrincipal— inject theprincipalof the currentAuthentication(typically aUserDetails, anOidcUser, or aJwt). On WebFlux the parameter can beMono<MyUser>. -
@CurrentSecurityContext— inject theSecurityContext, or a piece of it via an expression (@CurrentSecurityContext(expression = "authentication") Authentication auth). -
A plain
Authenticationorjava.security.Principalparameter is resolved the same way.
@GetMapping("/api/me")
public MeResponse me(@AuthenticationPrincipal UserDetails user, Authentication authentication) {
return new MeResponse(user.getUsername(), authentication.getAuthorities());
}
Testing
spring-security-test provides annotations that populate the SecurityContext for a test method:
@WithMockUser(roles = "ADMIN"), @WithUserDetails("alice") (runs the real UserDetailsService), and
@WithSecurityContext for a custom factory. For request-level testing, MockMvc /
WebTestClient take .with(…) / .mutateWith(…) post-processors such as
SecurityMockMvcRequestPostProcessors.jwt(). See
Unit & Integration Testing.
Reference table
| Annotation | Enabled by | Layer | Servlet / reactive |
|---|---|---|---|
|
|
service |
both |
|
|
service |
both |
|
|
service |
both |
|
|
service |
both |
|
|
service |
both |
|
|
web (URL) |
servlet |
|
|
web (URL) |
reactive |
|
argument resolver (always on) |
web |
both (reactive may wrap in |
|
|
test |
both |
How the SecurityContext works
The authenticated identity for the current request lives in a SecurityContext, which holds a single
Authentication. An Authentication has a principal (who), credentials (proof, usually cleared after
authentication), and a collection of GrantedAuthority (what they can do — roles are just authorities with a
ROLE_ prefix). Where that context is stored differs by stack.
Servlet
SecurityContextHolder is a static holder backed by a SecurityContextHolderStrategy. The default strategy is
MODE_THREADLOCAL (a ThreadLocal); MODE_INHERITABLETHREADLOCAL propagates the context to child threads.
Read the current principal with:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Across requests the context is persisted by a SecurityContextRepository:
-
HttpSessionSecurityContextRepository— the default; stores the context in theHttpSession. -
RequestAttributeSecurityContextRepository— request-scoped only, for stateless APIs that still want the context available later in the same request. -
NullSecurityContextRepository— store nothing (fully stateless).
SecurityContextHolderFilter runs early in the chain: it asks the repository for a DeferredSecurityContext
(loaded lazily) and sets it on the holder, then clears the holder in a finally block when the request
finishes, so nothing leaks to the next request on the same pooled thread. (SecurityContextHolderFilter
replaced the older SecurityContextPersistenceFilter; it never saves implicitly — authentication mechanisms
save explicitly.)
Reactive (WebFlux)
There is no request thread to pin a ThreadLocal to — a reactive pipeline hops threads between operators. So
the context is carried in the Reactor Context instead. ReactiveSecurityContextHolder.getContext() returns
a Mono<SecurityContext> that reads from the subscriber context; Spring Security’s AuthenticationWebFilter
writes it there with contextWrite(…). A controller can also take Mono<Principal> or call
exchange.getPrincipal().
Mono<String> currentUsername = ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.map(Authentication::getName);
Configuration is a SecurityWebFilterChain @Bean on a class annotated @EnableWebFluxSecurity, with
authorizeExchange(…) / pathMatchers(…). Cross-request storage is a ServerSecurityContextRepository — WebSessionServerSecurityContextRepository (the default, backed by the WebSession) or
NoOpServerSecurityContextRepository for stateless APIs. See
Reactive Programming for how the Reactor Context and
context propagation work in general.
@Configuration
@EnableWebFluxSecurity
public class ReactiveSecurityConfig {
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return http
.authorizeExchange(ex -> ex
.pathMatchers("/public/**").permitAll()
.anyExchange().authenticated())
.httpBasic(withDefaults())
.build();
}
}
| Concern | Servlet | Reactive |
|---|---|---|
Holder |
|
|
Per-request storage |
thread-bound, cleared by |
subscriber context, scoped to the |
Cross-request repository |
|
|
Context-loading filter |
|
|
Config annotation |
|
|
Chain bean |
|
|
Manager |
|
|
Building the SecurityContext from credentials
The classic username/password flow, filter to context:
Interfaces and beans to implement
-
A
UserDetailsService—loadUserByUsername(String)returning aUserDetails. Back it with your own table. Either have the domain entity implementUserDetails, or map it through a small adapter. -
A
PasswordEncoder@Bean— usePasswordEncoderFactories.createDelegatingPasswordEncoder(), which stores an{id}prefix ({bcrypt},{argon2}, …) so algorithms can be upgraded over time. -
Optionally a custom
AuthenticationProvider, or an exposedAuthenticationManager@Bean— build aProviderManageraround aDaoAuthenticationProvider(constructed with theUserDetailsService, with thePasswordEncoderset) — for the stateless variant below. -
GrantedAuthoritiesMapperand/or aRoleHierarchybean (RoleHierarchyImpl.fromHierarchy("ROLE_ADMIN > ROLE_USER")) to expand or remap authorities.
@Configuration
@EnableWebSecurity
public class CredentialSecurityConfig {
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
UserDetailsService userDetailsService(UserRepository users) {
return username -> users.findByUsername(username)
.map(AppUserDetails::new)
.orElseThrow(() -> new UsernameNotFoundException(username));
}
@Bean
AuthenticationManager authenticationManager(UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return new ProviderManager(provider);
}
}
Database model
(a) The Spring Security default JDBC schema — ships with the framework as users.ddl; use it directly with
JdbcUserDetailsManager (a UserDetailsService plus create/update/delete). Optional groups /
group_authorities / group_members tables add group-based authorities.
create table users (
username varchar(50) not null primary key,
password varchar(500) not null,
enabled boolean not null
);
create table authorities (
username varchar(50) not null,
authority varchar(50) not null,
constraint fk_authorities_users foreign key (username) references users (username)
);
create unique index ix_auth_username on authorities (username, authority);
-- optional group support
create table groups (
id bigint generated by default as identity primary key,
group_name varchar(50) not null
);
create table group_authorities (
group_id bigint not null,
authority varchar(50) not null,
constraint fk_group_authorities_group foreign key (group_id) references groups (id)
);
create table group_members (
id bigint generated by default as identity primary key,
username varchar(50) not null,
group_id bigint not null,
constraint fk_group_members_group foreign key (group_id) references groups (id)
);
@Bean
UserDetailsService jdbcUsers(DataSource dataSource) {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
manager.setEnableGroups(true);
return manager;
}
(b) A JPA version — your own entities plus a Spring Data repository. See Spring Data JPA.
@Entity
@Table(name = "app_user")
public class AppUser {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password; // already {bcrypt}-encoded
private boolean enabled = true;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "app_user_authority", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "authority")
private Set<String> authorities = new HashSet<>();
// getters / setters
}
public interface UserRepository extends JpaRepository<AppUser, Long> {
Optional<AppUser> findByUsername(String username);
}
public class AppUserDetails implements UserDetails {
private final AppUser user;
public AppUserDetails(AppUser user) { this.user = user; }
@Override public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getAuthorities().stream().map(SimpleGrantedAuthority::new).toList();
}
@Override public String getPassword() { return user.getPassword(); }
@Override public String getUsername() { return user.getUsername(); }
@Override public boolean isEnabled() { return user.isEnabled(); }
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
}
spring:
sql:
init:
mode: embedded # run schema.sql/data.sql for embedded DBs; use a migration tool otherwise
datasource:
url: jdbc:postgresql://localhost:5432/app
username: app
password: app
Stateless / REST variant
For a token API there is no session: authenticate once at a login endpoint, hand back a signed token, and let
every later request carry it. Set SessionCreationPolicy.STATELESS and a NullSecurityContextRepository so the
filter chain stores nothing, and call the AuthenticationManager directly from the controller.
@Bean
SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.securityContext(c -> c.securityContextRepository(new NullSecurityContextRepository()))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/login").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(o -> o.jwt(withDefaults())); // verify our own token -- see below
return http.build();
}
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final TokenService tokenService; // your JWT signer (e.g. Nimbus / JwtEncoder)
public AuthController(AuthenticationManager authenticationManager, TokenService tokenService) {
this.authenticationManager = authenticationManager;
this.tokenService = tokenService;
}
@PostMapping("/login")
public TokenResponse login(@RequestBody @Valid LoginRequest request) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.username(), request.password()));
return new TokenResponse(tokenService.issue(authentication));
}
}
The issued token is then verified on every subsequent request as a JWT — see Building the SecurityContext from a JWT.
Reactive equivalent
Implement ReactiveUserDetailsService (findByUsername returning Mono<UserDetails>), and expose a
ReactiveAuthenticationManager — typically UserDetailsRepositoryReactiveAuthenticationManager wrapping that
service plus the PasswordEncoder.
@Bean
ReactiveAuthenticationManager reactiveAuthenticationManager(
ReactiveUserDetailsService uds, PasswordEncoder encoder) {
UserDetailsRepositoryReactiveAuthenticationManager manager =
new UserDetailsRepositoryReactiveAuthenticationManager(uds);
manager.setPasswordEncoder(encoder);
return manager;
}
Building the SecurityContext from a JWT
Validating an incoming bearer token is the OAuth2 Resource Server role. For the token format itself — the
JOSE family, algorithm choice, JWKS and key rotation, the classic alg/kid attacks, and the full
validation checklist this section’s Spring wiring implements — see
JWT and the JOSE Family. Add:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
@Bean
SecurityFilterChain resourceServer(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://issuer.example.com # discovers jwk-set-uri, validates iss
# jwk-set-uri: https://issuer.example.com/oauth2/jwks
# public-key-location: classpath:public.pem # single RSA key instead of a JWK set
jws-algorithms: RS256
audiences: https://api.example.com # adds an audience validator
The chain
BearerTokenAuthenticationFilter → BearerTokenAuthenticationConverter (pulls the token from the
Authorization: Bearer header) → JwtAuthenticationProvider → JwtDecoder (NimbusJwtDecoder, built with
withIssuerLocation(…), withJwkSetUri(…), withPublicKey(…), or withSecretKey(…)) → an
OAuth2TokenValidator<Jwt> chain — JwtTimestampValidator (exp/nbf), JwtIssuerValidator (iss), an audience
check via JwtClaimValidator("aud", …), all composed with DelegatingOAuth2TokenValidator → a
JwtAuthenticationConverter wrapping a JwtGrantedAuthoritiesConverter (default: map the scope / scp claim
to SCOPE_ authorities) → a JwtAuthenticationToken whose principal is the Jwt and whose name is the
sub claim.
Customizing
Map roles from a custom claim, and optionally look up the local AppUser:
@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter scopes = new JwtGrantedAuthoritiesConverter(); // SCOPE_ prefix
JwtGrantedAuthoritiesConverter roles = new JwtGrantedAuthoritiesConverter();
roles.setAuthoritiesClaimName("roles");
roles.setAuthorityPrefix("ROLE_");
Converter<Jwt, Collection<GrantedAuthority>> combined = jwt -> {
Collection<GrantedAuthority> all = new ArrayList<>(scopes.convert(jwt));
all.addAll(roles.convert(jwt));
return all;
};
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(combined);
return converter;
}
@Bean
JwtDecoder jwtDecoder(OAuth2ResourceServerProperties properties) {
NimbusJwtDecoder decoder = NimbusJwtDecoder
.withIssuerLocation(properties.getJwt().getIssuerUri())
.build();
OAuth2TokenValidator<Jwt> withDefaults =
JwtValidators.createDefaultWithIssuer(properties.getJwt().getIssuerUri());
OAuth2TokenValidator<Jwt> audience =
new JwtClaimValidator<List<String>>("aud", aud -> aud.contains("https://api.example.com"));
decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(withDefaults, audience));
return decoder;
}
DelegatingJwtGrantedAuthoritiesConverter is the framework’s own way to combine several
Converter<Jwt, Collection<GrantedAuthority>> if you prefer it to the hand-written lambda above. To resolve a
local user, wrap the whole thing in a Converter<Jwt, ? extends AbstractAuthenticationToken> that builds a
JwtAuthenticationToken (or a custom token) carrying your AppUserDetails as the principal.
Reactive equivalent
NimbusReactiveJwtDecoder, JwtReactiveAuthenticationManager, ServerBearerTokenAuthenticationConverter, and
ReactiveJwtAuthenticationConverter, wired through http.oauth2ResourceServer(o → o.jwt(withDefaults())) on a
SecurityWebFilterChain.
Opaque tokens
If the token is not a JWT but a random string, validate it by calling the issuer’s introspection endpoint
(RFC 7662): set spring.security.oauth2.resourceserver.opaque-token.introspection-uri (plus client-id /
client-secret) and use http.oauth2ResourceServer(o → o.opaqueToken(withDefaults())). The extension point is
an OpaqueTokenIntrospector (SpringOpaqueTokenIntrospector by default) whose result becomes the
Authentication; a OpaqueTokenAuthenticationConverter customizes the mapping. For when to prefer an opaque
token over a JWT in the first place, and for revocation and token status lists, see
Opaque Tokens, Introspection and
Revocation.