DEV Community

Pratusha Raya
Pratusha Raya

Posted on • Originally published at Medium

Secure Your Spring Boot API with JWT and Role-Based Authorization

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>
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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.";
    }
}
Enter fullscreen mode Exit fullscreen mode

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!";
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Testing with curl

Login as admin:

curl -X POST http://localhost:8080/auth/login -H "Content-Type: application/json" -d "{\"username\":\"admin\",\"password\":\"password\"}"
Enter fullscreen mode Exit fullscreen mode

Login as user:

curl -X POST http://localhost:8080/auth/login -H "Content-Type: application/json" -d "{\"username\":\"user\",\"password\":\"password\"}"
Enter fullscreen mode Exit fullscreen mode

Access public endpoint:

curl -H "Authorization: Bearer <TOKEN>" http://localhost:8080/hello
Enter fullscreen mode Exit fullscreen mode

Access admin-only endpoint (should succeed for admin, fail for user):

curl -H "Authorization: Bearer <TOKEN>" http://localhost:8080/api/secure-data
Enter fullscreen mode Exit fullscreen mode

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?

Let me know in the comments! Planning follow-up articles based on your feedback. Thank you for reading!

Top comments (0)