Как реализовать механизм авторизации с использованием JWT токенов в 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 приложений

Ответ 18+ 🔞

О, слушай, смотри, вот эта вся хуйня с JWT — это ж как в баню с собой тазик тащить, только в мире апишек. Все им пользуются, а половина нихуя не понимает, как оно внутри работает, просто тыкают, пока не заработает.

Архитектура, блядь, этой пляски с бубном:

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

1. Рождение токена, или «Пойди туда, не знаю куда» (на сервере):

Смотри, вот ты на сервере, пользователь прислал логин-пароль, ты проверил — вроде не мудак. Надо ему пропуск выписать. Делается это так:

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

// Секретный ключ (это твоя мантра, храни как зеницу ока, а то потом охуеешь!)
String secretKey = "mySuperSecretKeyAtLeast32CharactersLong";

// Начинаем колдовать
String token = Jwts.builder()
    .setSubject(user.getUsername())           // Кто этот тип?
    .claim("roles", user.getRoles())         // А чем он, блядь, дышит? Какие права?
    .claim("userId", user.getId())
    .setIssuedAt(new Date())                  // Когда выписан?
    .setExpiration(new Date(System.currentTimeMillis() + 3600000)) // Годится до... (час)
    .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); // И права вот эти...

        // Смотрим, не просрочен ли пропуск
        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() // Для API часто выключают, но знай, что делаешь!
            .authorizeRequests()
                .antMatchers("/api/auth/**").permitAll() // Сюда пускаем всех
                .antMatchers("/api/admin/**").hasRole("ADMIN") // Сюда только начальство
                .anyRequest().authenticated() // Всё остальное — только по пропускам
            .and()
            .addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class); // Ставим наш фильтр-шпингалет
    }

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

// Сам фильтр, который каждого входящего будет щупать
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); // Отрезаем "Bearer "
            if (jwtUtil.validateToken(token)) { // Проверяем
                Authentication auth = jwtUtil.getAuthentication(token); // Создаём объект аутентификации
                SecurityContextHolder.getContext().setAuthentication(auth); // И кладём в контекст
            }
        }
        chain.doFilter(request, response); // Пропускаем дальше
    }
}

4. Best Practices, или как не обосраться:

  • Ключ, ёпта! Не хардкодь его в коде, как последний ламер. Environment variables, Vault, что угодно. Иначе однажды проснёшься знаменитым.
  • Алгоритмы:
    • HS256/HS512 — симметричные. Один ключ и на подпись, и на проверку. Просто, но если ключ утек — пиздец всему.
    • RS256/RS512 — асимметричные. Приватным ключом подписал, публичным проверил. Умнее.
  • Refresh tokens: Чтобы пользователь каждый час не бегал логиниться, как угорелый. Даёшь ему два токена: короткоживущий access_token (час) и долгоживущий refresh_token (неделя). Первый кончился — меняешь по второму.
    {
      "access_token": "eyJ...",
      "refresh_token": "def...",
      "expires_in": 3600
    }
  • Безопасность, мать её:
    • Жизнь access token — 15-60 минут. Не делай его бессмертным.
    • При логауте хорошо бы токены в чёрный список (blacklist) пихать, но это уже сложнее.
    • HTTPS — ОБЯЗАТЕЛЕН. Иначе все твои токены летят по сети как голые по проспекту.
    • Проверяй iss (кто выдал) и aud (для кого), если в этом есть смысл.

5. А что ещё есть, кроме этого?

  • OAuth 2.0 — это уже не просто токен, а целый протокол с дедушкой-авторизационным сервером, бабушкой-ресурсным сервером и внучкой-клиентом. JWT часто внутри него в качестве access token используется.
  • OpenID Connect — надстройка над OAuth 2.0, которая ещё и личность пользователя подтверждает.
  • Spring Security OAuth2 — если ты на Spring, то можешь не изобретать велосипед, а взять готовый, с багажником и фарами.

Вот и вся магия. Главное — не накосячить с безопасностью, а то будет не «JWT-авторизация», а «JWT-аутентификация твоих косяков перед всем интернетом».