How to Secure REST APIs in Spring Boot Using JWT Authentication

Building a REST API is exciting — but leaving it open to the world without security is a serious mistake. In this guide, you will learn exactly how to implement Spring Boot JWT authentication to protect your APIs from unauthorised access.

Whether you are a beginner who just built your first Spring Boot app, or an intermediate developer who wants to understand security better — this guide is written for you.


What Is JWT (JSON Web Token)?

JWT stands for JSON Web Token. It is an open standard (RFC 7519) that defines a compact, self-contained way to securely transmit information between a client and a server.

Think of JWT like a digital ID card. When a user logs in, the server creates a signed token that contains the user’s identity. The user attaches that token to every request, and the server uses it to verify who they are — without asking them to log in again.

Structure of a JWT

A JWT has three parts separated by dots (.):

Header.Payload.Signature
Part Name What It Contains
First segment Header Algorithm type — e.g. HS256
Second segment Payload User data — e.g. username, roles, expiry
Third segment Signature Hash that proves the token was not tampered with
{
  "sub": "john@example.com",
  "roles": ["ROLE_USER"],
  "iat": 1716000000,
  "exp": 1716086400
}

Important: JWT is Base64Url-encoded, not encrypted. Never store sensitive data like passwords inside a token.


Why Is REST API Security Important?

Without security, anyone can call your API endpoints. This leads to real-world consequences:

  • Data breaches — attackers can read private user data
  • Account takeovers — bad actors impersonate real users
  • API abuse — bots flood your server with thousands of requests
  • Legal risk — regulations like GDPR require you to protect user data

Why JWT Instead of Sessions?

Traditional session-based authentication stores user state on the server. This becomes hard to scale with multiple servers or microservices.

Spring Boot JWT authentication is stateless. The server creates a token at login, then forgets about it. On every subsequent request, it simply reads and verifies the token. No session storage. No shared state. Perfect for REST APIs.


Prerequisites

  • Java 17 or higher
  • Spring Boot 3.x project (Maven or Gradle)
  • Basic understanding of Spring Boot and REST APIs
  • Postman or cURL to test

Step-by-Step Implementation

Step 1 — Add the Required Dependencies

Open your pom.xml and add Spring Security and JJWT:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

Step 2 — Create the JWT Utility Class

This class handles generating and validating JWT tokens. It is the core of your Spring Boot JWT authentication setup.

@Component
public class JwtUtil {

    @Value("${jwt.secret}")
    private String secretKey;

    private static final long EXPIRATION_MS = 86_400_000L; // 24 hours

    public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_MS))
                .signWith(getSigningKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public boolean isTokenExpired(String token) {
        return extractClaim(token, Claims::getExpiration).before(new Date());
    }

    public boolean validateToken(String token, UserDetails userDetails) {
        return extractUsername(token).equals(userDetails.getUsername())
                && !isTokenExpired(token);
    }

    private <T> T extractClaim(String token, Function<Claims, T> resolver) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
        return resolver.apply(claims);
    }

    private Key getSigningKey() {
        byte[] bytes = Decoders.BASE64.decode(secretKey);
        return Keys.hmacShaKeyFor(bytes);
    }
}

Add to application.properties:

jwt.secret=dGhpcyBpcyBhIHZlcnkgbG9uZyBhbmQgc2VjdXJlIHNlY3JldCBrZXkgZm9yIEpXVA==

Production tip: Never hardcode secrets in source code. Use environment variables or a secrets manager like AWS Secrets Manager.

Step 3 — Create the JWT Request Filter

This filter runs on every HTTP request. It reads the Authorization header, extracts the token, validates it, and sets the authenticated user in Spring Security’s context.

@Component
public class JwtAuthFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {

        String authHeader = request.getHeader("Authorization");
        String token    = null;
        String username = null;

        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            token    = authHeader.substring(7);
            username = jwtUtil.extractUsername(token);
        }

        if (username != null
                && SecurityContextHolder.getContext().getAuthentication() == null) {

            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            if (jwtUtil.validateToken(token, userDetails)) {
                UsernamePasswordAuthenticationToken auth =
                        new UsernamePasswordAuthenticationToken(
                                userDetails, null, userDetails.getAuthorities());
                auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        }
        filterChain.doFilter(request, response);
    }
}

Step 4 — Configure Spring Security

Register your filter and define which endpoints are public versus protected.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private JwtAuthFilter jwtAuthFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated()
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Step 5 — Build the Authentication Controller

This controller exposes /api/auth/login. A successful login returns a JWT token the client can use for future requests.

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authManager;

    @Autowired
    private JwtUtil jwtUtil;

    @PostMapping("/login")
    public ResponseEntity<String> login(@RequestBody AuthRequest request) {
        authManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                request.getUsername(), request.getPassword()
            )
        );
        String token = jwtUtil.generateToken(request.getUsername());
        return ResponseEntity.ok(token);
    }
}

Step 6 — Test Your Setup

Get a token:

curl -X POST http://localhost:8080/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"john@example.com","password":"secret123"}'

Successful response:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqb2huQGV4YW1wbGUuY29tIn0.abc123xyz...

Call a protected endpoint:

curl -X GET http://localhost:8080/api/users/profile \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
  • ✅ Valid token → 200 OK
  • ❌ Missing or expired token → 401 Unauthorized

Real-World Example: Securing a Blog Platform API

Imagine you are building an API for a blog platform with two roles:

  • Reader — can view posts (GET /api/posts)
  • Author — can create and delete posts (POST /api/posts, DELETE /api/posts/{id})

With Spring Boot JWT authentication, the flow works like this:

  1. A reader logs in → receives a token with ROLE_USER
  2. An author logs in → receives a token with ROLE_AUTHOR
  3. Every API request carries the token in the Authorization header
  4. The server validates the token on every request — no session lookup needed
  5. If a reader tries DELETE /api/posts/1, Spring Security returns 403 Forbidden

Enforce role-based access with @PreAuthorize:

@DeleteMapping("/{id}")
@PreAuthorize("hasRole('AUTHOR')")
public ResponseEntity<Void> deletePost(@PathVariable Long id) {
    postService.delete(id);
    return ResponseEntity.noContent().build();
}

Common Mistakes to Avoid

Even experienced developers make these errors when setting up Spring Boot JWT authentication:

1. Hardcoding the secret key in source code

// WRONG
private String secret = "mysecret";

Always inject it from application.properties or environment variables using @Value.

2. Not setting a token expiry time
A token with no expiry never expires. If stolen, the attacker has permanent access. Always call .setExpiration().

3. Using a short or guessable secret key
HMAC-SHA256 requires at least a 256-bit key. Short keys can be brute-forced. Use a randomly generated Base64 string.

4. Forgetting to disable CSRF
JWT-based APIs are stateless, so CSRF does not apply. If you forget to disable it, your API will reject legitimate POST/PUT/DELETE requests.

5. Not returning helpful error messages on token failure
When a token is expired, return a clear 401 with a message like “Token expired. Please log in again.”

6. Storing the JWT in localStorage
localStorage is accessible to JavaScript and is vulnerable to XSS attacks. Use httpOnly cookies for browser applications.

7. Not logging failed authentication attempts
Silent failures make debugging hard and miss security incidents. Log invalid token attempts at WARN level.


Conclusion

Spring Boot JWT authentication is the industry-standard way to secure REST APIs. It is stateless, scalable, and works seamlessly across microservices and modern frontend frameworks.

In this guide, you covered everything you need:

  • What JWT is and how its three-part structure works
  • Why stateless authentication beats session-based auth for REST APIs
  • A complete working implementation with Spring Boot 3 and JJWT
  • Real-world role-based access control patterns
  • Seven common mistakes and how to avoid each one

The best way to learn is to build. Take this code, drop it into your next Spring Boot project, and start securing your endpoints today.

Where to go next:

  • Add refresh tokens so users stay logged in without re-authenticating
  • Implement role-based access control with @PreAuthorize
  • Connect to a real PostgreSQL or MySQL database with Spring Data JPA
  • Protect secrets using environment variables and a proper CI/CD pipeline

Need Help Building Your Project?

If you are building a Spring Boot application and need guidance, free developer tools, or expert resources — Owndevz is here to help.

We create free tools and write practical guides exactly like this one to help developers ship better software, faster.

👉 Contact us at Owndevz — whether you have a question, a project idea, or just need a second opinion on your code. We would love to hear from you.

Leave a comment