Spring Boot Security with JWT Authentication
Implement JWT authentication in Spring Boot 3. Covers security filter chain, token generation, refresh tokens, role-based access, and common security pitfalls.
What you'll learn
- ✓How to configure Spring Security 6 with JWT
- ✓How to generate and validate JWT tokens
- ✓How to implement refresh token rotation
Prerequisites
- •Spring Boot fundamentals
- •Basic understanding of HTTP and REST APIs
- •Java 17+
Spring Security 6 (Spring Boot 3+) overhauled its configuration model, moving from WebSecurityConfigurerAdapter to a component-based approach using SecurityFilterChain beans. This guide builds a complete JWT authentication system from scratch using the current API.
Project setup
Add these dependencies to your pom.xml:
<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 and validation
Create a service that handles all JWT operations:
@Service
public class JwtService {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.access-token-expiration:900000}") // 15 minutes
private long accessTokenExpiration;
@Value("${jwt.refresh-token-expiration:604800000}") // 7 days
private long refreshTokenExpiration;
private SecretKey getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
public String generateAccessToken(UserDetails userDetails) {
return buildToken(userDetails, accessTokenExpiration, Map.of("type", "access"));
}
public String generateRefreshToken(UserDetails userDetails) {
return buildToken(userDetails, refreshTokenExpiration, Map.of("type", "refresh"));
}
private String buildToken(UserDetails userDetails, long expiration, Map<String, Object> extraClaims) {
return Jwts.builder()
.claims(extraClaims)
.subject(userDetails.getUsername())
.claim("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey())
.compact();
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public boolean isTokenValid(String token, UserDetails userDetails) {
String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractClaim(token, Claims::getExpiration).before(new Date());
}
private <T> T extractClaim(String token, Function<Claims, T> resolver) {
Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
return resolver.apply(claims);
}
}
JWT authentication filter
This filter intercepts every request, extracts the JWT from the Authorization header, validates it, and sets the security context:
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String jwt = authHeader.substring(7);
try {
String username = jwtService.extractUsername(jwt);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
} catch (JwtException e) {
// invalid token: let the filter chain continue without authentication
// the security config will return 401 for protected endpoints
}
filterChain.doFilter(request, response);
}
}
Security configuration
Configure Spring Security to use your JWT filter:
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable()) // disable CSRF for stateless APIs
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Authentication controller
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
public record LoginRequest(String username, String password) {}
public record AuthResponse(String accessToken, String refreshToken) {}
public record RefreshRequest(String refreshToken) {}
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@RequestBody LoginRequest request) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.username(), request.password())
);
UserDetails user = userDetailsService.loadUserByUsername(request.username());
String accessToken = jwtService.generateAccessToken(user);
String refreshToken = jwtService.generateRefreshToken(user);
return ResponseEntity.ok(new AuthResponse(accessToken, refreshToken));
}
@PostMapping("/refresh")
public ResponseEntity<AuthResponse> refresh(@RequestBody RefreshRequest request) {
String username = jwtService.extractUsername(request.refreshToken());
UserDetails user = userDetailsService.loadUserByUsername(username);
if (!jwtService.isTokenValid(request.refreshToken(), user)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
String newAccessToken = jwtService.generateAccessToken(user);
String newRefreshToken = jwtService.generateRefreshToken(user);
return ResponseEntity.ok(new AuthResponse(newAccessToken, newRefreshToken));
}
}
Role-based access control
Define roles in your user entity and use them in endpoint security:
@Entity
@Table(name = "users")
public class User implements UserDetails {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
@Enumerated(EnumType.STRING)
private Role role; // USER, ADMIN, MODERATOR
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_" + role.name()));
}
// other UserDetails methods...
}
Then secure endpoints with method-level annotations:
@RestController
@RequestMapping("/api/admin")
public class AdminController {
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/users")
public List<UserDto> getAllUsers() {
return userService.findAll();
}
@PreAuthorize("hasAnyRole('ADMIN', 'MODERATOR')")
@DeleteMapping("/posts/{id}")
public ResponseEntity<Void> deletePost(@PathVariable Long id) {
postService.delete(id);
return ResponseEntity.noContent().build();
}
}
Enable method security in your configuration:
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {}
Application properties
# generate a secure key: openssl rand -base64 64
jwt.secret=your-base64-encoded-secret-key-at-least-256-bits
jwt.access-token-expiration=900000
jwt.refresh-token-expiration=604800000
Common security pitfalls
- Weak signing keys: Use at least 256 bits for HMAC-SHA256. Generate with
openssl rand -base64 64. - No token expiration: Always set short-lived access tokens (15 minutes) and longer refresh tokens (7 days).
- Storing tokens in localStorage: Vulnerable to XSS. Use httpOnly cookies for browser apps.
- Not validating token type: A refresh token should not be usable as an access token. Include a
typeclaim and check it. - Missing CORS configuration: Stateless APIs need explicit CORS setup for browser clients.
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://yourfrontend.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
Summary
JWT authentication in Spring Boot 3 uses the SecurityFilterChain bean for configuration, a custom OncePerRequestFilter for token validation, and the jjwt library for token operations. Keep access tokens short-lived, implement refresh token rotation, use BCrypt for passwords, and avoid storing tokens in browser localStorage. For production, consider adding token revocation via a database or Redis blocklist.
Related articles
- Java Spring Security Basics: Authentication, Authorization, and Filters
A practical introduction to Spring Security. Understand the filter chain, configure authentication, set up authorization rules, and avoid common mistakes.
- Java Spring Boot Dependency Injection Explained
How Spring Boot wires your beans: component scanning, constructor injection, qualifiers, profiles, and the testing strategies that follow from each choice.
- REST APIs REST API Authentication: API Keys, JWT, and OAuth 2.0
Learn the three most common REST API authentication methods. Compare API keys, JWT tokens, and OAuth 2.0 with working code examples and security best practices.
- Java Java CompletableFuture Patterns for Async Code
Master CompletableFuture composition patterns including chaining, combining, error handling, timeouts, and async pipeline design in Java.