← Kursa Dön
📄 Text · 15 min

API Security Checklist

Giriş

Güvenlik, uygulamanızın kapısındaki kilit gibidir. En güzel evi yapabilirsiniz ama kapıyı açık bırakırsanız herkes girip çıkar. API güvenliği de tam olarak budur — uygulamanız dışarıya ne kadar açık, saldırganlar ne kadar içeri girebilir?

2023 yılında OWASP'ın raporuna göre, web uygulamalarına yapılan saldırıların %90'ından fazlası bilinen ve önlenebilir güvenlik açıklarından kaynaklanıyor. SQL Injection, kimlik doğrulama zafiyetleri, yetersiz yetkilendirme — bunların hepsi "biliyorduk ama yapmadık" kategorisindeki sorunlar.

Spring Boot ve Spring Security ekosistemi, bu tehditlerin çoğuna karşı güçlü savunma mekanizmaları sunar. Ancak bu araçları doğru kullanmak geliştiricinin sorumluluğundadır. Bu derste katmanlı güvenlik (defense in depth) yaklaşımıyla, bir Spring Boot API'sini production-ready güvenlik seviyesine nasıl getireceğimizi adım adım inceleyeceğiz.

1. Input Validation — Girdi Doğrulama

Her güvenlik stratejisinin ilk katmanı: dışarıdan gelen her veriye şüpheyle yaklaşmak. SQL injection, XSS ve command injection gibi saldırıların tamamı, doğrulanmamış kullanıcı girdisinden kaynaklanır.

Bean Validation

// Her endpoint'te @Valid kullanın — validasyonsuz endpoint olmamalı
@RestController
@RequestMapping("/api/v1/users")
@Validated  // Path/Query param validasyonu için de gerekli
public class UserController {

    @PostMapping
    public ResponseEntity<UserResponse> createUser(
            @Valid @RequestBody CreateUserRequest request) {
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(userService.createUser(request));
    }

    @GetMapping
    public ResponseEntity<Page<UserResponse>> searchUsers(
            @RequestParam @Size(min = 1, max = 100) String query,
            @RequestParam @Min(0) int page,
            @RequestParam @Min(1) @Max(100) int size) {
        return ResponseEntity.ok(
            userService.search(query, page, size));
    }
}
public record CreateUserRequest(
    @NotBlank(message = "Kullanıcı adı boş olamaz")
    @Size(min = 3, max = 50, message = "Kullanıcı adı 3-50 karakter olmalı")
    @Pattern(regexp = "^[a-zA-Z0-9_-]+$",
             message = "Kullanıcı adı sadece harf, rakam, _ ve - içerebilir")
    String username,

    @NotBlank(message = "E-posta boş olamaz")
    @Email(message = "Geçerli bir e-posta adresi giriniz")
    String email,

    @NotBlank(message = "Şifre boş olamaz")
    @Size(min = 8, max = 128, message = "Şifre 8-128 karakter olmalı")
    @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&]).*$",
             message = "Şifre en az bir büyük harf, küçük harf, rakam ve özel karakter içermelidir")
    String password,

    @Size(max = 500, message = "Bio en fazla 500 karakter olabilir")
    String bio,

    @Past(message = "Doğum tarihi geçmişte olmalı")
    LocalDate birthDate
) {}

Custom Validator — Güvenli Metin

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SafeTextValidator.class)
public @interface SafeText {
    String message() default "Metin güvenli olmayan karakterler içeriyor";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

public class SafeTextValidator implements ConstraintValidator<SafeText, String> {

    // SQL Injection ve XSS pattern'leri
    private static final Pattern DANGEROUS_PATTERN = Pattern.compile(
        "(?i)((<script|javascript:|on\\w+=)|" +                    // XSS
        "(union\\s+select|drop\\s+table|insert\\s+into|" +        // SQL Injection
        "delete\\s+from|update\\s+set|exec\\s*\\()|" +
        "(\\$\\{|\\$\\(|`|\\|\\||&&))"                             // Command Injection
    );

    @Override
    public boolean isValid(String value, ConstraintValidatorContext ctx) {
        if (value == null) return true;
        return !DANGEROUS_PATTERN.matcher(value).find();
    }
}
public record SearchRequest(
    @SafeText
    @Size(max = 200)
    String query,

    @SafeText
    @Size(max = 50)
    String category
) {}

⚠️ Dikkat: Input validation tek başına SQL injection'ı engellemez. JPA/Hibernate parameterized query kullanır ve bu zaten SQL injection'a karşı temel savunmadır. Ancak doğrudan SQL yazdığınız yerlerde (native query, JDBC) mutlaka parameterized query kullanın, string concatenation ile SQL OLUŞTURMAYIN.

// ❌ YANLIŞ — SQL Injection açığı
@Query(value = "SELECT * FROM users WHERE name = '" + name + "'",
       nativeQuery = true)

// ✅ DOĞRU — Parameterized query
@Query(value = "SELECT * FROM users WHERE name = :name",
       nativeQuery = true)
List<User> findByName(@Param("name") String name);

2. Authentication — Kimlik Doğrulama

Kimlik doğrulama, "Sen kimsin?" sorusunun cevabıdır. Modern API'lerde en yaygın yöntem JWT (JSON Web Token) ve OAuth2'dir.

JWT Authentication

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtFilter;
    private final AuthenticationProvider authProvider;

    @Bean
    public SecurityFilterChain securityFilterChain(
            HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())  // Stateless API'de CSRF gereksiz
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                // Public endpoint'ler
                .requestMatchers("/api/v1/auth/**").permitAll()
                .requestMatchers("/actuator/health").permitAll()
                .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
                // Admin endpoint'leri
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                // Diğer tüm istekler authenticate olmalı
                .anyRequest().authenticated()
            )
            .authenticationProvider(authProvider)
            .addFilterBefore(jwtFilter,
                UsernamePasswordAuthenticationFilter.class)
            .build();
    }
}

JWT Token Üretimi

@Service
@RequiredArgsConstructor
public class JwtService {

    @Value("${app.jwt.secret}")
    private String secret;

    @Value("${app.jwt.access-token-expiration:900}")  // 15 dakika
    private long accessTokenExpiration;

    @Value("${app.jwt.refresh-token-expiration:604800}")  // 7 gün
    private long refreshTokenExpiration;

    public String generateAccessToken(UserDetails user) {
        return buildToken(user, accessTokenExpiration, Map.of(
            "type", "access",
            "roles", user.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .toList()
        ));
    }

    public String generateRefreshToken(UserDetails user) {
        return buildToken(user, refreshTokenExpiration, Map.of(
            "type", "refresh"
        ));
    }

    private String buildToken(UserDetails user, long expirationSeconds,
                              Map<String, Object> extraClaims) {
        Instant now = Instant.now();
        return Jwts.builder()
            .subject(user.getUsername())
            .issuedAt(Date.from(now))
            .expiration(Date.from(now.plusSeconds(expirationSeconds)))
            .claims(extraClaims)
            .signWith(getSigningKey(), Jwts.SIG.HS256)
            .compact();
    }

    private SecretKey getSigningKey() {
        byte[] keyBytes = Decoders.BASE64.decode(secret);
        return Keys.hmacShaKeyFor(keyBytes);
    }

    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public boolean isTokenValid(String token, UserDetails userDetails) {
        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) {
        Claims claims = Jwts.parser()
            .verifyWith(getSigningKey())
            .build()
            .parseSignedClaims(token)
            .getPayload();
        return resolver.apply(claims);
    }
}

JWT Best Practices

app:
  jwt:
    secret: ${JWT_SECRET}  # En az 256-bit, environment variable'dan
    access-token-expiration: 900       # 15 dakika — kısa tutun
    refresh-token-expiration: 604800   # 7 gün
  • Access token kısa ömürlü (15-60 dakika): Çalınsa bile kısa sürede geçersiz olur

  • Refresh token rotation: Her kullanımda yeni refresh token ver, eskisini geçersiz kıl

  • Token'da hassas bilgi yok: JWT payload Base64 encoded, şifrelenmemiştir — herkes okuyabilir

  • Blacklist mekanizması: Çıkış yapan veya şüpheli token'ları Redis'te blacklist'e alın

3. Authorization — Yetkilendirme

Yetkilendirme, "Ne yapabilirsin?" sorusunun cevabıdır. Kimliği doğrulanmış bir kullanıcının hangi kaynaklara erişebileceğini belirler.

URL-Level Authorization

// SecurityFilterChain'de URL bazlı yetkilendirme
.authorizeHttpRequests(auth -> auth
    .requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
    .requestMatchers(HttpMethod.POST, "/api/v1/products/**").hasRole("ADMIN")
    .requestMatchers(HttpMethod.DELETE, "/api/v1/products/**").hasRole("ADMIN")
    .requestMatchers("/api/v1/users/**").hasAnyRole("USER", "ADMIN")
    .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
    .requestMatchers("/api/v1/reports/**").hasAuthority("REPORT_VIEW")
)

Method-Level Authorization

@EnableMethodSecurity  // Ana sınıfa ekleyin
@SpringBootApplication
public class MyApp {}

@Service
public class OrderService {

    // Sadece ADMIN silebilir
    @PreAuthorize("hasRole('ADMIN')")
    public void deleteOrder(Long id) { ... }

    // Siparişin sahibi veya ADMIN erişebilir
    @PreAuthorize("hasRole('ADMIN') or @orderSecurity.isOwner(#id)")
    public OrderResponse getOrder(Long id) { ... }

    // Dönüş değerini filtrele — sadece kendi siparişlerini görsün
    @PostAuthorize("returnObject.userId == authentication.principal.id " +
                   "or hasRole('ADMIN')")
    public OrderResponse getOrderDetails(Long id) { ... }

    // Koleksiyon filtreleme
    @PostFilter("filterObject.userId == authentication.principal.id " +
                "or hasRole('ADMIN')")
    public List<OrderResponse> getAllOrders() { ... }
}

IDOR (Insecure Direct Object Reference) Koruması

IDOR, en yaygın yetkilendirme açıklarından biridir. Kullanıcı, URL'deki ID'yi değiştirerek başkasının verisine erişir:

GET /api/v1/orders/42   → Kendi siparişi ✅
GET /api/v1/orders/43   → Başkasının siparişi ❌ (IDOR açığı!)
@Component("orderSecurity")
@RequiredArgsConstructor
public class OrderSecurityEvaluator {

    private final OrderRepository orderRepository;

    public boolean isOwner(Long orderId) {
        Authentication auth = SecurityContextHolder.getContext()
            .getAuthentication();
        Long currentUserId = ((UserPrincipal) auth.getPrincipal()).getId();

        return orderRepository.findById(orderId)
            .map(order -> order.getUserId().equals(currentUserId))
            .orElse(false);
    }
}

@Service
public class OrderService {

    @PreAuthorize("hasRole('ADMIN') or @orderSecurity.isOwner(#id)")
    public OrderResponse getOrder(Long id) {
        return orderRepository.findById(id)
            .map(orderMapper::toResponse)
            .orElseThrow(() -> ResourceNotFoundException.order(id));
    }
}

4. Rate Limiting — İstek Sınırlama

Brute-force saldırıları, DDoS ve API abuse'u engellemek için istek sayısını sınırlayın:

// Bucket4j ile Rate Limiting
@Configuration
public class RateLimitConfig {

    @Bean
    public FilterRegistrationBean<RateLimitFilter> rateLimitFilter() {
        FilterRegistrationBean<RateLimitFilter> registrationBean =
            new FilterRegistrationBean<>();
        registrationBean.setFilter(new RateLimitFilter());
        registrationBean.addUrlPatterns("/api/*");
        registrationBean.setOrder(1);
        return registrationBean;
    }
}

@Component
public class RateLimitFilter extends OncePerRequestFilter {

    private final Map<String, Bucket> buckets =
        new ConcurrentHashMap<>();

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {

        String key = resolveKey(request);
        Bucket bucket = buckets.computeIfAbsent(key,
            k -> createBucket(request));

        if (bucket.tryConsume(1)) {
            // Rate limit headers ekle
            response.addHeader("X-Rate-Limit-Remaining",
                String.valueOf(bucket.getAvailableTokens()));
            filterChain.doFilter(request, response);
        } else {
            response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            response.setContentType("application/json");
            response.getWriter().write("""
                {
                    "code": "RATE_LIMIT_EXCEEDED",
                    "message": "Çok fazla istek gönderdiniz. Lütfen bekleyin.",
                    "retryAfter": 60
                }
                """);
        }
    }

    private Bucket createBucket(HttpServletRequest request) {
        // Login endpoint'i daha sıkı sınırla (brute-force koruması)
        if (request.getRequestURI().contains("/auth/login")) {
            return Bucket.builder()
                .addLimit(Bandwidth.simple(5, Duration.ofMinutes(1)))  // 5/dk
                .addLimit(Bandwidth.simple(20, Duration.ofHours(1)))   // 20/saat
                .build();
        }

        // Genel API limiti
        return Bucket.builder()
            .addLimit(Bandwidth.simple(100, Duration.ofMinutes(1)))
            .build();
    }

    private String resolveKey(HttpServletRequest request) {
        // Authenticated ise user ID, değilse IP
        Authentication auth = SecurityContextHolder.getContext()
            .getAuthentication();
        if (auth != null && auth.isAuthenticated()) {
            return "user:" + auth.getName();
        }
        return "ip:" + request.getRemoteAddr();
    }
}

5. HTTPS ve Security Headers

HTTPS Zorlaması

# application-prod.yml
server:
  ssl:
    enabled: true
    key-store: classpath:keystore.p12
    key-store-password: ${SSL_KEYSTORE_PASSWORD}
    key-store-type: PKCS12
  port: 443

# HTTP → HTTPS yönlendirme
spring:
  web:
    resources:
      add-mappings: false

Security Headers

@Configuration
public class SecurityHeadersConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(
            HttpSecurity http) throws Exception {
        return http
            .headers(headers -> headers
                // XSS koruması
                .xssProtection(xss -> xss
                    .headerValue(XXssProtectionHeaderWriter
                        .HeaderValue.ENABLED_MODE_BLOCK))
                // Content-Type sniffing engelleme
                .contentTypeOptions(Customizer.withDefaults())
                // Clickjacking koruması
                .frameOptions(frame -> frame.deny())
                // HSTS — tarayıcıyı HTTPS'e zorla
                .httpStrictTransportSecurity(hsts -> hsts
                    .includeSubDomains(true)
                    .maxAgeInSeconds(31536000))  // 1 yıl
                // Content Security Policy
                .contentSecurityPolicy(csp -> csp
                    .policyDirectives("default-src 'self'; " +
                        "script-src 'self'; " +
                        "style-src 'self' 'unsafe-inline'; " +
                        "img-src 'self' data:;"))
                // Referrer Policy
                .referrerPolicy(referrer -> referrer
                    .policy(ReferrerPolicyHeaderWriter
                        .ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN))
                // Permissions Policy
                .permissionsPolicy(permissions -> permissions
                    .policy("camera=(), microphone=(), geolocation=()"))
            )
            // ... diğer konfigürasyonlar
            .build();
    }
}

6. CORS Yapılandırması

@Configuration
public class CorsConfig {

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();

        // ❌ YANLIŞ — her şeye izin vermek
        // config.addAllowedOrigin("*");

        // ✅ DOĞRU — sadece bilinen origin'lere izin
        config.setAllowedOrigins(List.of(
            "https://myapp.com",
            "https://admin.myapp.com"
        ));
        config.setAllowedMethods(List.of(
            "GET", "POST", "PUT", "DELETE", "PATCH"));
        config.setAllowedHeaders(List.of(
            "Authorization", "Content-Type", "X-Request-Id"));
        config.setExposedHeaders(List.of(
            "X-Rate-Limit-Remaining", "X-Total-Count"));
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);  // Preflight cache: 1 saat

        UrlBasedCorsConfigurationSource source =
            new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", config);
        return source;
    }
}

⚠️ Dikkat: allowedOrigins("*") ile allowCredentials(true) birlikte kullanılamaz. Bu güvenlik açığı oluşturur. Spesifik origin'ler belirtin.

7. CSRF Koruması

// Stateless (JWT) API'de CSRF kapatılır
.csrf(csrf -> csrf.disable())

// Session-based uygulamada CSRF aktif olmalı
.csrf(csrf -> csrf
    .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
    .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()))

Neden stateless API'de CSRF kapatılır? CSRF saldırısı, tarayıcının otomatik olarak cookie gönderme özelliğini kötüye kullanır. JWT token, Authorization header'ında gönderilir ve tarayıcı bunu otomatik göndermez — dolayısıyla CSRF riski yoktur.

8. Data Protection — Veri Koruma

Response'da Gereksiz Veri Döndürmeyin (DTO Pattern)

// ❌ KÖTÜ — Entity doğrudan döndürme
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
    return userRepository.findById(id).orElseThrow();
    // password hash, internal notes, createdBy vs. HEPSİ gider!
}

// ✅ DOĞRU — DTO ile sadece gerekli alanlar
@GetMapping("/users/{id}")
public UserResponse getUser(@PathVariable Long id) {
    User user = userRepository.findById(id).orElseThrow();
    return new UserResponse(
        user.getId(),
        user.getUsername(),
        user.getEmail(),
        user.getCreatedAt()
        // password, internalNotes YOK
    );
}

Sensitive Data Maskeleme (Loglarda)

// ❌ KÖTÜ — hassas bilgi log'a yazılıyor
log.info("User login: email={}, password={}",
    request.email(), request.password());

// ✅ DOĞRU — hassas bilgi maskeleniyor
log.info("User login attempt: email={}",
    maskEmail(request.email()));

private String maskEmail(String email) {
    int atIndex = email.indexOf('@');
    if (atIndex <= 2) return "***@" + email.substring(atIndex + 1);
    return email.substring(0, 2) + "***" + email.substring(atIndex);
}

Password Hashing

@Configuration
public class PasswordConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        // BCrypt — adaptif, salt otomatik, güvenli
        return new BCryptPasswordEncoder(12);  // strength: 12 rounds
    }
}

// Kullanım
String hashed = passwordEncoder.encode(rawPassword);
boolean matches = passwordEncoder.matches(rawPassword, hashed);

// ASLA:
// - MD5 veya SHA-1 kullanmayın (kırılmış)
// - Salt'sız hash yapmayın
// - Şifreyi plain text saklamayın

9. Dependency Scanning — Bağımlılık Güvenliği

Projenizin bağımlılıkları bilinen güvenlik açıkları (CVE) içerebilir:

<!-- OWASP Dependency-Check Maven Plugin -->
<plugin>
    <groupId>org.owasp</groupId>
    <artifactId>dependency-check-maven</artifactId>
    <version>9.0.7</version>
    <configuration>
        <failBuildOnCVSS>7</failBuildOnCVSS>  <!-- CVSS 7+ → build fail -->
    </configuration>
</plugin>
# CI/CD'de çalıştırın
./mvnw dependency-check:check

# Snyk alternatifi
snyk test --all-projects

10. Audit Logging — Denetim Günlüğü

Kim ne yaptı, ne zaman yaptı — kritik operasyonları kayıt altına alın:

@Aspect
@Component
@Slf4j
public class AuditLogAspect {

    @Around("@annotation(Audited)")
    public Object audit(ProceedingJoinPoint joinPoint) throws Throwable {
        Authentication auth = SecurityContextHolder.getContext()
            .getAuthentication();
        String user = auth != null ? auth.getName() : "anonymous";
        String method = joinPoint.getSignature().toShortString();

        log.info("AUDIT | user={} | action={} | args={}",
            user, method, sanitizeArgs(joinPoint.getArgs()));

        Object result = joinPoint.proceed();

        log.info("AUDIT | user={} | action={} | result=SUCCESS",
            user, method);

        return result;
    }
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audited {}

// Kullanım
@Service
public class UserService {

    @Audited
    @PreAuthorize("hasRole('ADMIN')")
    public void deleteUser(Long userId) { ... }

    @Audited
    public void changePassword(Long userId, String newPassword) { ... }
}

Güvenlik Kontrol Listesi

Pre-Development

  • ☐ HTTPS everywhere (HTTP istekleri redirect)

  • ☐ Spring Security dependency ekli

  • ☐ Security configuration sınıfı oluşturuldu

  • ☐ CORS spesifik origin'lerle yapılandırıldı

Authentication

  • ☐ JWT access token kısa ömürlü (15-60 dk)

  • ☐ Refresh token rotation uygulandı

  • ☐ Password BCrypt ile hash'leniyor (strength ≥ 10)

  • ☐ Login endpoint'te rate limiting aktif (5/dk)

  • ☐ Failed login attempt sayacı ve lockout mekanizması

Authorization

  • ☐ URL-level authorization (SecurityFilterChain)

  • ☐ Method-level authorization (@PreAuthorize, @PostAuthorize)

  • ☐ IDOR koruması (object-level authorization)

  • ☐ Admin endpoint'ler ayrı route prefix'te (/api/v1/admin/)

Input/Output

  • ☐ Tüm endpoint'lerde @Valid

  • ☐ DTO pattern — entity'ler doğrudan dönmüyor

  • ☐ Parameterized query (native SQL'de)

  • ☐ Response'da hassas bilgi yok (password, internal fields)

Infrastructure

  • ☐ Security headers (HSTS, CSP, X-Frame-Options)

  • ☐ OWASP Dependency-Check CI/CD'de çalışıyor

  • ☐ Actuator endpoint'leri kısıtlı

  • ☐ Secret'lar environment variable veya Vault'ta

  • ☐ Audit logging aktif

Monitoring

  • ☐ Failed authentication girişimleri loglanıyor

  • ☐ Rate limit aşımları izleniyor

  • ☐ 401/403 hataları alert oluşturuyor

  • ☐ Dependency vulnerability tarama scheduled

Actuator Güvenliği

Spring Boot Actuator, production'da büyük güvenlik riski oluşturabilir:

# ❌ KÖTÜ — her şey açık
management:
  endpoints:
    web:
      exposure:
        include: "*"

# ✅ DOĞRU — sadece gerekli olanlar
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
      base-path: /internal/actuator  # Standart path'i değiştirin
  endpoint:
    health:
      show-details: when_authorized
    env:
      enabled: false  # Ortam değişkenlerini ASLA expose etmeyin
    configprops:
      enabled: false  # Konfigürasyonu expose etmeyin
// Actuator endpoint'lerini ayrı port'ta çalıştırın
// application-prod.yml
management:
  server:
    port: 9090  # Ana port: 8080, Actuator: 9090

// Kubernetes/Docker'da 9090 portunu dışarıya açmayın

Yaygın Hatalar

1. "Güvenlik sonra ekleriz" — Her zaman baştan ekleyin

2. CORS'ta wildcard (*) kullanmak — Spesifik origin belirtin

3. JWT token'da hassas bilgi saklamak — Token şifrelenmez, herkes okuyabilir

4. Rate limiting olmadan login endpoint — Brute-force saldırısına açık

5. Actuator endpoint'leri açık bırakmak — /env ve /configprops ortam değişkenlerini ifşa eder

Özet

  • Defense in Depth: Güvenlik tek katmanlı değil — input validation + authentication + authorization + HTTPS + monitoring birlikte çalışmalı

  • Input Validation: Her girdiye şüpheyle yaklaşın, @Valid + custom validator kullanın

  • JWT Best Practices: Kısa ömürlü access token, refresh token rotation, blacklist mekanizması

  • IDOR Koruması: URL-level yetmez, object-level authorization da şart (@PreAuthorize + custom evaluator)

  • Rate Limiting: Özellikle login ve hassas endpoint'lerde brute-force koruması

  • Dependency Scanning: OWASP Dependency-Check ile bilinen CVE'leri CI/CD'de yakala