← Назад к вопросам

Как валидировать JSON Web Token

2.0 Middle🔥 141 комментариев
#Безопасность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Валидация JSON Web Token (JWT)

JWT (JSON Web Token) — это компактный, самодостаточный способ для безопасной передачи информации между сторонами. Валидация JWT включает проверку подписи, истечения срока и других параметров.

Структура JWT

JWT состоит из трёх частей, разделённых точками:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

  ↓                ↓                    ↓
Header       Payload                 Signature

Компоненты JWT

// Header
{
  "alg": "HS256",  // Алгоритм
  "typ": "JWT"     // Тип токена
}

// Payload (Claims)
{
  "sub": "1234567890",    // Subject (кому)
  "name": "John Doe",     // Имя
  "iat": 1516239022,      // Issued At (когда выдан)
  "exp": 1516242622,      // Expiration (когда истекает)
  "roles": ["USER", "ADMIN"]
}

// Signature
HMAC_SHA256(
  base64UrlEncode(header) + "." + 
  base64UrlEncode(payload),
  secret_key
)

1. Валидация с использованием jjwt библиотеки

Моста популярная библиотека для работы с JWT:

<!-- 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. Базовая валидация JWT

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;

public class JwtValidator {
    private final SecretKey key;
    private final JwtParser parser;
    
    public JwtValidator(String secret) {
        // Ключ для подписи
        this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
        // Создаём парсер с ключом
        this.parser = Jwts.parserBuilder()
            .setSigningKey(key)
            .build();
    }
    
    // Валидация и получение claims
    public Claims validateToken(String token) {
        try {
            // Парсим и проверяем подпись
            Jws<Claims> jws = parser.parseClaimsJws(token);
            return jws.getBody();
        } catch (JwtException e) {
            throw new InvalidTokenException("Invalid JWT token", e);
        }
    }
    
    // Проверка истечения
    public boolean isTokenExpired(String token) {
        try {
            Claims claims = validateToken(token);
            return claims.getExpiration().before(new Date());
        } catch (JwtException e) {
            return true;
        }
    }
}

3. Полная валидация со всеми проверками

public class TokenValidator {
    private final SecretKey key;
    private final JwtParser parser;
    
    public TokenValidator(String secret) {
        this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
        this.parser = Jwts.parserBuilder()
            .setSigningKey(key)
            .requireIssuer("your-app")
            .requireAudience("your-api")
            .build();
    }
    
    public Claims validateAndGetClaims(String token) throws JwtValidationException {
        try {
            // 1. Проверка подписи — автоматически
            Jws<Claims> jws = parser.parseClaimsJws(token);
            Claims claims = jws.getBody();
            
            // 2. Проверка истечения срока
            if (isExpired(claims)) {
                throw new JwtValidationException("Token expired");
            }
            
            // 3. Проверка необходимых claims
            if (!hasClaim(claims, "sub")) {
                throw new JwtValidationException("Missing subject claim");
            }
            
            return claims;
            
        } catch (SignatureException e) {
            throw new JwtValidationException("Invalid signature", e);
        } catch (MalformedJwtException e) {
            throw new JwtValidationException("Invalid token format", e);
        } catch (ExpiredJwtException e) {
            throw new JwtValidationException("Token expired", e);
        } catch (UnsupportedJwtException e) {
            throw new JwtValidationException("Unsupported token", e);
        } catch (IllegalArgumentException e) {
            throw new JwtValidationException("Invalid token", e);
        }
    }
    
    private boolean isExpired(Claims claims) {
        Date expiration = claims.getExpiration();
        return expiration != null && expiration.before(new Date());
    }
    
    private boolean hasClaim(Claims claims, String claimName) {
        return claims.get(claimName) != null;
    }
}

4. Spring Security с JWT

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter(JwtValidator validator) {
        return new JwtAuthenticationFilter(validator);
    }
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthenticationFilter filter) 
            throws Exception {
        http
            .csrf().disable()
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/login").permitAll()
                .anyRequest().authenticated()
            )
            .addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
}

5. JWT фильтр для Spring Security

public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtValidator validator;
    
    public JwtAuthenticationFilter(JwtValidator validator) {
        this.validator = validator;
    }
    
    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain
    ) throws ServletException, IOException {
        try {
            String token = extractTokenFromRequest(request);
            
            if (token != null) {
                // Валидируем токен
                Claims claims = validator.validateToken(token);
                
                // Создаём Authentication объект
                String username = claims.getSubject();
                List<String> roles = claims.get("roles", List.class);
                
                List<GrantedAuthority> authorities = roles.stream()
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());
                
                UsernamePasswordAuthenticationToken auth = 
                    new UsernamePasswordAuthenticationToken(
                        username, 
                        null, 
                        authorities
                    );
                
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        } catch (JwtValidationException e) {
            response.sendError(
                HttpServletResponse.SC_UNAUTHORIZED,
                "Invalid JWT token: " + e.getMessage()
            );
            return;
        }
        
        filterChain.doFilter(request, response);
    }
    
    private String extractTokenFromRequest(HttpServletRequest request) {
        String authHeader = request.getHeader("Authorization");
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            return authHeader.substring(7);
        }
        return null;
    }
}

6. Создание и валидация токенов

@Service
public class AuthService {
    private final SecretKey key;
    private final JwtValidator validator;
    
    public AuthService(String secret, JwtValidator validator) {
        this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
        this.validator = validator;
    }
    
    // Создание токена
    public String generateToken(String username, List<String> roles) {
        long nowMillis = System.currentTimeMillis();
        long expirationMillis = nowMillis + 3600000; // 1 час
        
        return Jwts.builder()
            .setSubject(username)
            .claim("roles", roles)
            .claim("iat", nowMillis / 1000)
            .setIssuedAt(new Date(nowMillis))
            .setExpiration(new Date(expirationMillis))
            .signWith(key, SignatureAlgorithm.HS256)
            .compact();
    }
    
    // Валидация и получение пользователя
    public String getUsernameFromToken(String token) {
        Claims claims = validator.validateToken(token);
        return claims.getSubject();
    }
    
    // Получение ролей
    public List<String> getRolesFromToken(String token) {
        Claims claims = validator.validateToken(token);
        return claims.get("roles", List.class);
    }
}

7. Обработка исключений при валидации

public class JwtExceptionHandler {
    public static void handleJwtException(Exception e) {
        if (e instanceof SignatureException) {
            // Подпись невалидна
            System.out.println("Invalid signature");
        } else if (e instanceof MalformedJwtException) {
            // Токен в неправильном формате
            System.out.println("Invalid token format");
        } else if (e instanceof ExpiredJwtException) {
            // Токен истёк
            System.out.println("Token expired");
        } else if (e instanceof UnsupportedJwtException) {
            // Неподдерживаемый формат
            System.out.println("Unsupported token");
        } else if (e instanceof IllegalArgumentException) {
            // Пустой или null
            System.out.println("Empty token");
        }
    }
}

8. Проверка с использованием public ключа (асимметричное)

Для асимметричной подписи (RS256):

public class RsaJwtValidator {
    private final PublicKey publicKey;
    private final JwtParser parser;
    
    public RsaJwtValidator(String publicKeyPem) throws Exception {
        this.publicKey = loadPublicKey(publicKeyPem);
        this.parser = Jwts.parserBuilder()
            .setSigningKey(publicKey)
            .build();
    }
    
    public Claims validateToken(String token) {
        try {
            return parser.parseClaimsJws(token).getBody();
        } catch (JwtException e) {
            throw new InvalidTokenException("Invalid JWT: " + e.getMessage());
        }
    }
    
    private PublicKey loadPublicKey(String publicKeyPem) throws Exception {
        String publicKeyContent = publicKeyPem
            .replace("-----BEGIN PUBLIC KEY-----", "")
            .replace("-----END PUBLIC KEY-----", "")
            .replaceAll("\\n", "")
            .replaceAll("\\r", "");
        
        byte[] decoded = Base64.getDecoder().decode(publicKeyContent);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePublic(spec);
    }
}

9. Валидация с Refresh Token

public class TokenPair {
    private String accessToken;   // Короткоживущий (15 минут)
    private String refreshToken;  // Долгоживущий (7 дней)
}

@Service
public class RefreshTokenService {
    
    public TokenPair refreshAccessToken(String refreshToken) {
        try {
            // Валидируем refresh токен
            Claims claims = validator.validateToken(refreshToken);
            
            String username = claims.getSubject();
            List<String> roles = claims.get("roles", List.class);
            
            // Генерируем новый access токен
            String newAccessToken = generateAccessToken(username, roles);
            
            return new TokenPair(newAccessToken, refreshToken);
        } catch (JwtException e) {
            throw new InvalidTokenException("Cannot refresh token", e);
        }
    }
}

Лучшие практики валидации JWT

  1. Всегда проверяй подпись — это основная защита
  2. Проверяй истечение токена — используй exp claim
  3. Валидируй необходимые claims — sub, iat, exp
  4. Используй secure ключи — достаточно длинные (256+ бит)
  5. Ротация ключей — периодически обновляй secret
  6. HTTPS только — JWT передаётся по защищённому каналу
  7. Не храни чувствительные данные — в payload это видимо
  8. Используй refresh tokens — access токены с коротким TTL
  9. Обработка исключений — правильно обрабатывай ошибки
  10. Логирование — логируй попытки с невалидными токенами

Валидация JWT — критический элемент безопасности. Правильная реализация защищает приложение от подделанных токенов и несанкционированного доступа.