JWT Authentication Filter
JWT tabanlı authentication'da her HTTP isteği Authorization header'ında token taşır. Spring Security'nin filter chain'ine özel bir filter ekleyerek bu token'ı yakalayıp, doğrulayıp, SecurityContext'e yerleştirmemiz gerekir. Bu filter olmazsa Spring Security gelen kullanıcıyı tanıyamaz.
İstek Akışı (Request Flow)
HTTP İsteği
│
▼
┌─────────────────────────────┐
│ JwtAuthenticationFilter │ ← Bizim yazdığımız filter
│ 1. Authorization header al │
│ 2. "Bearer " prefix kontrol│
│ 3. Token'ı parse et │
│ 4. Username'i çıkar │
│ 5. UserDetails'ı yükle │
│ 6. Token'ı doğrula │
│ 7. SecurityContext'e koy │
└─────────────┬───────────────┘
│
▼
┌─────────────────────────────┐
│ UsernamePasswordAuth Filter │ ← Spring'in kendi filter'ı (form login)
│ (JWT modda devre dışı) │
└─────────────┬───────────────┘
│
▼
┌─────────────────────────────┐
│ AuthorizationFilter │ ← URL ve method yetkilendirme
│ (hasRole, hasAuthority) │
└─────────────┬───────────────┘
│
▼
Controller / ServiceOncePerRequestFilter
OncePerRequestFilter, her HTTP isteği için tam olarak bir kez çalışmayı garanti eder. Forward veya internal dispatch durumlarında bile tekrar çalışmaz — JWT doğrulaması için idealdir.
@Component
@RequiredArgsConstructor // Lombok — final alanlar için constructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
// 1. Authorization header'ını al
final String authHeader = request.getHeader("Authorization");
// Header yoksa veya "Bearer " ile başlamıyorsa → bu istek JWT taşımıyor
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
// 2. "Bearer " prefix'ini çıkar → saf token
final String jwt = authHeader.substring(7);
try {
// 3. Token'dan username'i çıkar (aynı zamanda signature doğrulanır)
final String username = jwtService.extractUsername(jwt);
// 4. Username varsa VE SecurityContext'te henüz authentication yoksa
if (username != null &&
SecurityContextHolder.getContext().getAuthentication() == null) {
// 5. Veritabanından UserDetails'ı yükle
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 6. Token geçerliliğini doğrula (username eşleşmesi + expiry)
if (jwtService.isTokenValid(jwt, userDetails)) {
// 7. Authentication token oluştur
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, // principal
null, // credentials (JWT'de gereksiz)
userDetails.getAuthorities() // roller/yetkiler
);
// İstek detaylarını ekle (IP, session ID vb.)
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
// 8. SecurityContext'e yerleştir — artık kullanıcı "authenticated"
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
} catch (ExpiredJwtException e) {
// Token süresi dolmuş — 401 dönecek (authentication yok)
log.debug("Expired JWT for request: {}", request.getRequestURI());
} catch (JwtException e) {
// Malformed, invalid signature vb.
log.warn("Invalid JWT: {}", e.getMessage());
}
// 9. Her durumda filter chain'e devam et
// Authentication yoksa → Spring Security 403/401 döner
filterChain.doFilter(request, response);
}
// Bazı path'ler için filter'ı atla (opsiyonel optimizasyon)
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getServletPath();
return path.startsWith("/api/auth/")
|| path.startsWith("/public/")
|| path.startsWith("/actuator/health");
}
}Neden SecurityContextHolder.getContext().getAuthentication() == null Kontrolü?
Eğer SecurityContext'te zaten bir authentication varsa (başka bir filter veya mekanizma set etmişse), JWT filter tekrar authenticate etmeye çalışmamalı. Bu kontrol:
Gereksiz veritabanı sorgusunu önler (
loadUserByUsername)Mevcut authentication'ı korur
Filter chain'deki diğer authentication mekanizmalarıyla çakışmayı engeller
AuthController — Login ve Register
JWT filter token'ı doğrular ama token'ı oluşturmaz. Token oluşturma işi AuthController'dadır:
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
private final UserService userService;
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
// 1. Spring Security'nin AuthenticationManager'ı ile doğrula
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getEmail(),
request.getPassword()
)
);
// authenticate() başarısız olursa BadCredentialsException fırlatır
// 2. UserDetails'ı yükle
UserDetails userDetails = userDetailsService
.loadUserByUsername(request.getEmail());
// 3. Token'ları oluştur
String accessToken = jwtService.generateAccessToken(userDetails);
String refreshToken = jwtService.generateRefreshToken(userDetails);
return ResponseEntity.ok(new AuthResponse(accessToken, refreshToken));
}
@PostMapping("/register")
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
// 1. Kullanıcıyı oluştur
User user = userService.createUser(request);
// 2. UserDetails'ı oluştur
UserDetails userDetails = userDetailsService
.loadUserByUsername(user.getEmail());
// 3. Token'ları oluştur
String accessToken = jwtService.generateAccessToken(userDetails);
String refreshToken = jwtService.generateRefreshToken(userDetails);
return ResponseEntity.status(HttpStatus.CREATED)
.body(new AuthResponse(accessToken, refreshToken));
}
}
// Request/Response DTO'ları
public record LoginRequest(
@NotBlank @Email String email,
@NotBlank String password
) {}
public record RegisterRequest(
@NotBlank String name,
@NotBlank @Email String email,
@NotBlank @Size(min = 8) String password
) {}
public record AuthResponse(
String accessToken,
String refreshToken,
String tokenType, // "Bearer"
long expiresIn // saniye cinsinden
) {
public AuthResponse(String accessToken, String refreshToken) {
this(accessToken, refreshToken, "Bearer", 900); // 15 dk
}
}SecurityFilterChain — Tam Konfigürasyon
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
// CSRF: REST API stateless — cookie kullanmıyoruz, CSRF gereksiz
.csrf(csrf -> csrf.disable())
// CORS: Frontend farklı origin'den geliyorsa
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// Session: STATELESS — Spring Security session oluşturmaz
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// URL bazlı yetkilendirme
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // Login/Register açık
.requestMatchers("/public/**").permitAll() // Public içerik
.requestMatchers("/actuator/health").permitAll() // Health check
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
// AuthenticationProvider (DaoAuthenticationProvider)
.authenticationProvider(authenticationProvider)
// JWT filter'ı UsernamePasswordAuthenticationFilter'dan ÖNCE ekle
.addFilterBefore(jwtAuthFilter,
UsernamePasswordAuthenticationFilter.class)
// Exception handling — özel 401/403 response'ları
.exceptionHandling(ex -> ex
.authenticationEntryPoint(jwtAuthEntryPoint())
.accessDeniedHandler(jwtAccessDeniedHandler())
)
.build();
}
// AuthenticationManager bean (AuthController'da kullanılır)
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
// AuthenticationProvider (UserDetailsService + PasswordEncoder)
@Bean
public AuthenticationProvider authenticationProvider(
UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return provider;
}
// CORS konfigürasyonu
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
}Custom Exception Handling — 401 ve 403 Response
// 401 Unauthorized — token yoksa veya geçersizse
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("""
{"error": "Unauthorized", "message": "Geçerli bir token gereklidir", "status": 401}
""");
}
}
// 403 Forbidden — token geçerli ama yetki yetersiz
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("""
{"error": "Forbidden", "message": "Bu işlem için yetkiniz yok", "status": 403}
""");
}
}Tam Akış Özeti
1. Client → POST /api/auth/login {email, password}
2. AuthController → AuthenticationManager.authenticate()
3. DaoAuthenticationProvider → UserDetailsService.loadUserByUsername()
4. Password doğrulanır → JwtService.generateAccessToken()
5. Client ← {accessToken, refreshToken, expiresIn}
--- Sonraki istekler ---
6. Client → GET /api/users [Authorization: Bearer eyJhbG...]
7. JwtAuthenticationFilter → header'dan token çıkar
8. JwtService.extractUsername() → signature doğrulanır
9. UserDetailsService.loadUserByUsername() → kullanıcı yüklenir
10. JwtService.isTokenValid() → expiry + username eşleşmesi
11. SecurityContext'e Authentication set edilir
12. AuthorizationFilter → hasRole/hasAuthority kontrol
13. Controller çalışır → Response döner💡 Özet:
OncePerRequestFilterextend edilerek JWT filter yazılır. Her istekte Authorization header kontrol edilir, token doğrulanır ve SecurityContext'e authentication nesnesi yerleştirilir. Session STATELESS olmalı. AuthController login/register endpoint'lerini sağlar. Custom EntryPoint ve AccessDeniedHandler ile 401/403 response'ları özelleştirilir.
AI Asistan
Sorularını yanıtlamaya hazır