Как реализовать механизм авторизации с использованием JWT токенов в Java-приложении?

«Как реализовать механизм авторизации с использованием JWT токенов в Java-приложении?» — вопрос из категории Безопасность, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

JWT (JSON Web Token) — стандарт для создания токенов доступа, широко используемый в REST API и микросервисных архитектурах.

Архитектура JWT-авторизации:

graph LR
    A[Клиент] -->|1. Логин/пароль| B[Сервер авторизации]
    B -->|2. Проверка учетных данных| C[(База данных)]
    B -->|3. Генерация JWT| D[Клиент]
    D -->|4. Запрос с токеном| E[Защищенный ресурс]
    E -->|5. Верификация токена| F[Доступ к ресурсу]

1. Генерация JWT токена (на стороне сервера):

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;

// Секретный ключ (должен храниться безопасно!)
String secretKey = "mySuperSecretKeyAtLeast32CharactersLong";

// Создание JWT
String token = Jwts.builder()
    .setSubject(user.getUsername())           // Идентификатор пользователя
    .claim("roles", user.getRoles())         // Дополнительные claims
    .claim("userId", user.getId())
    .setIssuedAt(new Date())                  // Время создания
    .setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1 час
    .signWith(SignatureAlgorithm.HS512, secretKey.getBytes()) // Алгоритм подписи
    .compact();

// Отправка клиенту
response.setHeader("Authorization", "Bearer " + token);

2. Верификация токена (в каждом защищенном endpoint):

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;

public boolean validateToken(String token) {
    try {
        Jws<Claims> claimsJws = Jwts.parser()
            .setSigningKey(secretKey.getBytes())
            .parseClaimsJws(token);

        Claims claims = claimsJws.getBody();
        String username = claims.getSubject();
        List<String> roles = claims.get("roles", List.class);

        // Проверка expiration
        return !claims.getExpiration().before(new Date());

    } catch (Exception e) {
        // Невалидный токен
        return false;
    }
}

3. Spring Security интеграция (рекомендуемый способ):

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
                .antMatchers("/api/auth/**").permitAll()
                .antMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            .and()
            .addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    public JwtAuthenticationFilter jwtFilter() {
        return new JwtAuthenticationFilter();
    }
}

// Кастомный фильтр для JWT
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                   HttpServletResponse response,
                                   FilterChain chain) {
        String header = request.getHeader("Authorization");

        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);
            if (jwtUtil.validateToken(token)) {
                Authentication auth = jwtUtil.getAuthentication(token);
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        }
        chain.doFilter(request, response);
    }
}

4. Best Practices и безопасность:

  • Хранение секретного ключа: Используйте environment variables или специализированные хранилища (HashiCorp Vault, AWS Secrets Manager)
  • Алгоритмы подписи:
    • HS256/HS512 — симметричные (требуют общего секрета)
    • RS256/RS512 — асимметричные (private key для подписи, public для проверки)
  • Refresh tokens: Для продления сессии без повторного логина
    {
      "access_token": "eyJ...",
      "refresh_token": "def...",
      "expires_in": 3600
    }
  • Защита от компрометации:
    • Короткое время жизни access token (15-60 минут)
    • Blacklist токенов при logout
    • HTTPS обязателен
    • Проверка issuer (iss) и audience (aud)

5. Альтернативы и дополнения:

  • OAuth 2.0 — полный протокол авторизации (JWT часто используется как access token)
  • OpenID Connect — идентификация поверх OAuth 2.0
  • Spring Security OAuth2 — готовая реализация для Spring приложений