JSON Web Tokens (JWT) w praktyce

TL;DR: JWT to format tokenu składający się z trzech części oddzielonych kropkami: header.payload.signature. Zawiera informacje o użytkowniku (claims) i jest podpisany cyfrowo. Używany do stateless authentication w API – klient wysyła token w nagłówku Authorization: Bearer. Nie wymaga session storage na serwerze.

W świecie REST API i aplikacji mobilnych potrzebujemy mechanizmu autoryzacji który nie wymaga sesji po stronie serwera. JWT (JSON Web Tokens) to eleganckie rozwiązanie tego problemu – kompaktowy, self-contained token który może bezpiecznie przenosić informacje o użytkowniku między klientem a serwerem.

Dlaczego JWT jest ważne

W 2016 roku JWT zyskuje popularność jako standard autoryzacji w API. Używa go GitHub, Auth0, Firebase i setki innych serwisów. JWT pozwala na stateless authentication – każdy request zawiera wszystkie potrzebne informacje, co czyni aplikacje łatwiejszymi do skalowania.

Stateless architecture to konieczność w nowoczesnych aplikacjach. JWT eliminuje potrzebę session storage, ułatwia load balancing i scaling. Dla firm oznacza to niższe koszty infrastruktury i lepszą performance aplikacji mobilnych.

Co się nauczysz:

  • Czym jest JWT i jak działa
  • Struktura tokenu: header, payload, signature
  • Jak tworzyć i weryfikować JWT w Javie
  • Best practices dla bezpieczeństwa JWT
  • Kiedy używać JWT, a kiedy tradycyjnych sesji

Wymagania wstępne:

  • Podstawowa znajomość REST API
  • Rozumienie HTTP headers i authentication
  • Znajomość JSON format
  • Podstawy kryptografii (podpis cyfrowy, hashing)

Struktura JWT

Analogia: JWT to jak paszport. Ma header (typ dokumentu), payload (twoje dane osobowe) i signature (pieczęć rządowa). Każdy może przeczytać dane, ale tylko rząd może wystawić prawdziwy paszport z poprawną pieczęcią.

JWT składa się z trzech części zakodowanych w Base64URL i oddzielonych kropkami:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Header          .         Payload          .         Signature

Header

Zawiera informacje o algorytmie podpisu:

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload (Claims)

Zawiera dane o użytkowniku i metadane:

{
  "sub": "1234567890",       // Subject (user ID)
  "name": "John Doe",        // Custom claim
  "email": "john@example.com", // Custom claim
  "role": "admin",           // Custom claim
  "iat": 1516239022,          // Issued at
  "exp": 1516325422           // Expiration time
}

Signature

Podpis cyfrowy weryfikujący autentyczność:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)
Pro tip: Payload nie jest szyfrowany, tylko zakodowany Base64. Nie umieszczaj tam wrażliwych danych jak hasła czy numery kart kredytowych!

Implementacja JWT w Javie

Maven dependency


    io.jsonwebtoken
    jjwt
    0.7.0

Tworzenie JWT

import io.jsonwebtoken.*;
import java.util.Date;

public class JwtUtil {
    
    private static final String SECRET_KEY = "mySecretKey123";
    private static final long EXPIRATION_TIME = 86400000; // 24 hours in milliseconds
    
    public static String generateToken(String username, String role) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + EXPIRATION_TIME);
        
        return Jwts.builder()
                .setSubject(username)                    // Subject claim
                .claim("role", role)                    // Custom claim
                .setIssuedAt(now)                        // Issued at
                .setExpiration(expiryDate)               // Expiration
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY) // Signature
                .compact();
    }
    
    // Example usage
    public static void main(String[] args) {
        String token = generateToken("john.doe", "admin");
        System.out.println("Generated JWT: " + token);
    }
}

Weryfikacja i odczytywanie JWT

public class JwtUtil {
    
    public static Claims parseToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(SECRET_KEY)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e) {
            throw new RuntimeException("Token expired", e);
        } catch (MalformedJwtException e) {
            throw new RuntimeException("Invalid token format", e);
        } catch (SignatureException e) {
            throw new RuntimeException("Invalid token signature", e);
        }
    }
    
    public static String getUsernameFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.getSubject();
    }
    
    public static String getRoleFromToken(String token) {
        Claims claims = parseToken(token);
        return (String) claims.get("role");
    }
    
    public static boolean isTokenExpired(String token) {
        try {
            Claims claims = parseToken(token);
            return claims.getExpiration().before(new Date());
        } catch (Exception e) {
            return true; // Treat invalid tokens as expired
        }
    }
    
    public static boolean validateToken(String token, String username) {
        try {
            String tokenUsername = getUsernameFromToken(token);
            return username.equals(tokenUsername) && !isTokenExpired(token);
        } catch (Exception e) {
            return false;
        }
    }
}

Użycie JWT w REST API

Login endpoint

@RestController
public class AuthController {
    
    @Autowired
    private UserService userService;
    
    @PostMapping("/login")
    public ResponseEntity login(@RequestBody LoginRequest loginRequest) {
        // Verify credentials
        User user = userService.authenticate(
            loginRequest.getUsername(), 
            loginRequest.getPassword()
        );
        
        if (user != null) {
            // Generate JWT token
            String token = JwtUtil.generateToken(user.getUsername(), user.getRole());
            
            return ResponseEntity.ok(new JwtResponse(token, user.getUsername(), user.getRole()));
        } else {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body(new ErrorResponse("Invalid credentials"));
        }
    }
}

// Response classes
public class JwtResponse {
    private String token;
    private String username;
    private String role;
    
    // constructors, getters, setters
}

public class LoginRequest {
    private String username;
    private String password;
    
    // getters, setters
}

JWT Filter dla autoryzacji

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                  HttpServletResponse response, 
                                  FilterChain filterChain) throws ServletException, IOException {
        
        String token = extractTokenFromRequest(request);
        
        if (token != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            try {
                String username = JwtUtil.getUsernameFromToken(token);
                
                if (JwtUtil.validateToken(token, username)) {
                    // Create authentication object
                    String role = JwtUtil.getRoleFromToken(token);
                    List authorities = 
                        Arrays.asList(new SimpleGrantedAuthority("ROLE_" + role.toUpperCase()));
                    
                    UsernamePasswordAuthenticationToken authentication = 
                        new UsernamePasswordAuthenticationToken(username, null, authorities);
                    
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            } catch (Exception e) {
                // Token is invalid
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return;
            }
        }
        
        filterChain.doFilter(request, response);
    }
    
    private String extractTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

Zabezpieczone endpointy

@RestController
@RequestMapping("/api")
public class SecureController {
    
    @GetMapping("/profile")
    public ResponseEntity getProfile(Authentication authentication) {
        String username = authentication.getName();
        User user = userService.findByUsername(username);
        return ResponseEntity.ok(user);
    }
    
    @GetMapping("/admin/users")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity> getAllUsers() {
        List users = userService.findAll();
        return ResponseEntity.ok(users);
    }
}

Klient – używanie JWT

JavaScript/AJAX example

// Login i otrzymanie tokenu
fetch('/login', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        username: 'john.doe',
        password: 'password123'
    })
})
.then(response => response.json())
.then(data => {
    // Zapisz token w localStorage
    localStorage.setItem('jwt_token', data.token);
    
    // Użyj tokenu w kolejnych requestach
    makeAuthenticatedRequest();
});

function makeAuthenticatedRequest() {
    const token = localStorage.getItem('jwt_token');
    
    fetch('/api/profile', {
        method: 'GET',
        headers: {
            'Authorization': 'Bearer ' + token
        }
    })
    .then(response => response.json())
    .then(data => {
        console.log('User profile:', data);
    })
    .catch(error => {
        if (error.status === 401) {
            // Token expired or invalid
            localStorage.removeItem('jwt_token');
            window.location.href = '/login';
        }
    });
}

Bezpieczeństwo JWT

⚠️ Ważne kwestie bezpieczeństwa:

  • Secret key – używaj silnego, losowego klucza (min. 256 bitów)
  • HTTPS tylko – nigdy nie przesyłaj JWT przez HTTP
  • Krótki czas wygaśnięcia – 15-60 minut dla access tokenów
  • Refresh tokens – długoterminowe tokeny do odnawiania
  • Nie przechowuj wrażliwych danych – payload jest tylko zakodowany, nie zaszyfrowany

Refresh token pattern

public class TokenResponse {
    private String accessToken;
    private String refreshToken;
    private long expiresIn;
    
    // getters, setters
}

@PostMapping("/refresh")
public ResponseEntity refreshToken(@RequestBody RefreshTokenRequest request) {
    String refreshToken = request.getRefreshToken();
    
    if (refreshTokenService.isValidRefreshToken(refreshToken)) {
        String username = refreshTokenService.getUsernameFromRefreshToken(refreshToken);
        User user = userService.findByUsername(username);
        
        // Generate new access token
        String newAccessToken = JwtUtil.generateToken(user.getUsername(), user.getRole());
        
        return ResponseEntity.ok(new TokenResponse(newAccessToken, refreshToken, 3600));
    } else {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(new ErrorResponse("Invalid refresh token"));
    }
}

JWT vs Sessions – porównanie

AspektJWTSessions
StatelessTakNie (server state)
ScalabilityBardzo dobraOgraniczona
Memory usageNiskieWysokie
SecurityDobra (z best practices)Bardzo dobra
RevocationTrudneŁatwe
Cross-domainŁatweSkomplikowane
Mobile appsIdealneProblematyczne

Best practices

✅ JWT best practices:

  • Używaj HTTPS – zawsze szyfruj transmisję
  • Krótkie TTL – 15-60 minut dla access tokenów
  • Validate input – sprawdzaj wszystkie claims
  • Strong secrets – używaj kryptograficznie silnych kluczy
  • Handle exceptions – graceful handling expired/invalid tokens
  • Blacklist mechanism – dla logout i revocation
Błąd #1: Przechowywanie wrażliwych danych w payload – JWT jest tylko zakodowany, nie zaszyfrowany.
Błąd #2: Zbyt długi czas wygaśnięcia – zwiększa ryzyko security breach.
Błąd #3: Słaby secret key – używaj minimum 256-bitowych losowych kluczy.
Błąd #4: Brak HTTPS – token może być przechwycony przez man-in-the-middle.
Gdzie przechowywać JWT po stronie klienta?

localStorage jest wygodny ale podatny na XSS. httpOnly cookies są bezpieczniejsze ale wymagają CSRF protection. W aplikacjach mobilnych używaj secure storage.

Jak zaimplementować logout z JWT?

JWT jest stateless więc nie można go „wylogować” po stronie serwera. Opcje: token blacklist, krótkie TTL z refresh tokenami, lub store token state w bazie.

Czy JWT jest lepsze niż cookies?

Zależy od use case. JWT lepsze dla API i mobile apps, cookies lepsze dla traditional web apps. JWT jest stateless, cookies wymagają server-side session storage.

Jak długi powinien być JWT?

JWT powinien zawierać minimum informacji. Duże tokeny (>8KB) mogą nie mieścić się w HTTP headers. Include tylko niezbędne claims.

Czy mogę używać JWT do session management?

Można, ale JWT jest designed for stateless auth. Dla session management lepiej użyć tradycyjnych sessions lub hybrid approach z JWT + server-side validation.

Jak rotować JWT secret keys?

Use key versioning – include key ID w header, maintain multiple active keys, gradually phase out old keys. Critical dla security w production systems.

Przydatne zasoby:

🚀 Zadanie dla Ciebie

Zaimplementuj kompletny JWT authentication system:

  • Login endpoint generujący JWT z user claims
  • JWT filter validujący tokeny w każdym requeście
  • Protected endpoints wymagające authorization
  • Refresh token mechanism dla długoterminowych sesji
  • Logout endpoint invalidujący tokeny
  • Role-based authorization (USER, ADMIN)

Przetestuj:

  • Login z poprawnymi i błędnymi credentials
  • Access do protected resources z valid/invalid tokens
  • Token expiration handling
  • Refresh token workflow

Masz pytania o JWT? Podziel się swoimi doświadczeniami w komentarzach – JWT ma swoje niuanse security, ale dobrze zaimplementowany znacznie ułatwia building scalable APIs!

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry