JWT Auth in Spring Boot — The Right Way
Stop copying StackOverflow. Here's production-grade JWT with refresh tokens, Spring Security filters, and proper error handling.
Why Most JWT Tutorials Are Wrong
Every JWT tutorial I found did one of these things wrong:
- Stored refresh tokens in
localStorage(huge security risk) - Never implemented token rotation
- Didn't handle token expiry gracefully on the frontend
Let's fix all three.
The Architecture
- Access token: Short-lived (15 min), stored in memory
- Refresh token: Long-lived (7 days), stored in
HttpOnlycookie - Rotation: Refresh tokens rotate on every use
Setup
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
The JWT Service
Your JWT secret must be at least 256 bits (32 bytes) for HS256. Use openssl rand -base64 64 to generate one.
Spring Security Filter
@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@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 jwt = authHeader.substring(7);
final String username = jwtService.extractUsername(jwt);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(jwt, userDetails)) {
var authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
Step-by-Step: Refresh Token Flow
Client sends expired access token
The frontend catches a 401 response and automatically calls /auth/refresh.
Server reads HttpOnly cookie
The refresh token comes from the cookie — the client JavaScript can't even read it.
Rotate and respond
We invalidate the old refresh token and issue a new one. This prevents token reuse attacks.
Testing It
Before deploying: rotate secrets, enable HTTPS only for cookies, set SameSite=Strict, and log all auth failures.
What's Next?
In the next post we'll add role-based access control (RBAC) and per-endpoint permissions using Spring Security's @PreAuthorize.
✦ Enjoyed this post?
Get posts like this in your inbox
No spam, just real tutorials when they're ready.
Discussion
Powered by GitHubComments use GitHub Discussions — no separate account needed if you have GitHub.