Spring Boot JWT authentication is the default way most teams secure a REST API today. Instead of keeping a session in server memory, the client carries a signed JSON Web Token on every request, and the server verifies it. No session table, no sticky load balancing, no “which node has my session?” — which is exactly why it fits SPAs, mobile apps, and microservices. Let’s wire it up properly, and talk about the parts that bite you later.

The authentication flow

The whole dance is four steps:

  1. The client POSTs credentials to a login endpoint (e.g. /auth/login).
  2. You validate them, then mint a signed JWT containing claims like the subject (username), roles, issued-at, and expiry.
  3. The client stores that token and sends it on every subsequent request as Authorization: Bearer <token>.
  4. A filter on the server validates the signature and expiry, builds an Authentication, and drops it into the SecurityContext.

The key property: the token is signed, not encrypted. Anyone can decode and read the claims (they’re just base64url) — the signature only proves the server issued it and nobody tampered with it. So never put secrets in a JWT, and always serve it over HTTPS.

Creating the token at login

Spring Security doesn’t ship a JWT encoder/decoder for symmetric signing out of the box, so most people reach for jjwt or Nimbus JOSE. A token service typically wraps a signing key and exposes generateToken(user) and extractUsername(token).

Version heads-up [needs source]: jjwt’s builder API changed between 0.11.x (setSubject, signWith(key, SignatureAlgorithm.HS256)) and 0.12.x (subject, signWith(key)). Check the version in your pom.xml before copying snippets from older tutorials — that mismatch is the #1 reason these examples don’t compile.

Whatever library you use, the rules are the same: sign with a strong key (256-bit minimum for HS256), set a short expiry on the access token (15–60 minutes is typical), and store the signing secret in config/secret manager, never in source.

Validating tokens with a filter

This is the heart of it. Add a OncePerRequestFilter that runs before Spring’s username/password filter, pulls the bearer token, validates it, and authenticates the request:

@Component
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtService jwtService;

    public JwtAuthFilter(JwtService jwtService) {
        this.jwtService = jwtService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain)
            throws ServletException, IOException {

        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);
            try {
                String username = jwtService.extractUsername(token); // throws if bad/expired
                if (username != null
                        && SecurityContextHolder.getContext().getAuthentication() == null) {
                    var auth = new UsernamePasswordAuthenticationToken(
                            username, null, jwtService.extractAuthorities(token));
                    auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(auth);
                }
            } catch (JwtException ex) {
                // Invalid or expired token: leave the context empty.
                // The entry point will answer with 401 for protected routes.
            }
        }
        chain.doFilter(request, response);
    }
}

Note what it does not do: it doesn’t reject the request itself. It just authenticates when it can and steps aside when it can’t. Authorization rules decide the rest.

Wiring it into Spring Security

On Spring Boot 3 (Spring Security 6) you configure a SecurityFilterChain bean with the lambda DSL. Make the API stateless and slot your filter in:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthFilter) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // safe for a token API with no cookies
            .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**").permitAll()
                .anyRequest().authenticated())
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

STATELESS is the line that matters — it tells Spring not to create or use an HttpSession, which is the entire point of going token-based.

Using it in your controllers

Once the context is populated, your controllers behave like any secured Spring app. Method security gives you fine-grained control:

@GetMapping("/admin/reports")
@PreAuthorize("hasRole('ADMIN')")
public List<Report> reports() { ... }

Add @EnableMethodSecurity to turn on @PreAuthorize. When a token is missing or invalid, the request fails authorization and your error handling returns a clean 401/403 — wire that through the same global exception handler you use for everything else so clients get a consistent JSON error instead of a stack trace.

Common pitfalls

  • Long-lived access tokens. You can’t revoke a JWT once issued. Keep access tokens short and add refresh tokens (stored server-side, revocable) for longer sessions.
  • No logout story. “Logout” with stateless JWTs means deleting the token client-side and/or maintaining a short server-side denylist for the access-token window.
  • Weak or hardcoded secrets. A leaked HS256 key lets anyone forge tokens. Rotate keys and externalize config.
  • Trusting unverified claims. Always validate signature and expiry before reading any claim.
  • CSRF confusion. Disabling CSRF is fine for a bearer-token API, but not if you store the token in a cookie — then you need CSRF protection again.

FAQ

Is a JWT encrypted?
No. It’s signed and base64url-encoded, so the payload is readable by anyone. Don’t store sensitive data in it; rely on HTTPS for confidentiality in transit.
JWT or server sessions?
Sessions are simpler and instantly revocable — great for a classic monolith with a browser. JWTs shine when you have multiple clients or services and want to avoid shared session state.
How do I revoke a token?
You don’t, directly. Use short expiries plus refresh tokens, or keep a denylist of token IDs (jti) until they expire.
Where should the client store the token?
An HttpOnly cookie (with CSRF protection) or in-memory. Avoid localStorage if you can — it’s reachable by any XSS on the page.

Key takeaway: Spring Boot JWT authentication = a signed token on every request + a stateless SecurityFilterChain + a OncePerRequestFilter that validates and authenticates. Keep access tokens short, sign with a strong externalized key, add refresh tokens for real sessions, and serve everything over HTTPS.

New to securing APIs? Start with a plain endpoint first — see Your First Spring Boot REST Controller in 5 Minutes — then layer security on top.