← Назад к вопросам
Какие знаешь способы реализации 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.