In 2025, stateless authentication remains the go-to approach for securing REST APIs. Spring Security combined with JWT (JSON Web Tokens) continues to be a powerful, scalable solution — but the practices around token handling, refresh flows, and endpoint protection have evolved.
This guide walks you through securing your Spring Boot REST APIs using Spring Security and JWT, with updated approaches for 2025.
⚙️ Why JWT for REST API Security?
JWT enables stateless authentication, eliminating the need for session storage. It allows clients to carry the user’s identity and permissions in an encrypted token.
Benefits:
- Stateless = scalable across microservices
- Easy integration with frontend/mobile clients
- Self-contained (no DB lookups needed for each request)
🧱 Project Setup (Spring Boot 3.2+)
Make sure you’re using:
- Java 21+
- Spring Boot 3.2+
- Spring Security 6+
jjwtornimbus-jose-jwtfor token handling
Add dependencies (Maven):
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>
🔐 Creating the JWT Utility Class
@Component
public class JwtService {
private final String secretKey = "your-256-bit-secret-key";
private final Duration expiration = Duration.ofMinutes(15);
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(Date.from(Instant.now().plus(expiration)))
.signWith(Keys.hmacShaKeyFor(secretKey.getBytes()), SignatureAlgorithm.HS256)
.compact();
}
public String extractUsername(String token) {
return Jwts.parserBuilder()
.setSigningKey(secretKey.getBytes())
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean isValid(String token) {
try {
return !extractExpiration(token).before(new Date());
} catch (JwtException e) {
return false;
}
}
private Date extractExpiration(String token) {
return Jwts.parserBuilder()
.setSigningKey(secretKey.getBytes())
.build()
.parseClaimsJws(token)
.getBody()
.getExpiration();
}
}
🛡️ Configuring Spring Security with JWT Authentication Filter
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated())
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}
JWT Filter:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtService jwtService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
final String token = authHeader.substring(7);
if (jwtService.isValid(token)) {
String username = jwtService.extractUsername(token);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(username, null, List.of());
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
}
🔁 Refresh Token Flow (2025 Best Practices)
To keep your JWTs short-lived (15 minutes or less), use a refresh token stored securely (e.g., HTTP-only cookie or secure local storage).
Sample Refresh Endpoint
@PostMapping("/api/auth/refresh")
public ResponseEntity<TokenResponse> refresh(@RequestBody RefreshRequest request) {
String refreshToken = request.getRefreshToken();
// Validate refresh token from DB or cache
if (!refreshTokenService.isValid(refreshToken)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
String newAccessToken = jwtService.generateToken(request.getUsername());
return ResponseEntity.ok(new TokenResponse(newAccessToken));
}
🧠 Tips for Production Security in 2025
- Use short-lived access tokens (≤ 15 mins)
- Use long-lived refresh tokens (7–30 days), stored securely
- Enable CORS and CSRF protection appropriately for SPAs
- Add IP/device metadata checks for refresh flows
- Implement token revocation logic (DB, cache, or blacklist)
- Use HTTPS exclusively
🧪 Testing with Postman or Curl
Login Request:
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"john", "password":"password"}'
Authenticated Request:
curl http://localhost:8080/api/user/profile \ -H "Authorization: Bearer <access_token>"
📚 Further Reading & Tools
✅ Conclusion
Securing REST APIs with JWT and Spring Security in 2025 means prioritizing stateless design, short-lived access tokens, and safe refresh token flows. With the latest Spring Security features, this setup is both powerful and clean — ready to scale with your modern web applications.
Thank you!
We will contact you soon.
Eleftheria DrosopoulouMay 20th, 2025Last Updated: May 16th, 2025

This site uses Akismet to reduce spam. Learn how your comment data is processed.