I am currently implementing JWT authentication in a Spring Boot application. In most tutorials and examples, I see that the UserDetailsService.loadUserByUsername method is called for each request to validate the token. Here is a snippet of my filter:
@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
private static final String BEARER_PREFIX = "Bearer ";
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final int TOKEN_START_INDEX = 7;
private final JwtService jwtService;
private final UserDetailsService jpaUserDetailsService;
@Override
protected void doFilterInternal(final HttpServletRequest request,
final HttpServletResponse response,
final FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader(AUTHORIZATION_HEADER);
if (hasBearerHeader(authHeader)) {
String token = extractToken(authHeader);
String username = jwtService.extractUsername(token);
if (username != null && !isUserAuthenticated()) {
UserDetails userDetails = jpaUserDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(token, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
}
filterChain.doFilter(request, response);
}
private boolean hasBearerHeader(final String authHeader) {
return StringUtils.hasText(authHeader) && authHeader.startsWith(BEARER_PREFIX);
}
private String extractToken(String authHeader) {
return Optional.of(authHeader)
.filter(s -> s.length() > TOKEN_START_INDEX)
.map(s -> s.substring(TOKEN_START_INDEX))
.orElseThrow(() -> new BadCredentialsException("Invalid Authorization header: Bearer token is missing or invalid."));
}
private boolean isUserAuthenticated() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication != null && authentication.isAuthenticated();
}
}
JwtService:
@Service
public class JwtService {
private static final int TOKEN_VALIDITY_HOURS = 1;
private static final int SECONDS_PER_HOUR = 3600;
private static final int TOKEN_VALIDITY_SECONDS = TOKEN_VALIDITY_HOURS * SECONDS_PER_HOUR;
private static final String ALGORITHM = "HmacSHA256";
@Value("${secret.key}")
private String secretKey;
public String generateToken(final UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
public String generateToken(final Map<String, ?> claims, final UserDetails userDetails) {
Instant currentTime = Instant.now();
Instant expirationTime = currentTime.plusSeconds(TOKEN_VALIDITY_SECONDS);
return Jwts
.builder()
.claims(claims)
.subject(userDetails.getUsername())
.issuedAt(Date.from(currentTime))
.expiration(Date.from(expirationTime))
.signWith(getSecretKey(), Jwts.SIG.HS256)
.compact();
}
public boolean isTokenValid(final String token, final UserDetails userDetails) {
String usernameFromToken = extractUsername(token);
return usernameFromToken.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
private boolean isTokenExpired(final String token) {
return extractExpiration(token).before(new Date());
}
public String extractUsername(final String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(final String token) {
return extractClaim(token, Claims::getExpiration);
}
private <T> T extractClaim(final String token, final Function<Claims, T> extractor) {
Claims claims = Jwts.parser()
.verifyWith(getSecretKey())
.build()
.parseSignedClaims(token)
.getPayload();
return extractor.apply(claims);
}
private SecretKey getSecretKey() {
byte[] decodedKey = Base64.getDecoder().decode(secretKey);
return new SecretKeySpec(decodedKey, 0, decodedKey.length, ALGORITHM);
}
}
I am confused about the necessity of loading user details from the database with each request. If the token is issued and verified, doesn’t it mean the user is already authenticated? Can’t we just verify the token’s signature and claims without hitting the database every time?
I appreciate any detailed explanations and recommendations on how to handle JWT authentication efficiently in Spring Security.