Spring Security ile JWT Authentication
JWT Nedir?
JSON Web Token (JWT), iki taraf arasında güvenli bilgi aktarımı sağlayan açık bir standarttır (RFC 7519). Bir JWT üç bölümden oluşur:
Header: Token tipi ve imzalama algoritması (HS256, RS256 vb.)
Payload: Kullanıcı bilgileri ve claim'ler (sub, exp, iat, roles vb.)
Signature: Header ve Payload'ın secret key ile imzalanmış hali
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. <-- Header
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ii... <-- Payload
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQ... <-- SignatureJWT'nin session-based authentication'a göre en büyük avantajı stateless olmasıdır. Sunucu tarafında session tutmanıza gerek yoktur. Token kendi içinde kullanıcı bilgilerini taşır. Bu özellik microservice mimarilerinde kritik öneme sahiptir.
Spring Security 6 Yapılandırması
Spring Boot 3 ile birlikte Spring Security 6 geldi ve yapılandırma şekli önemli ölçüde değişti. Artık WebSecurityConfigurerAdapter yok, yerine fonksiyonel SecurityFilterChain bean'i var.
Öncelikle gerekli dependency'leri ekleyelim:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
</dependencies>Security yapılandırmasını oluşturalım:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
public SecurityConfig(JwtAuthenticationFilter jwtAuthFilter,
AuthenticationProvider authenticationProvider) {
this.jwtAuthFilter = jwtAuthFilter;
this.authenticationProvider = authenticationProvider;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}Önemli: JWT kullanıyorsanız CSRF korumasını devre dışı bırakabilirsiniz. JWT zaten CSRF saldırılarına karşı doğal koruma sağlar çünkü token cookie'de değil, Authorization header'da taşınır.
JWT Token Üretme ve Doğrulama Servisi
Token üretme, doğrulama ve bilgi çıkarma işlemlerini merkezi bir servis üzerinden yönetelim:
@Service
public class JwtService {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.access-token-expiration}")
private long accessTokenExpiration;
@Value("${jwt.refresh-token-expiration}")
private long refreshTokenExpiration;
public String generateAccessToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails, accessTokenExpiration);
}
public String generateRefreshToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails, refreshTokenExpiration);
}
public String generateToken(Map<String, Object> extraClaims,
UserDetails userDetails,
long expiration) {
var authorities = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList();
return Jwts.builder()
.claims(extraClaims)
.subject(userDetails.getUsername())
.claim("roles", authorities)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey())
.compact();
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractClaim(token, Claims::getExpiration).before(new Date());
}
private <T> T extractClaim(String token, Function<Claims, T> resolver) {
final Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
return resolver.apply(claims);
}
private SecretKey getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}application.yml yapılandırması:
jwt:
secret: "bXktdmVyeS1sb25nLXNlY3JldC1rZXktZm9yLWp3dC0yNTYtYml0LW1pbmltdW0="
access-token-expiration: 900000 # 15 dakika
refresh-token-expiration: 604800000 # 7 günJWT Authentication Filter
Her HTTP isteğinde çalışan filter, Authorization header'dan token'ı çıkarır ve doğrular:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtService jwtService,
UserDetailsService userDetailsService) {
this.jwtService = jwtService;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
final String jwt = authHeader.substring(7);
try {
final String username = jwtService.extractUsername(jwt);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
} catch (JwtException e) {
// Token geçersiz — authentication olmadan devam et
}
filterChain.doFilter(request, response);
}
}Authentication Controller ve Refresh Token
Login, register ve token yenileme endpoint'leri:
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authManager;
private final JwtService jwtService;
private final UserService userService;
private final RefreshTokenService refreshTokenService;
// Constructor injection...
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
authManager.authenticate(
new UsernamePasswordAuthenticationToken(request.email(), request.password())
);
var user = userService.findByEmail(request.email());
String accessToken = jwtService.generateAccessToken(user);
String refreshToken = jwtService.generateRefreshToken(user);
// Refresh token'ı DB'ye kaydet
refreshTokenService.save(user, refreshToken);
return ResponseEntity.ok(new AuthResponse(accessToken, refreshToken));
}
@PostMapping("/register")
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
var user = userService.register(request);
String accessToken = jwtService.generateAccessToken(user);
String refreshToken = jwtService.generateRefreshToken(user);
refreshTokenService.save(user, refreshToken);
return ResponseEntity.status(HttpStatus.CREATED)
.body(new AuthResponse(accessToken, refreshToken));
}
@PostMapping("/refresh")
public ResponseEntity<AuthResponse> refresh(@RequestBody RefreshTokenRequest request) {
return refreshTokenService.findByToken(request.refreshToken())
.map(refreshTokenService::verifyExpiration)
.map(RefreshToken::getUser)
.map(user -> {
String newAccessToken = jwtService.generateAccessToken(user);
return ResponseEntity.ok(new AuthResponse(newAccessToken, request.refreshToken()));
})
.orElseThrow(() -> new UnauthorizedException("Gecersiz refresh token"));
}
@PostMapping("/logout")
public ResponseEntity<Void> logout(@RequestBody RefreshTokenRequest request) {
refreshTokenService.deleteByToken(request.refreshToken());
return ResponseEntity.noContent().build();
}
}Refresh Token Entity ve Servisi
Access token kısa ömürlü (15 dakika) olmalı, refresh token ise uzun ömürlü (7 gün). Refresh token'ları veritabanında saklayarak iptal edilebilir hale getirirsiniz:
@Entity
@Table(name = "refresh_tokens")
public class RefreshToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String token;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Column(nullable = false)
private Instant expiryDate;
// Getters, setters...
}@Service
public class RefreshTokenService {
private final RefreshTokenRepository repository;
@Value("${jwt.refresh-token-expiration}")
private long refreshTokenExpiration;
public RefreshToken save(UserDetails user, String token) {
// Eski refresh token'ları sil
repository.deleteByUser((User) user);
RefreshToken refreshToken = new RefreshToken();
refreshToken.setUser((User) user);
refreshToken.setToken(token);
refreshToken.setExpiryDate(Instant.now().plusMillis(refreshTokenExpiration));
return repository.save(refreshToken);
}
public RefreshToken verifyExpiration(RefreshToken token) {
if (token.getExpiryDate().isBefore(Instant.now())) {
repository.delete(token);
throw new UnauthorizedException("Refresh token suresi dolmus. Lutfen tekrar giris yapin.");
}
return token;
}
public Optional<RefreshToken> findByToken(String token) {
return repository.findByToken(token);
}
public void deleteByToken(String token) {
repository.deleteByToken(token);
}
}Role-Based Authorization
Kullanıcı rollerini JWT payload'ına ekleyip, method seviyesinde yetkilendirme yapabilirsiniz:
// Method seviyesinde role kontrolü
@RestController
@RequestMapping("/api/admin")
public class AdminController {
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/users")
public ResponseEntity<List<UserDto>> getAllUsers() {
return ResponseEntity.ok(userService.findAll());
}
@PreAuthorize("hasAnyRole('ADMIN', 'MODERATOR')")
@DeleteMapping("/posts/{id}")
public ResponseEntity<Void> deletePost(@PathVariable Long id) {
postService.delete(id);
return ResponseEntity.noContent().build();
}
// Custom SpEL expression
@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
@GetMapping("/users/{userId}/profile")
public ResponseEntity<UserProfile> getUserProfile(@PathVariable Long userId) {
return ResponseEntity.ok(userService.getProfile(userId));
}
}Best Practices
Secret key'i güçlü tutun: En az 256-bit (HS256 için). Environment variable olarak saklayın, kodda hardcode etmeyin.
Access token süresini kısa tutun: 15-30 dakika yeterlidir. Uzun süreli oturumlar için refresh token kullanın.
Token'ı localStorage yerine httpOnly cookie'de saklamayın — veya saklayın ama XSS riskini bilin. En güvenli yaklaşım: access token memory'de, refresh token httpOnly cookie'de.
Token blacklist: Logout işleminde access token'ı bir blacklist'e (Redis) ekleyerek hemen geçersiz kılabilirsiniz.
Rate limiting: Login endpoint'ine mutlaka rate limiting ekleyin. Brute force saldırılarına karşı koruma sağlar.
Özet
JWT stateless authentication sağlar — microservice'ler için ideal
Spring Security 6 ile
SecurityFilterChainbean'i kullanarak yapılandırma yapınJwtService ile token üretme, doğrulama ve claim çıkarma işlemlerini merkezileştirin
Refresh token mekanizması ile kısa ömürlü access token + uzun ömürlü refresh token stratejisi uygulayın
Role-based authorization için
@PreAuthorizeannotation'ını kullanınSecret key'i güçlü tutun, access token süresini kısa tutun, rate limiting uygulayın
Bu yazıyı beğendiniz mi?
Bültene abone olun ve yeni yazılardan ilk siz haberdar olun. Spam yok, söz.
Bu konuyu derinlemesine öğrenmek ister misin?
Spring Boot: Sıfırdan İleri Seviyeye
İlgili Yazılar
Spring Boot'ta Exception Handling: @ControllerAdvice ile Profesyonel Hata Yönetimi
Spring Boot uygulamalarında hata yönetimini profesyonel seviyeye taşıyın. @ControllerAdvice, @ExceptionHandler, custom e...
Spring Boot Nedir? Neden Spring Boot Kullanmalısınız?
Spring Boot nedir, ne işe yarar? Auto-configuration, starter dependencies, embedded server özellikleri. Java backend gel...
Spring Boot'ta Custom Annotation Yazma
Spring Boot'ta kendi annotation'ınızı yazın: meta-annotation, AOP ile cross-cutting concerns, validation ve composed ann...