İçeriğe geç

Spring Security ile JWT Authentication

T
Tolgahan
· · 15 dk okuma · 918 görüntülenme

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... <-- Signature

JWT'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ün

JWT 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

  1. Secret key'i güçlü tutun: En az 256-bit (HS256 için). Environment variable olarak saklayın, kodda hardcode etmeyin.

  2. Access token süresini kısa tutun: 15-30 dakika yeterlidir. Uzun süreli oturumlar için refresh token kullanın.

  3. 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.

  4. Token blacklist: Logout işleminde access token'ı bir blacklist'e (Redis) ekleyerek hemen geçersiz kılabilirsiniz.

  5. 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 SecurityFilterChain bean'i kullanarak yapılandırma yapın

  • JwtService 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 @PreAuthorize annotation'ını kullanın

  • Secret key'i güçlü tutun, access token süresini kısa tutun, rate limiting uygulayın

Paylaş:
Son güncelleme: Jul 19, 2026

Yorumlar

Giriş yapın ve yorum bırakın.

Henüz yorum yok

Düşüncelerinizi paylaşan ilk siz olun!

Bu yazıyı beğendiniz mi?

Bültene abone olun ve yeni yazılardan ilk siz haberdar olun. Spam yok, söz.

İlgili Yazılar