Originally published on Medium
Securing your REST API is essential for protecting sensitive data and ensuring only authorized users can access your endpoints. In this article, you'll learn how to secure a Spring Boot API using JSON Web Tokens (JWT) with role-based authorization, clear error handling, and best practices.
Project Setup
Create a Spring Boot project with the following dependencies:
- Spring Web
- Spring Security
- jjwt (add to
pom.xml
):
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
DTO for Login
package com.example.jwtsecuritydemo;
//DTO for login requests. Used to receive username and password as JSON.
public class LoginRequest {
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;
}
}
JWT Utility
package com.example.jwtsecuritydemo;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
//Utility class for JWT operations: generate, validate, and extract claims.
public class JwtUtil {
private static final String SECRET_KEY = "my-super-secret-key-for-jwt";
private static final long EXPIRATION_TIME = 3600000; // 1 hour
private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
private static final Key key = new SecretKeySpec(SECRET_KEY.getBytes(), SIGNATURE_ALGORITHM.getJcaName());
public static String generateToken(String username, String role) {
return Jwts.builder()
.setSubject(username)
.claim("role", role)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SIGNATURE_ALGORITHM, key)
.compact();
}
public static boolean validateToken(String token) {
try {
Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
public static String getUsernameFromToken(String token) {
return Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public static String getRoleFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(token)
.getBody();
return claims.get("role", String.class);
}
}
Authentication Controller
package com.example.jwtsecuritydemo;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/auth")
public class AuthController {
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
String username = loginRequest.getUsername();
String password = loginRequest.getPassword();
String role = "admin".equals(username) ? "ROLE_ADMIN" : "ROLE_USER";
if (("admin".equals(username) && "password".equals(password)) ||
("user".equals(username) && "password".equals(password))) {
String token = JwtUtil.generateToken(username, role);
return ResponseEntity.ok().body(token);
}
return ResponseEntity.status(401).body("Invalid credentials");
}
}
JWT Filter
package com.example.jwtsecuritydemo;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Collections;
public class JwtFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
if (JwtUtil.validateToken(token)) {
String username = JwtUtil.getUsernameFromToken(token);
String role = JwtUtil.getRoleFromToken(token);
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role);
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
username, null, Collections.singletonList(authority));
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
chain.doFilter(request, response);
}
}
Security Configuration
package com.example.jwtsecuritydemo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/login").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class)
.build();
}
}
Global Exception Handler
package com.example.jwtsecuritydemo;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import jakarta.servlet.ServletException;
import java.io.IOException;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
@ResponseBody
public ResponseEntity<?> handleAccessDeniedException(AccessDeniedException ex) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body("Access denied: " + ex.getMessage());
}
@ExceptionHandler({ServletException.class, IOException.class})
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ResponseBody
public ResponseEntity<?> handleServletException(Exception ex) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("Unauthorized: " + ex.getMessage());
}
}
Secured Endpoints
HelloController.java
package com.example.jwtsecuritydemo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello! You have accessed a secured endpoint.";
}
}
SecuredController.java
package com.example.jwtsecuritydemo;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class SecuredController {
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
@GetMapping("/secure-data")
public String secureData() {
return "Access granted to admin user!";
}
}
Main Application
package com.example.jwtsecuritydemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class JwtsecuritydemoApplication {
public static void main(String[] args) {
SpringApplication.run(JwtsecuritydemoApplication.class, args);
}
}
Testing with curl
Login as admin:
curl -X POST http://localhost:8080/auth/login -H "Content-Type: application/json" -d "{\"username\":\"admin\",\"password\":\"password\"}"
Login as user:
curl -X POST http://localhost:8080/auth/login -H "Content-Type: application/json" -d "{\"username\":\"user\",\"password\":\"password\"}"
Access public endpoint:
curl -H "Authorization: Bearer <TOKEN>" http://localhost:8080/hello
Access admin-only endpoint (should succeed for admin, fail for user):
curl -H "Authorization: Bearer <TOKEN>" http://localhost:8080/api/secure-data
Conclusion
You now have a robust, stateless, and role-based JWT security setup for your Spring Boot API. This approach is scalable, production-ready, and easy to extend for real-world applications.
What You Gain from This Article
Practical Security: A hands-on, production-ready template for securing any Spring Boot API using JWT and role-based access control.
Scalability: Stateless JWT approach ideal for microservices, cloud deployments, and modern distributed systems.
Extensibility: Modular code that’s easy to extend — add refresh tokens, integrate databases, or connect OAuth providers.
Best Practices: Use of DTOs, global exception handling, and method-level security—all industry standards.
Who Should Use This Guide
Java/Spring Boot developers looking to secure APIs quickly and correctly.
Backend engineers building microservices or RESTful APIs.
Students and learners wanting a clear, step-by-step JWT security example.
Architects and tech leads seeking a reference implementation.
How to Get the Most Out of This Article
Try the curl commands to see the authentication flow in action.
Fork and extend the code — add user registration, connect to a real database, or implement refresh tokens.
Share your feedback or questions in the comments—let's learn together!
Bonus: What’s Next?
Interested in:
Integrating with OAuth2 or social login?
Adding refresh tokens?
Using JWT with GraphQL or WebSockets?
Deploying this setup to the cloud?
Top comments (0)