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.
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
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 )
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); Listauthorities = 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 ResponseEntitygetProfile(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
Aspekt | JWT | Sessions |
---|---|---|
Stateless | Tak | Nie (server state) |
Scalability | Bardzo dobra | Ograniczona |
Memory usage | Niskie | Wysokie |
Security | Dobra (z best practices) | Bardzo dobra |
Revocation | Trudne | Łatwe |
Cross-domain | Łatwe | Skomplikowane |
Mobile apps | Idealne | Problematyczne |
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
localStorage jest wygodny ale podatny na XSS. httpOnly cookies są bezpieczniejsze ale wymagają CSRF protection. W aplikacjach mobilnych używaj secure storage.
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.
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.
JWT powinien zawierać minimum informacji. Duże tokeny (>8KB) mogą nie mieścić się w HTTP headers. Include tylko niezbędne claims.
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.
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:
- JWT.io – JWT debugger and libraries
- RFC 7519 – JWT Specification
- JJWT – Java JWT library
- JWT Security Best Practices
🚀 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!