What is JWT?
JWT (JSON Web Token) is a compact, URL-safe token used to securely transfer claims (like user identity) between client and server — usually after login
❓ Why Use JWT?
In modern apps (especially REST APIs and mobile apps), we want stateless authentication. That means:
No session storage on the server
Every request carries its own proof (the token)
JWT makes this easy by:
Issuing a token when user logs in
Validating that token on each request
Letting us avoid database checks on every call
🛠️ Is JWT the Only Option?
No — other methods include:
Session-based authentication (used in web apps)
OAuth 2.0 (for delegated access, like logging in via Google)
API keys (used for internal systems)
But for most Spring Boot REST APIs, JWT is the go-to standard.
✅ What We’ll Build
We created a working Spring Boot app with:
/auth/login
→ generates access + refresh token/auth/refresh
→ gets a new access token using refresh token/api/user
→ a protected API endpoint
📦 Technologies Used
Java 17
Spring Boot 3
Spring Security
JJWT (io.jsonwebtoken)
IntelliJ IDEA
📂 Folder Structure

1. SecurityConfig.java
package com.example.jwtauthdemo.config;
import com.example.jwtauthdemo.util.JwtUtil;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
@Configuration
public class SecurityConfig {
@Autowired
private JwtUtil jwtUtil;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public OncePerRequestFilter jwtAuthenticationFilter() {
return new OncePerRequestFilter() {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.replace("Bearer ", "");
try {
Claims claims = jwtUtil.validateToken(token);
String username = claims.getSubject();
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
username, null, List.of(new SimpleGrantedAuthority("ROLE_USER")));
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (Exception ignored) {}
}
chain.doFilter(request, response);
}
};
}
}
2. AuthController.Java
package com.example.jwtauthdemo.controller;
import com.example.jwtauthdemo.model.AuthRequest;
import com.example.jwtauthdemo.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private TokenService tokenService;
@PostMapping("/login")
public ResponseEntity> login(@RequestBody AuthRequest request) {
// Dummy login
if ("user".equals(request.getUsername()) && "password".equals(request.getPassword())) {
return ResponseEntity.ok(tokenService.generateTokens(request.getUsername()));
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials");
}
}
@PostMapping("/refresh")
public ResponseEntity> refresh(@RequestParam String refreshToken) {
try {
String newAccessToken = tokenService.refreshAccessToken(refreshToken);
return ResponseEntity.ok(Map.of("accessToken", newAccessToken));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid refresh token");
}
}
}
3. UserController.java
package com.example.jwtauthdemo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/user")
public class UserController {
@GetMapping
public String getUser() {
return "Welcome, authenticated user!";
}
}
4. AuthRequest.java
package com.example.jwtauthdemo.model;
public class AuthRequest {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
5. InMemoryRefreshTokenStore.java
package com.example.jwtauthdemo.service;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class InMemoryRefreshTokenStore {
private final Map store = new ConcurrentHashMap<>();
public void save(String refreshToken, String username) {
store.put(refreshToken, username);
}
public String getUsername(String refreshToken) {
return store.get(refreshToken);
}
public void delete(String refreshToken) {
store.remove(refreshToken);
}
}
6. TokenService.java
package com.example.jwtauthdemo.service;
import com.example.jwtauthdemo.util.JwtUtil;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
public class TokenService {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private InMemoryRefreshTokenStore tokenStore;
public Map generateTokens(String username) {
String accessToken = jwtUtil.generateToken(username, 1000 * 60 * 15); // 15 min
String refreshToken = jwtUtil.generateToken(username, 1000 * 60 * 60 * 24 * 7); // 7 days
tokenStore.save(refreshToken, username);
return Map.of("accessToken", accessToken, "refreshToken", refreshToken);
}
public String refreshAccessToken(String refreshToken) {
Claims claims = jwtUtil.validateToken(refreshToken);
String username = tokenStore.getUsername(refreshToken);
if (username == null || !username.equals(claims.getSubject())) {
throw new RuntimeException("Invalid refresh token");
}
return jwtUtil.generateToken(username, 1000 * 60 * 15);
}
}
7. JwtUtil.java
package com.example.jwtauthdemo.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
@Component
public class JwtUtil {
private static final String SECRET_KEY = "Z0D8xgU0TbRqq1zQEfivJXt+eUFrJZrD9r93UzEqVUk=";
private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY);
return Keys.hmacShaKeyFor(keyBytes);
}
public String generateToken(String username, long expiryMillis) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiryMillis))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
public Claims validateToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
}
🧪 Test It with Postman
Login
URL:
POST /auth/login
Body:
{ "username": "user", "password": "password" }
Call API
URL:
GET /api/user
Header:
Authorization: Bearer <accessToken>
Access Token Expired?
URL:
POST /auth/refresh?refreshToken=...