Method-level RBAC and dynamic permission checks, custom AuthenticationProviders, invalidating stateless JWTs, opaque tokens vs JWTs, how @PreAuthorize works behind the scenes, @Secured vs @RolesAllowed vs @PreAuthorize, session-based vs stateless login, OAuth2 vs JWT (not the same kind of thing), and Spring Authorization Server.
Published September 25, 2026
The basics (the filter chain, SecurityFilterChain, JWT flows) are covered in the 5–8 years tier. This lesson goes deeper: authorisation internals, token lifecycle trade-offs, and identity-provider choices. Use Spring Security 6 APIs (AuthorizationManager, @EnableMethodSecurity).
Short answer:
@EnableMethodSecurity (Spring Security 6; it replaces @EnableGlobalMethodSecurity).@PreAuthorize("hasRole('ADMIN')");hasAuthority('SCOPE_orders.write');@PreAuthorize("#userId == authentication.name");@PostAuthorize("returnObject.ownerId == authentication.name"), for object-level checks on results;@PreFilter/@PostFilter for collections (with care, because they filter in memory).@PreAuthorize("@orderPolicy.canApprove(authentication, #orderId)"), or implement a PermissionEvaluator (hasPermission(#id, 'Order', 'APPROVE')). The policy bean can consult a database, ABAC attributes, or an external policy engine (OPA, Cerbos).JwtAuthenticationConverter), or from the database. Use a RoleHierarchy bean for ADMIN > MANAGER > USER.@Component("orderPolicy")
class OrderPolicy {
boolean canApprove(Authentication auth, UUID orderId) {
Order o = orders.find(orderId);
return auth.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_MANAGER"))
&& o.region().equals(regionOf(auth))
&& o.total().compareTo(limitFor(auth)) <= 0; // approval limit per manager
}
}
@PreAuthorize("@orderPolicy.canApprove(authentication, #orderId)")
public void approve(UUID orderId) { ... }
Learn it in depth → Role-Based Access Control
AuthenticationProvider?Short answer: Implement AuthenticationProvider:
authenticate(Authentication) validates the credentials in the incoming token, and returns an authenticated Authentication, with authorities. It throws AuthenticationException subclasses on failure (BadCredentialsException, LockedException), or returns null to let other providers try.supports(Class<?>) declares which Authentication types it handles.Register it as a bean, or on HttpSecurity/AuthenticationManager. ProviderManager iterates through the providers.
@Component
class ApiKeyAuthenticationProvider implements AuthenticationProvider {
private final ApiKeyService keys;
ApiKeyAuthenticationProvider(ApiKeyService keys) { this.keys = keys; }
@Override public Authentication authenticate(Authentication auth) {
String presented = (String) auth.getCredentials();
ApiClient client = keys.findByHashedKey(sha256(presented)) // store hashes, never raw keys
.orElseThrow(() -> new BadCredentialsException("Invalid API key"));
return ApiKeyAuthenticationToken.authenticated(client.id(), client.authorities());
}
@Override public boolean supports(Class<?> type) { return ApiKeyAuthenticationToken.class.isAssignableFrom(type); }
}
It's paired with a filter (or AuthenticationConverter + AuthenticationFilter) that extracts the credential from the request.
Use cases: API keys, a legacy SSO token, OTP or MFA steps, LDAP plus custom rules, and partner-issued signatures. Use constant-time comparisons, rate limiting and audit logging.
Short answer: You can't "delete" a self-contained signed token, so you limit its life, and add state where necessary:
jti values (in Redis, with a TTL equal to the token's remaining lifetime), checked by the resource servers. It adds a lookup per request, but only for the short remaining window.tokenVersion or sessionVersion claim, compared against the user's current version (bumped on password change or "log out everywhere").Short answer:
| JWT (self-contained) | Opaque token (reference) | |
|---|---|---|
| Content | Signed claims (sub, scope, exp…), readable by anyone | A random string, meaningless to clients and resource servers |
| Validation | Locally: signature + claims, through the JWKS (fast, no network call) | Introspection call to the authorisation server (RFC 7662), usually cached |
| Revocation | Hard: wait for expiry, or use a denylist | Immediate: the authorisation server says it's inactive |
| Privacy | Claims are exposed (unless encrypted) | Nothing leaks |
| Size | Larger headers | Small |
| Coupling | Resource servers trust the issuer's keys | Resource servers depend on the authorisation server being available |
Spring's resource server supports both: oauth2ResourceServer(o -> o.jwt(...)) or .opaqueToken(...). A common hybrid is opaque tokens at the edge, exchanged by the gateway for internal JWTs (the "phantom token" pattern).
@PreAuthorize work behind the scenes?Short answer:
@EnableMethodSecurity registers advisors, including an AuthorizationManagerBeforeMethodInterceptor backed by PreAuthorizeAuthorizationManager.@PreAuthorize get an AOP proxy, so the proxy rules apply (self-invocation and private methods are skipped).Authentication from the SecurityContextHolder;MethodSecurityExpressionHandler evaluation context (the authentication, the method arguments as #name through parameter-name discovery, beans through @bean, plus hasRole, hasAuthority and hasPermission functions);AccessDeniedException (mapped to 403 by ExceptionTranslationFilter in web requests);@PostAuthorize evaluates after execution, with returnObject available, so side effects have already happened. Use it for reads only.
@Secured, @RolesAllowed and @PreAuthorize differ?Short answer:
@Secured("ROLE_ADMIN"): Spring's legacy annotation. It's a simple list of authorities, with no SpEL. You enable it with @EnableMethodSecurity(securedEnabled = true).@RolesAllowed("ADMIN"): a JSR-250 / Jakarta standard annotation (portable). The role prefix is added automatically. Enable it with jsr250Enabled = true.@PreAuthorize/@PostAuthorize: Spring, SpEL-based: arguments, the return object, bean methods, and complex conditions. They're enabled by default with @EnableMethodSecurity, and they're the most powerful and the most common today.Use @PreAuthorize for anything beyond static roles. Avoid mixing styles in one codebase.
Short answer:
SecurityContext in an HTTP session, and the browser sends a session cookie (HttpOnly, Secure, SameSite).
SessionCreationPolicy.STATELESS).
Short answer: They're different kinds of things, so the question is a common trap:
OAuth2 access tokens may be JWTs, or opaque strings. You can also use JWTs without OAuth2 (a home-grown login that issues JWTs), but then you must build what OAuth2 already standardises: key rotation, refresh, revocation, scopes, and client registration.
Short answer: Spring's OAuth 2.1 and OpenID Connect 1.0 authorisation server framework (it replaced the deprecated Spring Security OAuth). It lets you run your own identity provider on Spring Boot:
When to use it: when you need a customised, embedded identity provider (special claims, a multi-tenant setup, integration with a legacy user store) and have the security expertise to run it. Otherwise, a managed or off-the-shelf IdP (Keycloak, Okta/Auth0, Entra ID, Cognito) reduces risk and effort.
Q: How is the SecurityContext propagated to @Async threads, or reactive code?
A: Through DelegatingSecurityContextExecutor or TaskDecorator wrappers (or SecurityContextHolder strategies) for thread pools. In WebFlux, it's carried in the Reactor Context (ReactiveSecurityContextHolder), not in a ThreadLocal.
Q: Why does hasRole('ADMIN') check for ROLE_ADMIN?
A: hasRole automatically adds the ROLE_ prefix. hasAuthority('ROLE_ADMIN') checks the exact string. OAuth2 scopes arrive as SCOPE_x authorities by default, so use hasAuthority('SCOPE_orders.read').
Q: How do you test method security?
A: With @WithMockUser(roles = "ADMIN"), @WithUserDetails, or custom @WithSecurityContext annotations, in slice or integration tests. Assert that AccessDeniedException is thrown for unauthorised calls.
Q: What is AuthorizationManager?
A: Spring Security 6's unified authorisation abstraction (check(authentication, object) returning an AuthorizationDecision). It's used for both request authorisation (authorizeHttpRequests) and method security, replacing the older AccessDecisionManager/voters.