JWT in Spring Boot: The Three Dependencies You Need
Want to implement JWT authentication in Spring Boot? You need three dependencies, not one. Here's why and what each one does.
The Three JWT Dependenciesβ
Add these to your pom.xml:
<dependencies>
<!-- JWT API - The interface definitions -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<!-- JWT Implementation - The actual code -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<!-- JWT Jackson - JSON processing -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
</dependencies>
Why Three Dependencies?β
1. jjwt-api (The Interface)β
What it is: Interface definitions and method signatures
What you import in your code:
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.Claims;
Purpose: Compile-time dependency - lets you write JWT code
Scope: compile (default)
2. jjwt-impl (The Implementation)β
What it is: Actual implementation of the JWT API
What it does:
- Provides the real code behind the interfaces
- Handles JWT creation and parsing
- Manages signatures and validation
Purpose: Runtime dependency - actually executes JWT operations
Scope: runtime (not needed during compilation)
3. jjwt-jackson (The JSON Processor)β
What it is: JSON serialization/deserialization for JWT
What it does:
- Converts Java objects to JSON (in JWT payload)
- Parses JSON back to Java objects
- Handles JWT claims processing
Purpose: Runtime dependency - processes JWT JSON data
Scope: runtime (not needed during compilation)
The Dependency Chainβ
Your Code
β
jjwt-api (compile time)
β
jjwt-impl (runtime) β Executes JWT operations
β
jjwt-jackson (runtime) β Handles JSON in JWT
Think of it like:
- API = The contract (interface)
- Impl = The worker (implementation)
- Jackson = The translator (JSON processor)
What Each Scope Meansβ
compile (jjwt-api)β
<dependency>
<artifactId>jjwt-api</artifactId>
<scope>compile</scope> <!-- or omit (default) -->
</dependency>
Needed for:
- β Compiling your code
- β Running your app
runtime (jjwt-impl, jjwt-jackson)β
<dependency>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
Needed for:
- β Not for compiling
- β Running your app
Why runtime?
- You don't directly import classes from these JARs
- They're discovered and used automatically at runtime
- Keeps your code independent of specific implementations
Basic JWT Usage Exampleβ
Once dependencies are added, you can create and validate JWTs:
Creating a JWTβ
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;
public class JwtUtil {
private static final Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username) // Who the token is for
.setIssuedAt(new Date()) // When issued
.setExpiration(new Date(System.currentTimeMillis() + 86400000)) // 24h
.signWith(SECRET_KEY) // Sign with secret
.compact(); // Build the JWT string
}
}
What happens:
jjwt-apiβ ProvidesJwts.builder()methodjjwt-implβ Actually builds the JWTjjwt-jacksonβ Converts claims to JSON
Validating a JWTβ
import io.jsonwebtoken.Claims;
public class JwtUtil {
public Claims validateToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody();
}
public String extractUsername(String token) {
Claims claims = validateToken(token);
return claims.getSubject();
}
}
What happens:
jjwt-apiβ Provides parsing methodsjjwt-implβ Actually parses and validatesjjwt-jacksonβ Converts JSON toClaimsobject
What JWT Looks Likeβ
The Token Structureβ
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqb2huIiwiaWF0IjoxNjk4NzY1NDMyLCJleHAiOjE2OTg4NTE4MzJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Three parts separated by dots:
HEADER.PAYLOAD.SIGNATURE
1. Header (Algorithm & Token Type)β
{
"alg": "HS256",
"typ": "JWT"
}
Base64 encoded: eyJhbGciOiJIUzI1NiJ9
2. Payload (Claims/Data)β
{
"sub": "john",
"iat": 1698765432,
"exp": 1698851832
}
Base64 encoded: eyJzdWIiOiJqb2huIiwiaWF0IjoxNjk4NzY1NDMyLCJleHAiOjE2OTg4NTE4MzJ9
3. Signature (Verification)β
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
Base64 encoded: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Common Claims in JWTβ
Jwts.builder()
.setSubject("john") // Subject (username/user ID)
.setIssuedAt(new Date()) // When issued
.setExpiration(new Date(expiryTime)) // When expires
.setIssuer("my-app") // Who issued
.claim("role", "ADMIN") // Custom claim
.claim("email", "john@email.com") // Custom claim
.signWith(key)
.compact();
Standard claims:
sub(subject) β User identifieriat(issued at) β Timestampexp(expiration) β Expiry timestampiss(issuer) β Who created itaud(audience) β Who it's for
Custom claims: Any data you want!
Dependency Verificationβ
Check Dependencies Are Downloadedβ
mvn dependency:tree | grep jjwt
Expected output:
[INFO] +- io.jsonwebtoken:jjwt-api:jar:0.12.3:compile
[INFO] +- io.jsonwebtoken:jjwt-impl:jar:0.12.3:runtime
[INFO] +- io.jsonwebtoken:jjwt-jackson:jar:0.12.3:runtime
Check JAR Files Existβ
ls ~/.m2/repository/io/jsonwebtoken/
Should see:
jjwt-api/
jjwt-impl/
jjwt-jackson/
Why Not Just One Dependency?β
Historical reasons:
- Modular design (separation of concerns)
- Choose your JSON processor (Jackson, Gson, or custom)
- Keep API stable while updating implementation
- Support different Java versions
Alternatives to jjwt-jackson:
<!-- Option 1: Jackson (recommended) -->
<dependency>
<artifactId>jjwt-jackson</artifactId>
</dependency>
<!-- Option 2: Gson -->
<dependency>
<artifactId>jjwt-gson</artifactId>
</dependency>
<!-- Option 3: org.json -->
<dependency>
<artifactId>jjwt-orgjson</artifactId>
</dependency>
Most projects use Jackson (it's what Spring Boot uses internally)
Common Issuesβ
Issue 1: Missing Implementationβ
Error:
java.lang.ClassNotFoundException: io.jsonwebtoken.impl.DefaultJwtBuilder
Solution: Add jjwt-impl:
<dependency>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
Issue 2: Missing JSON Processorβ
Error:
java.lang.ClassNotFoundException: com.fasterxml.jackson.databind.ObjectMapper
Solution: Add jjwt-jackson:
<dependency>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
Issue 3: Version Mismatchβ
Error:
Incompatible version: jjwt-api 0.12.3 requires jjwt-impl 0.12.3
Solution: Use same version for all three:
<properties>
<jjwt.version>0.12.3</jjwt.version>
</properties>
<dependency>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
Best Practicesβ
β Do Thisβ
1. Use the same version for all three
<properties>
<jjwt.version>0.12.3</jjwt.version>
</properties>
2. Use runtime scope for impl and jackson
<scope>runtime</scope>
3. Store secrets securely
// β Don't hardcode
private static final String SECRET = "mySecret123";
// β
Use environment variables
private static final String SECRET = System.getenv("JWT_SECRET");
4. Set reasonable expiration times
// Access token: short-lived (15 minutes)
.setExpiration(new Date(System.currentTimeMillis() + 900000))
// Refresh token: long-lived (7 days)
.setExpiration(new Date(System.currentTimeMillis() + 604800000))
β Avoid Thisβ
1. Don't use weak secrets
// β Too short
Keys.hmacShaKeyFor("secret".getBytes())
// β
Use proper key generation
Keys.secretKeyFor(SignatureAlgorithm.HS256)
2. Don't store sensitive data in JWT
// β Bad - passwords in JWT!
.claim("password", user.getPassword())
// β
Good - only non-sensitive data
.claim("role", user.getRole())
3. Don't skip validation
// β Parse without validation
Jwts.parser().parse(token)
// β
Parse with signature verification
Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
Summaryβ
The Three Dependenciesβ
jjwt-api β Interface (compile time)
jjwt-impl β Implementation (runtime)
jjwt-jackson β JSON processor (runtime)
Quick Startβ
1. Add to pom.xml:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
2. Create JWT:
String token = Jwts.builder()
.setSubject(username)
.setExpiration(expiryDate)
.signWith(secretKey)
.compact();
3. Validate JWT:
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
Remember: All three dependencies work together. The API defines methods, the implementation executes them, and Jackson handles the JSON. Missing any one = errors! β οΈ
Tags: #spring-boot #jwt #authentication #security #maven
