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:
- A reader logs in → receives a token with
ROLE_USER - An author logs in → receives a token with
ROLE_AUTHOR - Every API request carries the token in the
Authorizationheader - The server validates the token on every request — no session lookup needed
- If a reader tries
DELETE /api/posts/1, Spring Security returns403 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 localStoragelocalStorage 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.