Spring Boot JWT Authentication: Complete Implementation Guide
JWT (JSON Web Token) authentication is the standard approach for securing stateless REST APIs. Instead of server-side sessions, the client receives a signed token on login and presents it with every subsequent request. This guide walks through a full Spring Boot 3 + Spring Security 6 JWT implementation from scratch.
How JWT Authentication Works
- Client sends credentials (username + password) to
POST /auth/login - Server validates credentials, generates a JWT signed with a secret key, returns it
- Client stores the token (memory or HttpOnly cookie) and sends it as
Authorization: Bearer <token>on each request - Server validates the token signature and expiry on every request — no database lookup needed
A JWT has three Base64-encoded parts: header (algorithm), payload (claims — userId, roles, expiry), and signature (HMAC or RSA of header + payload).
Dependencies
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
</dependencies>
JWT Service — Token Generation & Validation
@Service
public class JwtService {
@Value("${app.jwt.secret}")
private String secretKey;
@Value("${app.jwt.expiration-ms:86400000}") // 24h default
private long expirationMs;
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey));
}
public String generateToken(UserDetails userDetails) {
return Jwts.builder()
.subject(userDetails.getUsername())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expirationMs))
.signWith(getSigningKey())
.compact();
}
public String extractUsername(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}
public boolean isTokenValid(String token, UserDetails userDetails) {
try {
String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isExpired(token);
} catch (JwtException e) {
return false;
}
}
private boolean isExpired(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload()
.getExpiration()
.before(new Date());
}
}
Add to application.yml:
app:
jwt:
secret: your-256-bit-base64-encoded-secret-key-here
expiration-ms: 86400000 # 24 hours
JWT Authentication Filter
This filter intercepts every request, extracts the token from the Authorization header, validates it, and sets the security context:
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
public JwtAuthFilter(JwtService jwtService, UserDetailsService userDetailsService) {
this.jwtService = jwtService;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
String username = jwtService.extractUsername(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(token, userDetails)) {
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
chain.doFilter(request, response);
}
}
Security Configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthFilter jwtAuthFilter;
public SecurityConfig(JwtAuthFilter jwtAuthFilter) {
this.jwtAuthFilter = jwtAuthFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
Login Endpoint
@RestController
@RequestMapping("/auth")
public class AuthController {
private final AuthenticationManager authManager;
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@PostMapping("/login")
public ResponseEntity<Map<String, String>> login(@RequestBody LoginRequest req) {
authManager.authenticate(
new UsernamePasswordAuthenticationToken(req.username(), req.password())
);
UserDetails user = userDetailsService.loadUserByUsername(req.username());
String token = jwtService.generateToken(user);
return ResponseEntity.ok(Map.of("token", token));
}
}
public record LoginRequest(String username, String password) {}
Testing the Flow
# Step 1: Login and get token
curl -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"user@example.com","password":"secret"}'
# Response: {"token":"eyJhbGci..."}
# Step 2: Call secured endpoint
curl http://localhost:8080/api/orders \
-H "Authorization: Bearer eyJhbGci..."
Security Best Practices
- Use RS256 (asymmetric) in production for microservices — the private key signs tokens, services use the public key to verify. No shared secret needed.
- Short expiry + refresh tokens: Access tokens should expire in 15–60 minutes. Issue a long-lived refresh token separately (stored server-side) to get new access tokens.
- Never store JWTs in localStorage — vulnerable to XSS. Use HttpOnly cookies for the access token.
- Rotate signing keys periodically. Implement JWKS endpoint if using RS256 so clients can fetch the current public key.
- Validate all claims: Always check
exp,iss, andaud— not just the signature.
Conclusion
JWT authentication in Spring Boot 3 involves four components: a JwtService to generate and validate tokens, a JwtAuthFilter to intercept requests, a SecurityConfig to wire them together, and a login endpoint to issue tokens. The pattern is stateless, scales horizontally, and is compatible with any frontend framework. Focus on short-lived access tokens, HttpOnly cookies, and refresh token rotation for a production-grade implementation.
Related: Spring Boot Hub | Java Microservices Architecture