
Securing REST APIs is one of the most critical parts of backend development — especially when you’re building SaaS platforms, mobile backends, or multi-tenant services. In this tutorial, you’ll learn how to build a secure REST API in Spring Boot 3 using Spring Security and JWT (JSON Web Tokens), step by step.
We’ll walk through authentication, token generation, filtering requests, and protecting API endpoints using modern best practices for 2025.
If you’re new to Spring Boot, check out our full Getting Started with Spring Boot 3 guide first.
🔐 What is JWT?
JWT (JSON Web Token) is a compact, self-contained token format used to securely transmit user identity and claims between a client and server. It’s commonly used for stateless authentication in REST APIs and mobile apps.
A JWT is composed of three parts:
HEADER.PAYLOAD.SIGNATURE
Example:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyQGV4YW1wbGUuY29tIn0.abc123...
📘 Learn more: What is JWT? – jwt.io
⚙️ Project Setup
Start by generating a Spring Boot 3 project with the following dependencies:
- Spring Web
- Spring Security
- Spring Data JPA (for storing users, optional)
- H2 / PostgreSQL
- JWT library: We’re using
jjwt
(Java JWT)
Gradle example:
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
Make sure your app runs successfully before implementing security.
🧠 Step 1: Create a User Entity
@Entity
public class AppUser {
@Id
@GeneratedValue
private Long id;
private String email;
private String password;
}
You can store users in an in-memory database (H2) or connect to PostgreSQL for production.
🔧 Step 2: Implement a CustomUserDetailsService
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) {
AppUser user = userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return new User(user.getEmail(), user.getPassword(), new ArrayList<>());
}
}
Spring Security will use this service to authenticate users by email.
🔐 Step 3: JWT Utility Class
Create a utility to generate, extract, and validate JWTs.
@Component
public class JwtUtil {
private final String SECRET_KEY = "your_secret";
public String generateToken(UserDetails userDetails) {
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60)) // 1 hour
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
public String extractUsername(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody().getSubject();
}
public boolean validateToken(String token, UserDetails userDetails) {
return extractUsername(token).equals(userDetails.getUsername());
}
}
In a real-world app, store the secret securely and rotate it periodically.
🧱 Step 4: Add a JWT Filter
This filter intercepts requests, extracts the JWT, and sets authentication context.
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authHeader != null && authHeader.startsWith("Bearer ")) {
jwt = authHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(token);
}
}
chain.doFilter(request, response);
}
}
🔒 Step 5: Configure Spring Security
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtRequestFilter jwtFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.csrf().disable()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated())
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}
All requests outside /api/auth/**
now require a valid JWT token.
🎯 Step 6: Create Login Endpoint
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authManager;
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/login")
public ResponseEntity login(@RequestBody AuthRequest authRequest) {
Authentication auth = authManager.authenticate(
new UsernamePasswordAuthenticationToken(authRequest.getEmail(), authRequest.getPassword()));
final String jwt = jwtUtil.generateToken((UserDetails) auth.getPrincipal());
return ResponseEntity.ok(new AuthResponse(jwt));
}
}
Test with Postman or Curl:
POST /api/auth/login
{
"email": "user@example.com",
"password": "password123"
}
Use the returned token in headers:
Authorization: Bearer
🛡️ Bonus Tips for Production
- Use BCrypt to hash passwords (
BCryptPasswordEncoder
) - Set shorter expiration + implement refresh tokens
- Store secrets in environment variables or vaults
- Use
@PreAuthorize
for role-based access control
✅ Final Thoughts
JWT-based authentication with Spring Security is fast, scalable, and stateless — ideal for modern SaaS, mobile, and microservice backends. With just a few configurations and a filter, you can secure your API and manage authentication cleanly.
Spring Boot 3 makes this even smoother with native support for stateless configurations and better developer ergonomics.