Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Способы валидации токена
Валидация токена — критический компонент безопасности современных веб-приложений. Существует несколько подходов, различающихся по сложности, безопасности и 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.