Authentication remains one of the most misunderstood and poorly implemented aspectsof modern backend systems.
Not because frameworks are weak — but because security requires correct design decisions, not just dependencies.

In this article, we’ll walk through how to properly secure Spring Boot APIs using Spring Security and JWT, with:

1. Why Authentication Still Breaks in Modern Applications

Despite mature frameworks, authentication remains a top attack surface due to:

Security failures are usually design failures, not tooling failures.

Spring Security is powerful — but only if used intentionally.

2. Encryption vs Hashing: The Most Common Password Mistake

A critical clarification:

Encryption

Hashing

Reversible

One-way

Requires key

No key

Bad for passwords

Correct for passwords

Why passwords must NOT be encrypted

What secure password storage requires

Spring Security solves this with BCrypt.

3. Password Hashing That Actually Works (BCrypt)

Configuration

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

Password creation


String hashed = passwordEncoder.encode("password");

Password verification


passwordEncoder.matches(rawPassword, storedHash);

4. JWT: Why Tokens Instead of Sessions?

JWT (JSON Web Tokens) enable stateless authentication.

Session-based authentication

JWT-based authentication

JWT is not “better” — it’s better for APIs and distributed systems.

5. JWT Token Generation (Core Logic)

JwtService.java


package com.example.jwtdemo.security;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Service;
import java.security.Key;
import java.util.Date;
@Service
public class JwtService {
    private static final String SECRET = "this-is-a-very-secure-secret-key-which-is-at-least-256-bits";
    private static final long EXPIRATION = 1000 * 60 * 60;
    private Key getSignKey() {
        return Keys.hmacShaKeyFor(SECRET.getBytes());
    }
    public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
                .signWith(getSignKey(), SignatureAlgorithm.HS256)
                .compact();
    }
    public String extractUsername(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSignKey())
                .build()
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }
}

6. Why You Need a JWT Filter (Most Tutorials Miss This)

Spring Security does NOT automatically validate JWTs.

You must:

  1. Extract token from header
  2. Validate signature
  3. Extract user
  4. Populate Security Context

This must happen before controllers run.

7. JWT Filter Using OncePerRequestFilter

JwtAuthFilter.java


package com.example.jwtdemo.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@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 filterChain) {
        try {
            String authHeader = request.getHeader("Authorization");
            if (authHeader != null && authHeader.startsWith("Bearer ")) {
                String jwt = authHeader.substring(7);
                String username = jwtService.extractUsername(jwt);
                if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                    UsernamePasswordAuthenticationToken authToken =
                            new UsernamePasswordAuthenticationToken(username, null, null);
                    authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authToken);
                }
            }
            filterChain.doFilter(request, response);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

8. Spring Security Configuration (Correct & Minimal)

SecurityConfig.java


package com.example.jwtdemo.config;
import com.example.jwtdemo.security.JwtAuthFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfig {
    private final JwtAuthFilter jwtAuthFilter;
    public SecurityConfig(JwtAuthFilter jwtAuthFilter) {
        this.jwtAuthFilter = jwtAuthFilter;
    }
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable())
            .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**").permitAll()
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}

Why this matters

9. Login Endpoint with Password Hashing

AuthController.java


package com.example.jwtdemo.controller;
import com.example.jwtdemo.security.JwtService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/auth")
public class AuthController {
    private final JwtService jwtService;
    private final PasswordEncoder passwordEncoder;
    private String storedUser = "admin";
    private String storedPassword;
    public AuthController(JwtService jwtService, PasswordEncoder passwordEncoder) {
        this.jwtService = jwtService;
        this.passwordEncoder = passwordEncoder;
        this.storedPassword = passwordEncoder.encode("password");
    }
    @PostMapping("/login")
    public String login(@RequestParam String username,
                        @RequestParam String password) {
        if (storedUser.equals(username) && passwordEncoder.matches(password, storedPassword)) {
            return jwtService.generateToken(username);
        }
        throw new RuntimeException("Invalid credentials");
    }
}

10. Authentication Flow (End-to-End)

Client → POST /auth/login

→ Password verified

*→ JWT generated*

Client → API request

→ Authorization: Bearer <JWT>

→ JwtAuthFilter validates the token

→ SecurityContext populated

→ Controller executes

No session.
No server-side token storage.
Fully stateless.

11. Login API – Input & Output

Request

POST http://localhost:8080/auth/login?username=admin&password=password

Body

username=admin

password=password

Response (Output)

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImlhdCI6MTc2OTc2OTMxMSwiZXhwIjoxNzY5NzcyOTExfQ.XbRQxBnuybpyJ7noMOLsg7Z6saeOAwE_D_5nK7ZFnNI