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

Какие знаешь способы реализации JWT аутентификации?

1.8 Middle🔥 211 комментариев
#Spring Framework#Безопасность

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

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

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

Способы реализации JWT аутентификации

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

1. Базовая JWT реализация с Spring Security

// Модель JWT
public class JwtTokenProvider {
    private final String secretKey;
    private final long jwtExpirationMs = 86400000;  // 24 часа
    
    public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) {
        this.secretKey = secretKey;
    }
    
    // Генерация JWT
    public String generateToken(UserDetails userDetails) {
        return Jwts.builder()
            .setSubject(userDetails.getUsername())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
            .signWith(SignatureAlgorithm.HS512, secretKey)
            .claim("roles", userDetails.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList()))
            .compact();
    }
    
    // Извлечение username из JWT
    public String getUsernameFromToken(String token) {
        return Jwts.parser()
            .setSigningKey(secretKey)
            .parseClaimsJws(token)
            .getBody()
            .getSubject();
    }
    
    // Проверка срока действия
    public boolean validateToken(String token) {
        try {
            Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(token);
            return true;
        } catch (ExpiredJwtException e) {
            logger.error("JWT token has expired", e);
            return false;
        } catch (JwtException | IllegalArgumentException e) {
            logger.error("Invalid JWT token", e);
            return false;
        }
    }
}

// Фильтр для проверки JWT
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtTokenProvider jwtTokenProvider;
    private final UserDetailsService userDetailsService;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) 
            throws ServletException, IOException {
        try {
            String token = extractTokenFromRequest(request);
            
            if (token != null && jwtTokenProvider.validateToken(token)) {
                String username = jwtTokenProvider.getUsernameFromToken(token);
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                
                // Создаём аутентификацию
                UsernamePasswordAuthenticationToken authentication = 
                    new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception e) {
            logger.error("Cannot set user authentication", e);
        }
        
        filterChain.doFilter(request, response);
    }
    
    private String extractTokenFromRequest(HttpServletRequest request) {
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            return header.substring(7);
        }
        return null;
    }
}

// Security конфигурация
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, 
                                                    JwtAuthenticationFilter jwtFilter) 
            throws Exception {
        http
            .csrf().disable()
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            .authorizeHttpRequests(auth -> auth
                .antMatchers("/api/auth/login", "/api/auth/register").permitAll()
                .antMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        
        return http.build();
    }
}

2. Refresh Token паттерн

JWT токены имеют срок действия, поэтому нужен refresh механизм:

@Entity
@Table(name = "refresh_tokens")
public class RefreshToken {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;
    
    @Column(nullable = false, unique = true)
    private String token;
    
    @Column(nullable = false)
    private LocalDateTime expiryDate;
    
    @Column(nullable = false)
    private LocalDateTime createdAt = LocalDateTime.now();
}

@Service
public class RefreshTokenService {
    private final RefreshTokenRepository refreshTokenRepository;
    private final JwtTokenProvider jwtTokenProvider;
    private final long refreshTokenDurationMs = 604800000;  // 7 дней
    
    public RefreshToken createRefreshToken(User user) {
        RefreshToken refreshToken = new RefreshToken();
        refreshToken.setUser(user);
        refreshToken.setToken(UUID.randomUUID().toString());
        refreshToken.setExpiryDate(
            LocalDateTime.now().plusSeconds(refreshTokenDurationMs / 1000)
        );
        
        return refreshTokenRepository.save(refreshToken);
    }
    
    public boolean validateRefreshToken(String token) {
        RefreshToken refreshToken = refreshTokenRepository.findByToken(token)
            .orElseThrow(() -> new RuntimeException("Invalid refresh token"));
        
        if (refreshToken.getExpiryDate().isBefore(LocalDateTime.now())) {
            refreshTokenRepository.delete(refreshToken);
            throw new RuntimeException("Refresh token expired");
        }
        
        return true;
    }
    
    public String refreshAccessToken(String refreshTokenString) {
        RefreshToken refreshToken = refreshTokenRepository.findByToken(refreshTokenString)
            .orElseThrow(() -> new RuntimeException("Invalid refresh token"));
        
        validateRefreshToken(refreshTokenString);
        
        User user = refreshToken.getUser();
        return jwtTokenProvider.generateToken(new org.springframework.security.core.userdetails
            .User(user.getUsername(), user.getPassword(), new ArrayList<>()));
    }
}

// Controller
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    private final JwtTokenProvider jwtTokenProvider;
    private final RefreshTokenService refreshTokenService;
    private final AuthenticationManager authenticationManager;
    
    @PostMapping("/login")
    public ResponseEntity<JwtResponse> login(@RequestBody LoginRequest request) {
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                request.getUsername(),
                request.getPassword()
            )
        );
        
        SecurityContextHolder.getContext().setAuthentication(authentication);
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        String accessToken = jwtTokenProvider.generateToken(userDetails);
        
        // Создаём refresh token
        User user = userRepository.findByUsername(request.getUsername())
            .orElseThrow();
        RefreshToken refreshToken = refreshTokenService.createRefreshToken(user);
        
        return ResponseEntity.ok(new JwtResponse(
            accessToken,
            refreshToken.getToken(),
            "Bearer"
        ));
    }
    
    @PostMapping("/refresh")
    public ResponseEntity<JwtResponse> refreshToken(@RequestBody RefreshTokenRequest request) {
        String accessToken = refreshTokenService.refreshAccessToken(request.getRefreshToken());
        return ResponseEntity.ok(new JwtResponse(
            accessToken,
            request.getRefreshToken(),
            "Bearer"
        ));
    }
}

3. JWT с ролями и разрешениями

public class JwtTokenProvider {
    public String generateToken(User user) {
        List<String> roles = user.getRoles().stream()
            .map(Role::getName)
            .collect(Collectors.toList());
        
        List<String> permissions = user.getRoles().stream()
            .flatMap(role -> role.getPermissions().stream())
            .map(Permission::getName)
            .collect(Collectors.toList());
        
        return Jwts.builder()
            .setSubject(user.getUsername())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + 86400000))
            .claim("roles", roles)
            .claim("permissions", permissions)
            .claim("userId", user.getId())
            .signWith(SignatureAlgorithm.HS512, secretKey)
            .compact();
    }
    
    public List<String> getRolesFromToken(String token) {
        return Jwts.parser()
            .setSigningKey(secretKey)
            .parseClaimsJws(token)
            .getBody()
            .get("roles", List.class);
    }
}

// Использование в контроллере
@RestController
@RequestMapping("/api/admin")
public class AdminController {
    @GetMapping("/users")
    @PreAuthorize("hasRole('ADMIN') and hasAuthority('USER_READ')")
    public ResponseEntity<List<UserDTO>> getAllUsers() {
        return ResponseEntity.ok(userService.getAllUsers());
    }
}

4. JWT с JJWT библиотекой

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

<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>
@Service
public class JwtTokenProvider {
    private final Key key;
    private final long jwtExpirationMs;
    
    public JwtTokenProvider(@Value("${jwt.secret}") String secret,
                           @Value("${jwt.expiration}") long jwtExpirationMs) {
        this.key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
        this.jwtExpirationMs = jwtExpirationMs;
    }
    
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("roles", userDetails.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.toList()));
        
        return Jwts.builder()
            .setClaims(claims)
            .setSubject(userDetails.getUsername())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
            .signWith(key, SignatureAlgorithm.HS512)
            .compact();
    }
    
    public String getUsernameFromToken(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(key)
            .build()
            .parseClaimsJws(token)
            .getBody()
            .getSubject();
    }
}

5. Двухфакторная аутентификация (2FA) с JWT

@Entity
public class User {
    private String username;
    private String password;
    private boolean twoFactorEnabled;
    private String twoFactorSecret;  // Google Authenticator secret
}

@Service
public class TwoFactorAuthService {
    public String generateSecret() {
        return Base32.random(32);  // Используй Google Authenticator формат
    }
    
    public boolean verifyTwoFactor(String secret, String code) {
        TimeBasedOneTimePasswordProvider totp = new TimeBasedOneTimePasswordProvider();
        return totp.verify(secret, Integer.parseInt(code));
    }
}

@RestController
@RequestMapping("/api/auth")
public class AuthController {
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        User user = userRepository.findByUsername(request.getUsername())
            .orElseThrow(() -> new BadCredentialsException("Invalid credentials"));
        
        if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
            throw new BadCredentialsException("Invalid credentials");
        }
        
        if (user.isTwoFactorEnabled()) {
            // Отправляем временный токен для 2FA
            String tempToken = jwtTokenProvider.generateTemporaryToken(user.getId());
            return ResponseEntity.ok(Map.of("tempToken", tempToken, "requires2FA", true));
        }
        
        // Или создаём полный JWT
        String token = jwtTokenProvider.generateToken(user);
        return ResponseEntity.ok(new JwtResponse(token));
    }
    
    @PostMapping("/verify-2fa")
    public ResponseEntity<JwtResponse> verify2FA(@RequestBody TwoFARequest request) {
        String tempToken = request.getTempToken();
        String code = request.getCode();
        
        Long userId = jwtTokenProvider.getUserIdFromTemporaryToken(tempToken);
        User user = userRepository.findById(userId).orElseThrow();
        
        if (!twoFactorAuthService.verifyTwoFactor(user.getTwoFactorSecret(), code)) {
            throw new RuntimeException("Invalid 2FA code");
        }
        
        String accessToken = jwtTokenProvider.generateToken(user);
        return ResponseEntity.ok(new JwtResponse(accessToken));
    }
}

6. JWT Revocation List (Blacklist)

Для отзыва токенов используй Redis:

@Service
public class TokenBlacklistService {
    private final RedisTemplate<String, String> redisTemplate;
    
    public void revokeToken(String token, long expirationTimeMs) {
        // Сохраняем отозванный токен с TTL равным сроку истечения
        redisTemplate.opsForValue().set(
            "blacklist:" + token,
            "revoked",
            Duration.ofMillis(expirationTimeMs)
        );
    }
    
    public boolean isTokenRevoked(String token) {
        return Boolean.TRUE.equals(
            redisTemplate.hasKey("blacklist:" + token)
        );
    }
}

@Service
public class JwtTokenProvider {
    private final TokenBlacklistService blacklistService;
    
    public boolean validateToken(String token) {
        // Проверяем blacklist
        if (blacklistService.isTokenRevoked(token)) {
            return false;
        }
        
        try {
            Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}

@RestController
@RequestMapping("/api/auth")
public class AuthController {
    @PostMapping("/logout")
    public ResponseEntity<Void> logout(@RequestHeader("Authorization") String authHeader) {
        String token = authHeader.substring(7);
        long expirationTimeMs = jwtTokenProvider.getExpirationTimeFromToken(token);
        tokenBlacklistService.revokeToken(token, expirationTimeMs);
        
        return ResponseEntity.ok().build();
    }
}

Best Practices для JWT

  • Используй HTTPS — JWT может быть перехвачен если передавать по HTTP
  • Короткие сроки действия для access token (15-30 минут)
  • Refresh token в httpOnly cookie — защита от XSS атак
  • Подписывай токены криптографическим ключом
  • Кодируй sensitive данные с помощью RS256 (RSA) для большей безопасности
  • Валидируй все claims на сервере
  • Использование aud (audience) claim для валидации намерения
  • Не храни чувствительные данные в JWT (он может быть декодирован)
  • Ротируй ключи подписи периодически
  • Тестируй истечение токена в unit тестах

В целом: JWT — это мощный инструмент для stateless аутентификации, но требует правильной реализации security best practices.

Какие знаешь способы реализации JWT аутентификации? | PrepBro