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.

flowchart TB req["HTTP request"] --> dfp["DelegatingFilterProxy\n(registered in the servlet container)"] dfp --> fcp["FilterChainProxy"] fcp --> pick{"first SecurityFilterChain\nwhose matcher accepts\nthe request"} pick --> sfc subgraph sfc["SecurityFilterChain (ordered filters)"] direction TB f1["SecurityContextHolderFilter\nload SecurityContext from the repository"] f2["authentication filter\ne.g. UsernamePasswordAuthenticationFilter,\nBearerTokenAuthenticationFilter"] f3["AuthorizationFilter\nevaluate authorizeHttpRequests rules"] f1 --> f2 --> f3 end f2 --> am["AuthenticationManager\n(ProviderManager -> AuthenticationProvider)"] am --> ctx["SecurityContext\nheld in SecurityContextHolder,\npersisted by SecurityContextRepository"] f3 --> app["DispatcherServlet -> your controller"] ctx --> app

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. prePostEnabled is on by default, so @PreAuthorize / @PostAuthorize / @PreFilter / @PostFilter work immediately. securedEnabled = true turns on @Secured; jsr250Enabled = true turns on @RolesAllowed / @PermitAll / @DenyAll.

  • @EnableReactiveMethodSecurity — the WebFlux stack, for methods that return Mono / 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; needs securedEnabled = true.

  • @RolesAllowed("ADMIN") — the JSR-250 equivalent; needs jsr250Enabled = 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 the principal of the current Authentication (typically a UserDetails, an OidcUser, or a Jwt). On WebFlux the parameter can be Mono<MyUser>.

  • @CurrentSecurityContext — inject the SecurityContext, or a piece of it via an expression (@CurrentSecurityContext(expression = "authentication") Authentication auth).

  • A plain Authentication or java.security.Principal parameter 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

@PreAuthorize / @PostAuthorize

@EnableMethodSecurity (default) / @EnableReactiveMethodSecurity

service

both

@PreFilter / @PostFilter

@EnableMethodSecurity (default) / @EnableReactiveMethodSecurity

service

both

@Secured

@EnableMethodSecurity(securedEnabled = true)

service

both

@RolesAllowed / @PermitAll / @DenyAll

@EnableMethodSecurity(jsr250Enabled = true)

service

both

@AuthorizeReturnObject

@EnableMethodSecurity

service

both

authorizeHttpRequests / requestMatchers

SecurityFilterChain bean

web (URL)

servlet

authorizeExchange / pathMatchers

SecurityWebFilterChain bean

web (URL)

reactive

@AuthenticationPrincipal / @CurrentSecurityContext

argument resolver (always on)

web

both (reactive may wrap in Mono)

@WithMockUser / @WithUserDetails / @WithSecurityContext

spring-security-test

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 the HttpSession.

  • 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();
    }
}
flowchart LR subgraph servlet["Servlet stack"] direction TB s_hold["SecurityContextHolder\n(ThreadLocal strategy)"] s_filter["SecurityContextHolderFilter"] s_repo["SecurityContextRepository\n(HttpSession / RequestAttribute / Null)"] s_chain["SecurityFilterChain\n+ AuthenticationManager (blocking)"] s_filter --> s_hold s_repo --> s_filter s_chain --> s_hold end subgraph reactive["Reactive stack (WebFlux)"] direction TB r_hold["ReactiveSecurityContextHolder\n(Reactor Context, not ThreadLocal)"] r_filter["AuthenticationWebFilter"] r_repo["ServerSecurityContextRepository\n(WebSession / NoOp)"] r_chain["SecurityWebFilterChain\n+ ReactiveAuthenticationManager (non-blocking)"] r_filter --> r_hold r_repo --> r_filter r_chain --> r_hold end
Concern Servlet Reactive

Holder

SecurityContextHolder (ThreadLocal)

ReactiveSecurityContextHolder (Reactor Context)

Per-request storage

thread-bound, cleared by SecurityContextHolderFilter

subscriber context, scoped to the Mono/Flux

Cross-request repository

SecurityContextRepository (HttpSessionSecurityContextRepository)

ServerSecurityContextRepository (WebSessionServerSecurityContextRepository)

Context-loading filter

SecurityContextHolderFilter

ReactorContextWebFilter (+ AuthenticationWebFilter)

Config annotation

@EnableWebSecurity

@EnableWebFluxSecurity

Chain bean

SecurityFilterChain

SecurityWebFilterChain

Manager

AuthenticationManager / ProviderManager

ReactiveAuthenticationManager

Building the SecurityContext from credentials

The classic username/password flow, filter to context:

flowchart TB form["POST /login (username + password)"] --> upf["UsernamePasswordAuthenticationFilter\n(an AbstractAuthenticationProcessingFilter)"] upf --> tok["unauthenticated\nUsernamePasswordAuthenticationToken"] tok --> am["AuthenticationManager\n= ProviderManager"] am --> dap["DaoAuthenticationProvider"] dap --> uds["UserDetailsService.loadUserByUsername(username)"] dap --> pe["PasswordEncoder.matches(raw, encoded)"] uds --> ok{"user found\n& password matches?"} pe --> ok ok -- "yes" --> auth["authenticated Authentication\n(principal = UserDetails, authorities set)"] ok -- "no" --> fail["AuthenticationException -> 401"] auth --> hold["SecurityContextHolder.getContext().setAuthentication(...)"] hold --> save["SecurityContextRepository.saveContext(...)"]

Interfaces and beans to implement

  • A UserDetailsService — loadUserByUsername(String) returning a UserDetails. Back it with your own table. Either have the domain entity implement UserDetails, or map it through a small adapter.

  • A PasswordEncoder @Bean — use PasswordEncoderFactories.createDelegatingPasswordEncoder(), which stores an {id} prefix ({bcrypt}, {argon2}, …​) so algorithms can be upgraded over time.

  • Optionally a custom AuthenticationProvider, or an exposed AuthenticationManager @Bean — build a ProviderManager around a DaoAuthenticationProvider (constructed with the UserDetailsService, with the PasswordEncoder set) — for the stateless variant below.

  • GrantedAuthoritiesMapper and/or a RoleHierarchy bean (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

BearerTokenAuthenticationFilterBearerTokenAuthenticationConverter (pulls the token from the Authorization: Bearer header) → JwtAuthenticationProviderJwtDecoder (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.