Spring BootSeniorCoding
Как реализовать JWT-аутентификацию в Spring Boot?
Реализуется через jjwt: JwtService генерирует/валидирует токен, OncePerRequestFilter извлекает его из заголовка Authorization и выставляет SecurityContext, SecurityFilterChain добавляет фильтр перед UsernamePasswordAuthenticationFilter с STATELESS-политикой.
Зависимости
Добавьте в pom.xml (Spring Boot 3.x):
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
Генерация и валидация токена
@Service
public class JwtService {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration-ms:3600000}")
private long expirationMs;
private SecretKey signingKey() {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
}
public String generateToken(UserDetails user) {
return Jwts.builder()
.subject(user.getUsername())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expirationMs))
.signWith(signingKey())
.compact();
}
public String extractUsername(String token) {
return Jwts.parser()
.verifyWith(signingKey())
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}
public boolean isTokenValid(String token, UserDetails user) {
return extractUsername(token).equals(user.getUsername())
&& !isExpired(token);
}
private boolean isExpired(String token) {
return Jwts.parser()
.verifyWith(signingKey())
.build()
.parseSignedClaims(token)
.getPayload()
.getExpiration()
.before(new Date());
}
}
OncePerRequestFilter
@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain)
throws ServletException, IOException {
String header = req.getHeader(HttpHeaders.AUTHORIZATION);
if (header == null || !header.startsWith("Bearer ")) {
chain.doFilter(req, res);
return;
}
String token = header.substring(7);
String username = jwtService.extractUsername(token);
if (username != null
&& SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails user = userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(token, user)) {
var auth = new UsernamePasswordAuthenticationToken(
user, null, user.getAuthorities());
auth.setDetails(new WebAuthenticationDetailsSource()
.buildDetails(req));
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
chain.doFilter(req, res);
}
}
SecurityFilterChain
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthFilter jwtAuthFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s ->
s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter,
UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public AuthenticationManager authManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Подводные камни
- Хранить
jwt.secretвapplication.propertiesв репозитории нельзя — используйте env-переменные или Vault. - Алгоритм HS256 с коротким секретом уязвим к брутфорсу; рекомендуется RS256 (пара ключей) в production.
- JWT без revocation: при logout или смене пароля старый токен остаётся валидным до
exp— нужен blocklist в Redis. SecurityContextHolderиспользует ThreadLocal — в реактивном стеке (WebFlux) нуженReactiveSecurityContextHolder.- Не проверять
expвручную:jjwtвыбрасываетExpiredJwtExceptionпри парсинге — достаточно поймать его в фильтре. - Access-токен должен быть коротким (15–60 мин); refresh-токен — длинным (7–30 дней), хранить в httpOnly cookie.
- Не передавать sensitive данные (пароль, номер карты) в payload — JWT только base64-encoded, не зашифрован.
Common mistakes
- Путать термин «jwt authentication» с соседним механизмом Spring Boot.
- Не называть границу lifecycle, transaction, thread или request для «jwt authentication».
- Игнорировать production-эффекты «jwt authentication»: latency, SQL shape, memory, security или observability.
What the interviewer is testing
- Попросить объяснить механизм «jwt authentication» на минимальном примере.
- Проверить, видит ли кандидат failure mode и диагностику для «jwt authentication».
- Уточнить, какие настройки или API меняют «jwt authentication» в реальном сервисе.