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

Какие знаешь способы валидации токена?

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

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

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

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

Способы валидации токена

Валидация токена — критический компонент безопасности современных веб-приложений. Существует несколько подходов, различающихся по сложности, безопасности и performance. Рассмотрю способы, которые использовал в production системах на Java.

1. JWT (JSON Web Token) валидация

Одна из самых популярных схем. JWT состоит из трёх частей, разделённых точками: header.payload.signature

// Зависимость: io.jsonwebtoken:jjwt

public class JWTValidator {
    private final String secretKey;
    private final long expirationTime;  // в миллисекундах
    
    public JWTValidator(String secretKey, long expirationTime) {
        this.secretKey = secretKey;
        this.expirationTime = expirationTime;
    }
    
    // СОЗДАНИЕ токена
    public String generateToken(String userId, Map<String, Object> claims) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expirationTime);
        
        return Jwts.builder()
                .subject(userId)
                .claims(claims)
                .issuedAt(now)
                .expiration(expiryDate)
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }
    
    // ВАЛИДАЦИЯ токена
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(secretKey)
                    .build()
                    .parseClaimsJws(token);  // Парсит и валидирует подпись
            return true;
        } catch (SignatureException e) {
            // Подпись невалидна
            throw new SecurityException("Invalid JWT signature", e);
        } catch (MalformedJwtException e) {
            // Формат токена неверный
            throw new SecurityException("Invalid JWT format", e);
        } catch (ExpiredJwtException e) {
            // Токен истёк
            throw new SecurityException("JWT token expired", e);
        } catch (UnsupportedJwtException e) {
            // Алгоритм не поддерживается
            throw new SecurityException("JWT not supported", e);
        } catch (IllegalArgumentException e) {
            // Пустой токен
            throw new SecurityException("JWT claims string is empty", e);
        }
    }
    
    // ИЗВЛЕЧЕНИЕ данных из токена
    public String extractUserId(String token) {
        try {
            Claims claims = Jwts.parserBuilder()
                    .setSigningKey(secretKey)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
            return claims.getSubject();
        } catch (JwtException e) {
            return null;
        }
    }
    
    public Claims extractClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
    
    public boolean isTokenExpired(String token) {
        try {
            Claims claims = extractClaims(token);
            return claims.getExpiration().before(new Date());
        } catch (JwtException e) {
            return true;
        }
    }
}

// Использование
public class AuthService {
    private final JWTValidator jwtValidator;
    
    public AuthService(JWTValidator jwtValidator) {
        this.jwtValidator = jwtValidator;
    }
    
    public String login(String username, String password) {
        // Проверяем пароль
        if (validatePassword(username, password)) {
            Map<String, Object> claims = new HashMap<>();
            claims.put("role", "ADMIN");
            claims.put("email", "user@example.com");
            
            return jwtValidator.generateToken(username, claims);
        }
        throw new UnauthorizedException("Invalid credentials");
    }
    
    public User authenticate(String token) {
        jwtValidator.validateToken(token);  // Выбросит exception если невалидно
        
        String userId = jwtValidator.extractUserId(token);
        return userRepository.findById(userId)
                .orElseThrow(() -> new UnauthorizedException("User not found"));
    }
}

RSA подпись для JWT

Безопаснее HS256, так как приватный ключ не нужно хранить везде:

public class RSAJWTValidator {
    private final PrivateKey privateKey;  // Для создания токенов
    private final PublicKey publicKey;    // Для валидации
    
    public String generateToken(String userId) {
        return Jwts.builder()
                .subject(userId)
                .issuedAt(new Date())
                .expiration(new Date(System.currentTimeMillis() + 3600000))
                .signWith(privateKey, SignatureAlgorithm.RS256)  // RSA!
                .compact();
    }
    
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(publicKey)  // Публичный ключ
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (JwtException e) {
            return false;
        }
    }
}

2. OAuth 2.0 валидация (с сервером авторизации)

Для микросервисной архитектуры, где токены создает центральный сервер:

@Configuration
@EnableResourceServer  // Spring Security
public class OAuthConfiguration extends ResourceServerConfigurerAdapter {
    
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .oauth2Login();  // OAuth 2.0 login
    }
}

public class TokenValidator {
    private final RestTemplate restTemplate;
    private final String tokenEndpoint;
    
    // Валидация через обращение к сервису авторизации
    public boolean validateTokenWithServer(String token) {
        try {
            HttpHeaders headers = new HttpHeaders();
            headers.add("Authorization", "Bearer " + token);
            headers.setContentType(MediaType.APPLICATION_JSON);
            
            ResponseEntity<TokenValidationResponse> response = 
                    restTemplate.exchange(
                            tokenEndpoint,
                            HttpMethod.POST,
                            new HttpEntity<>(headers),
                            TokenValidationResponse.class
                    );
            
            return response.getStatusCode().is2xxSuccessful()
                    && response.getBody().isValid();
        } catch (RestClientException e) {
            return false;
        }
    }
}

3. Валидация сессионного токена (stateful)

Традиционный подход с хранением сессий на сервере:

public class SessionTokenValidator {
    private final SessionRepository sessionRepository;  // БД или кэш
    
    // СОЗДАНИЕ токена
    public String generateSessionToken(String userId) {
        String token = UUID.randomUUID().toString();
        
        Session session = new Session();
        session.setToken(token);
        session.setUserId(userId);
        session.setCreatedAt(Instant.now());
        session.setExpiresAt(Instant.now().plus(Duration.ofHours(24)));
        
        sessionRepository.save(session);
        return token;
    }
    
    // ВАЛИДАЦИЯ
    public Session validateSessionToken(String token) {
        return sessionRepository.findByToken(token)
                .filter(session -> !session.isExpired())  // Проверка срока
                .orElseThrow(() -> new UnauthorizedException("Invalid session"));
    }
    
    // ИНВАЛИДАЦИЯ (logout)
    public void invalidateToken(String token) {
        sessionRepository.deleteByToken(token);
    }
}

public class Session {
    @Id
    private String token;
    private String userId;
    private Instant createdAt;
    private Instant expiresAt;
    
    public boolean isExpired() {
        return Instant.now().isAfter(expiresAt);
    }
}

4. Two-Token система (Access + Refresh токены)

Максимизирует безопасность при сохранении performance:

public class TwoTokenValidator {
    private final String accessTokenSecret;
    private final String refreshTokenSecret;
    private final long accessTokenExpiry = 15 * 60 * 1000;      // 15 минут
    private final long refreshTokenExpiry = 7 * 24 * 60 * 1000;  // 7 дней
    
    // СОЗДАНИЕ пары токенов
    public TokenPair generateTokenPair(String userId) {
        String accessToken = Jwts.builder()
                .subject(userId)
                .claim("type", "access")
                .issuedAt(new Date())
                .expiration(new Date(System.currentTimeMillis() + accessTokenExpiry))
                .signWith(SignatureAlgorithm.HS256, accessTokenSecret)
                .compact();
        
        String refreshToken = Jwts.builder()
                .subject(userId)
                .claim("type", "refresh")
                .issuedAt(new Date())
                .expiration(new Date(System.currentTimeMillis() + refreshTokenExpiry))
                .signWith(SignatureAlgorithm.HS256, refreshTokenSecret)
                .compact();
        
        return new TokenPair(accessToken, refreshToken);
    }
    
    // ВАЛИДАЦИЯ access токена
    public String validateAccessToken(String token) {
        try {
            Claims claims = Jwts.parserBuilder()
                    .setSigningKey(accessTokenSecret)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
            
            if (!"access".equals(claims.get("type"))) {
                throw new SecurityException("Not an access token");
            }
            
            return claims.getSubject();
        } catch (ExpiredJwtException e) {
            throw new TokenExpiredException("Access token expired", e);
        }
    }
    
    // ОБНОВЛЕНИЕ access токена используя refresh токен
    public String refreshAccessToken(String refreshToken) {
        try {
            Claims claims = Jwts.parserBuilder()
                    .setSigningKey(refreshTokenSecret)
                    .build()
                    .parseClaimsJws(refreshToken)
                    .getBody();
            
            if (!"refresh".equals(claims.get("type"))) {
                throw new SecurityException("Not a refresh token");
            }
            
            String userId = claims.getSubject();
            return Jwts.builder()
                    .subject(userId)
                    .claim("type", "access")
                    .issuedAt(new Date())
                    .expiration(new Date(System.currentTimeMillis() + accessTokenExpiry))
                    .signWith(SignatureAlgorithm.HS256, accessTokenSecret)
                    .compact();
        } catch (JwtException e) {
            throw new SecurityException("Invalid refresh token", e);
        }
    }
}

public class TokenPair {
    public final String accessToken;
    public final String refreshToken;
    
    public TokenPair(String accessToken, String refreshToken) {
        this.accessToken = accessToken;
        this.refreshToken = refreshToken;
    }
}

// REST контроллер
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
    private final TwoTokenValidator twoTokenValidator;
    
    @PostMapping("/refresh")
    public ResponseEntity<TokenResponse> refresh(
            @RequestHeader("Authorization") String bearerToken) {
        String refreshToken = bearerToken.replace("Bearer ", "");
        String newAccessToken = twoTokenValidator.refreshAccessToken(refreshToken);
        return ResponseEntity.ok(new TokenResponse(newAccessToken));
    }
}

5. HMAC (Hash-based Message Authentication Code)

Симметричная валидация с секретным ключом:

public class HMACTokenValidator {
    private final String secret;
    
    public String generateToken(String data, String userId) throws Exception {
        Mac hmac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKeySpec = new SecretKeySpec(
                secret.getBytes(StandardCharsets.UTF_8),
                0,
                secret.getBytes(StandardCharsets.UTF_8).length,
                "HmacSHA256"
        );
        hmac.init(secretKeySpec);
        
        String payload = userId + ":" + System.currentTimeMillis();
        byte[] hash = hmac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
        String signature = Base64.getEncoder().encodeToString(hash);
        
        return payload + ":" + signature;
    }
    
    public boolean validateToken(String token) throws Exception {
        String[] parts = token.split(":");
        if (parts.length != 3) return false;
        
        String userId = parts[0];
        String timestamp = parts[1];
        String signature = parts[2];
        
        // Проверяем срок
        long tokenTime = Long.parseLong(timestamp);
        if (System.currentTimeMillis() - tokenTime > 3600000) {  // 1 час
            return false;
        }
        
        // Пересчитываем подпись и сравниваем
        String payload = userId + ":" + timestamp;
        Mac hmac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKeySpec = new SecretKeySpec(
                secret.getBytes(StandardCharsets.UTF_8),
                0,
                secret.getBytes(StandardCharsets.UTF_8).length,
                "HmacSHA256"
        );
        hmac.init(secretKeySpec);
        
        byte[] hash = hmac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
        String expectedSignature = Base64.getEncoder().encodeToString(hash);
        
        return signature.equals(expectedSignature);
    }
}

6. Bearer токен валидация (в HTTP заголовке)

public class BearerTokenValidator {
    
    // Извлечение токена из заголовка
    public String extractTokenFromHeader(HttpRequest request) {
        String authHeader = request.getHeader("Authorization");
        
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            return authHeader.substring("Bearer ".length());
        }
        throw new UnauthorizedException("Missing or invalid Authorization header");
    }
    
    // Spring Security фильтр для валидации
    @Component
    public class TokenAuthenticationFilter extends OncePerRequestFilter {
        
        private final JWTValidator jwtValidator;
        
        @Override
        protected void doFilterInternal(
                HttpServletRequest request,
                HttpServletResponse response,
                FilterChain filterChain) throws ServletException, IOException {
            
            String authHeader = request.getHeader("Authorization");
            
            if (authHeader != null && authHeader.startsWith("Bearer ")) {
                String token = authHeader.substring("Bearer ".length());
                
                try {
                    jwtValidator.validateToken(token);
                    String userId = jwtValidator.extractUserId(token);
                    
                    // Устанавливаем Spring Security context
                    UsernamePasswordAuthenticationToken auth = 
                            new UsernamePasswordAuthenticationToken(
                                    userId, null, new ArrayList<>()
                            );
                    SecurityContextHolder.getContext().setAuthentication(auth);
                } catch (JwtException e) {
                    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    return;
                }
            }
            
            filterChain.doFilter(request, response);
        }
    }
}

7. Черный список (Blacklist) для инвалидации

Для logout и отзыва токенов:

@Service
public class TokenBlacklistService {
    private final RedisTemplate<String, String> redisTemplate;
    
    // Добавляем токен в черный список
    public void blacklistToken(String token, long expirationTime) {
        long ttl = expirationTime - System.currentTimeMillis();
        if (ttl > 0) {
            redisTemplate.opsForValue().set(
                    "blacklist:" + token,
                    "true",
                    ttl,
                    TimeUnit.MILLISECONDS
            );
        }
    }
    
    // Проверяем, в ли токен черном списке
    public boolean isTokenBlacklisted(String token) {
        return Boolean.TRUE.equals(
                redisTemplate.hasKey("blacklist:" + token)
        );
    }
    
    // Logout
    public void logout(String token) {
        Claims claims = extractClaims(token);
        blacklistToken(token, claims.getExpiration().getTime());
    }
}

// Валидация с проверкой черного списка
public class SecureTokenValidator {
    private final JWTValidator jwtValidator;
    private final TokenBlacklistService blacklistService;
    
    public User validateToken(String token) {
        // Проверяем черный список ДО парсинга (более быстро)
        if (blacklistService.isTokenBlacklisted(token)) {
            throw new UnauthorizedException("Token has been revoked");
        }
        
        // Валидируем подпись и срок
        jwtValidator.validateToken(token);
        
        return getUserFromToken(token);
    }
}

8. Сравнение методов

МетодБезопасностьPerformanceМасштабируемостьСложность
JWT✓ Высокая (подпись)✓✓ Отличная✓✓ ОтличнаяСредняя
Session✓✓ Очень высокая✗ Требует БД✗ Требует синхроНизкая
OAuth 2.0✓✓ Очень высокая✗ Требует запроса~ Хорошая✓✓ Высокая
HMAC✓ Хорошая✓✓ Отличная✓✓ ОтличнаяНизкая
Two-Token✓✓✓ Максимум✓✓ Отличная✓✓ Отличная✓ Высокая

9. Best Practices валидации

@Component
public class TokenValidationBestPractices {
    
    // ✓ НИКОГДА не доверяй данным из токена без валидации подписи
    public void badExample() {
        String payload = Base64.getDecoder().decode(tokenPart);
        // НЕПРАВИЛЬНО! Можно подделать!
    }
    
    // ✓ ВСЕГДА валидируй подпись
    public void goodExample(String token) {
        Jwts.parserBuilder()
                .setSigningKey(secret)  // Обязательно!
                .build()
                .parseClaimsJws(token);
    }
    
    // ✓ КЭШИРУЙ публичные ключи для OAuth
    @Cacheable("oauth-public-keys")
    public PublicKey getPublicKey(String keyId) {
        // Загружаем публичный ключ один раз
        return oauthProvider.getPublicKey(keyId);
    }
    
    // ✓ ИСПОЛЬЗУЙ Rate Limiting на endpoint валидации
    @RateLimiter(limit = 100, duration = "1m")
    public void validateToken(String token) { }
    
    // ✓ ЛОГИРУЙ неудачные попытки валидации
    public void validateWithLogging(String token) {
        try {
            validateToken(token);
        } catch (JwtException e) {
            logger.warn("Token validation failed: {}", e.getMessage());
            // Отправляем в security monitoring
        }
    }
}

Вывод

Выбор метода зависит от требований:

  • JWT: быстрая валидация, stateless сервисы, микросервисы
  • Session: высокая безопасность, полный контроль, монолитные приложения
  • OAuth 2.0: интеграция с внешними сервисами, SSO
  • Two-Token: баланс безопасности и performance

В production я обычно использую Two-Token JWT систему с публичным/приватным ключами, чёрным списком для logout, и rate limiting на endpoints.

Какие знаешь способы валидации токена? | PrepBro