← Kursa Dön
📄 Text · 12 min

CORS Konfigürasyonu

CORS (Cross-Origin Resource Sharing — Çapraz Kaynak Paylaşımı), web güvenliğinin temel kavramlarından biridir. Modern web tarayıcıları, bir web sayfasının farklı bir kaynaktan (origin) veri çekmesini varsayılan olarak engeller. Bu güvenlik mekanizmasına Same-Origin Policy (Aynı Kaynak Politikası) denir. CORS, bu kısıtlamayı kontrollü bir şekilde gevşetmenizi sağlar.

Bir apartman düşünün. Apartman kapısında güvenlik görevlisi var. Kendi apartmanınızın sakini iseniz doğrudan girebilirsiniz (same-origin). Ama farklı bir apartmandan geliyorsanız, güvenlik görevlisi yöneticiden onay almalıdır (CORS). CORS, sunucunun tarayıcıya "bu origin'den gelen isteklere izin ver" demesini sağlayan mekanizmadır.

Same-Origin Policy Nedir?

İki URL'in "aynı kaynak" (same origin) sayılması için protokol, host ve port aynı olmalıdır:

https://example.com/page1
https://example.com/page2       → Aynı origin ✅ (path farklı, sorun değil)

https://example.com
http://example.com              → Farklı origin ❌ (protokol farklı: https vs http)

https://example.com
https://api.example.com         → Farklı origin ❌ (subdomain farklı)

https://example.com
https://example.com:8443        → Farklı origin ❌ (port farklı)

https://example.com:443
https://example.com             → Aynı origin ✅ (443 varsayılan HTTPS portu)

Same-Origin Policy sayesinde, evil-site.com'daki bir JavaScript kodu bank.com'un API'sine istek atamaz. Bu, kullanıcıların güvenliğini sağlar. Tarayıcı bu kontrolü yapar — sunucu değil.

⚠️ Dikkat: Same-Origin Policy sadece tarayıcı tarafından uygulanır. Postman, curl veya mobil uygulamalar bu kısıtlamaya tabi değildir. CORS bir güvenlik önlemidir ama tek başına yeterli değildir — sunucu tarafında da yetkilendirme gerekir.

CORS Neden Gerekli?

Modern web uygulamalarında frontend ve backend genellikle farklı sunucularda çalışır:

Frontend: https://app.example.com     (React/Vue uygulaması — Vercel'de)
Backend:  https://api.example.com     (Spring Boot API — AWS'de)

Bu durumda frontend'den backend'e yapılan istekler cross-origin isteği sayılır ve tarayıcı tarafından engellenir. Geliştirme ortamında da aynı sorun vardır:

Frontend: http://localhost:3000  (React dev server)
Backend:  http://localhost:8080  (Spring Boot)

Port farklı olduğu için bu da cross-origin'dir! CORS yapılandırması olmadan frontend backend'e istek atamaz.

CORS Nasıl Çalışır?

CORS iki tip istek tanır:

1. Simple Request (Basit İstek)

GET, HEAD veya POST (belirli content-type'larla: text/plain, multipart/form-data, application/x-www-form-urlencoded) istekleri basit istek sayılır. Tarayıcı doğrudan isteği gönderir ve yanıttaki CORS header'larını kontrol eder:

# İstek:
GET /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com

# Yanıt:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json

[{"id": 1, "name": "Ali"}, ...]

Tarayıcı, yanıttaki Access-Control-Allow-Origin header'ını kontrol eder. Eğer istemcinin origin'i bu header'da varsa, yanıtı JavaScript'e gösterir. Yoksa yanıtı engeller (network seviyesinde veri gelir ama JavaScript erişemez).

2. Preflight Request (Ön Kontrol İsteği)

PUT, DELETE, PATCH veya custom header içeren isteklerden önce tarayıcı otomatik olarak bir OPTIONS isteği gönderir. Bu "ön kontrol" isteği sunucuya "bu tip bir istek yapabilir miyim?" diye sorar:

# Adım 1 — Preflight (OPTIONS) İsteği (tarayıcı otomatik gönderir):
OPTIONS /api/users/42 HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization

# Adım 2 — Preflight Yanıtı (sunucu):
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 3600

# Adım 3 — Asıl İstek (preflight başarılıysa):
PUT /api/users/42 HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
Authorization: Bearer xxx

{"name": "Ahmet Güncel"}

Access-Control-Max-Age: 3600 header'ı, tarayıcının preflight sonucunu 1 saat cache'lemesini sağlar. Bu süre içinde aynı tipteki istekler için tekrar preflight yapılmaz — performans kazancı.

Spring Boot'ta CORS — @CrossOrigin Annotation

En basit CORS yapılandırması @CrossOrigin annotation'ı iledir. Sınıf veya metot seviyesinde kullanılabilir:

// Metot seviyesinde — sadece bu endpoint için
@RestController
@RequestMapping("/api/users")
public class UserController {

    @CrossOrigin(origins = "https://app.example.com")
    @GetMapping
    public List<User> getUsers() {
        return userService.findAll();
    }
}

// Sınıf seviyesinde — tüm endpoint'ler için
@RestController
@RequestMapping("/api/products")
@CrossOrigin(
    origins = {"https://app.example.com", "https://admin.example.com"},
    methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE},
    allowedHeaders = {"Content-Type", "Authorization", "X-Request-Id"},
    exposedHeaders = {"X-Total-Count", "X-Request-Id"},
    allowCredentials = "true",
    maxAge = 3600
)
public class ProductController {

    @GetMapping
    public List<Product> getAll() { /* ... */ }

    @PostMapping
    public Product create(@RequestBody Product product) { /* ... */ }
}

@CrossOrigin Özellikleri:

ÖzellikAçıklamaVarsayılan
originsİzin verilen origin'ler* (tümü)
methodsİzin verilen HTTP metotlarıMapping'deki metotlar
allowedHeadersİzin verilen request header'lar* (tümü)
exposedHeadersİstemcinin okuyabileceği response header'larBoş
allowCredentialsCookie/auth gönderilsin mi?false
maxAgePreflight cache süresi (saniye)1800 (30 dk)

💡 İpucu: @CrossOrigin küçük projelerde pratiktir ancak her controller'a tek tek eklemek büyük projelerde zahmetlidir. Global yapılandırma daha uygundur.

Global CORS Konfigürasyonu — WebMvcConfigurer

Her controller'a tek tek @CrossOrigin eklemek yerine, global yapılandırma yapabilirsiniz:

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")  // URL pattern
            .allowedOrigins(
                "https://app.example.com",
                "https://admin.example.com")     // İzin verilen origin'ler
            .allowedMethods("GET", "POST", "PUT",
                "DELETE", "PATCH", "OPTIONS")     // İzin verilen HTTP metotları
            .allowedHeaders("*")                  // İzin verilen header'lar
            .exposedHeaders("X-Total-Count",
                "X-Page-Number", "X-Request-Id")  // İstemciye açılan header'lar
            .allowCredentials(true)               // Cookie/auth izni
            .maxAge(3600);                        // Preflight cache (1 saat)

        // Farklı pattern'ler için farklı kurallar
        registry.addMapping("/public/**")
            .allowedOrigins("*")
            .allowedMethods("GET")
            .maxAge(86400);  // 24 saat — public endpoint'ler değişken değil

        // Webhook'lar — sadece belirli servislerden
        registry.addMapping("/webhooks/**")
            .allowedOrigins("https://stripe.com", "https://github.com")
            .allowedMethods("POST")
            .allowedHeaders("Content-Type", "X-Hub-Signature-256");
    }
}

CorsFilter ile Yapılandırma

Spring Security kullanıyorsanız CorsFilter bean'i tanımlamak daha güvenilir bir yaklaşımdır. Security filter chain'i WebMvcConfigurer'dan önce çalışır, bu yüzden CORS konfigürasyonunun Security katmanında da tanınması gerekir:

@Configuration
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of(
            "https://app.example.com",
            "https://admin.example.com"
        ));
        config.setAllowedMethods(List.of(
            "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
        config.setAllowedHeaders(List.of("*"));
        config.setExposedHeaders(List.of(
            "X-Total-Count", "X-Request-Id", "Authorization"));
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);

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

        return new CorsFilter(source);
    }
}

Spring Security ile CORS Entegrasyonu

Spring Security kullanırken CORS yapılandırması Security config'de de yapılmalıdır:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .csrf(csrf -> csrf.disable()) // REST API'ler için
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // Preflight
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of(
            "https://app.example.com",
            "https://admin.example.com"));
        config.setAllowedMethods(List.of(
            "GET", "POST", "PUT", "DELETE", "PATCH"));
        config.setAllowedHeaders(List.of("*"));
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);

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

⚠️ Dikkat: Spring Security ile birlikte CORS kullanırken mutlaka http.cors() ekleyin. Aksi takdirde Security filter chain CORS header'larını eklemez ve tüm cross-origin istekler başarısız olur.

Ortama Göre CORS Yapılandırması

Geliştirme ve production ortamlarında farklı CORS ayarları gerekir:

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Value("${app.cors.allowed-origins}")
    private String[] allowedOrigins;

    @Value("${app.cors.max-age:3600}")
    private long maxAge;

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins(allowedOrigins)
            .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH")
            .allowedHeaders("*")
            .allowCredentials(true)
            .maxAge(maxAge);
    }
}
# application-dev.properties (geliştirme)
app.cors.allowed-origins=http://localhost:3000,http://localhost:5173,http://localhost:4200

# application-prod.properties (production)
app.cors.allowed-origins=https://app.example.com,https://admin.example.com

# application-staging.properties (staging)
app.cors.allowed-origins=https://staging.example.com

Bu yaklaşımla CORS yapılandırması ortama göre otomatik değişir. Geliştirmede localhost portları, production'da gerçek domain'ler kullanılır.

Güvenlik Uyarıları

⚠️ Kritik Güvenlik Kuralları:

  1. `allowedOrigins("*")` ile `allowCredentials(true)` birlikte kullanılamaz. Cookie gönderimini etkinleştiriyorsanız, origin'leri açıkça belirtmelisiniz. Tarayıcı bu kombinasyonu reddeder.

  2. Production'da `*` origin kullanmayın. Tüm origin'lere izin vermek, herhangi bir web sitesinin API'nize istek atmasına olanak tanır.

  3. Gereksiz HTTP metotlarına izin vermeyin. Public API'lerde sadece GET yeterli olabilir. DELETE veya PUT'a izin vermeniz gerekmeyebilir.

  4. `exposedHeaders` ile sadece gerekli header'ları açın. Gereksiz header bilgisi güvenlik riski oluşturabilir.

  5. `allowedOriginPatterns()` ile wildcard pattern kullanabilirsiniz:

config.setAllowedOriginPatterns(List.of(
    "https://*.example.com",      // Tüm subdomain'ler
    "http://localhost:[*]"         // Tüm localhost portları (dev)
));

Gerçek Dünya Senaryosu: Microservice CORS

Microservice mimarisinde API Gateway üzerinden CORS yönetimi en temiz yaklaşımdır. Ancak direkt backend erişimi de olabilir:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Value("${app.cors.allowed-origins}")
    private List<String> allowedOrigins;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .cors(cors -> cors.configurationSource(corsConfig()))
            .csrf(csrf -> csrf.disable())
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                .requestMatchers("/api/public/**", "/actuator/health").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
            .build();
    }

    @Bean
    public CorsConfigurationSource corsConfig() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(allowedOrigins);
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH"));
        config.setAllowedHeaders(List.of(
            "Authorization", "Content-Type", "X-Request-Id",
            "X-Correlation-Id", "Accept", "Origin"));
        config.setExposedHeaders(List.of(
            "X-Total-Count", "X-Request-Id", "X-Correlation-Id",
            "X-RateLimit-Remaining", "X-RateLimit-Reset"));
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);

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

Bu yapılandırmada dikkat edilecek noktalar:

  • Stateless session: JWT tabanlı authentication ile session kullanılmaz

  • OAuth2 Resource Server: JWT token doğrulaması

  • Correlation ID: Distributed tracing için özel header'lar expose edilir

  • Rate limit header'ları: İstemci kalan istek hakkını görebilir

Nginx ile CORS (Reverse Proxy)

Eğer uygulamanızın önünde Nginx varsa, CORS header'larını Nginx seviyesinde de yönetebilirsiniz. Ancak dikkatli olun — hem Nginx hem Spring'de CORS yapılandırması olursa header'lar çift eklenir:

# nginx.conf — CORS header'ları Nginx'te yönetiliyor
location /api/ {
    # Preflight istekleri Nginx'te karşıla
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' '$http_origin' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH' always;
        add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;
        add_header 'Access-Control-Max-Age' 86400;
        add_header 'Content-Length' 0;
        return 204;
    }

    proxy_pass http://spring-boot-app:8080;
    
    # Spring'den gelen yanıtlara CORS header ekle
    add_header 'Access-Control-Allow-Origin' '$http_origin' always;
    add_header 'Access-Control-Allow-Credentials' 'true' always;
}

⚠️ Dikkat: Eğer Nginx'te CORS yönetiyorsanız, Spring tarafındaki CORS yapılandırmasını kapatın. Aksi takdirde Access-Control-Allow-Origin header'ı iki kez eklenir ve tarayıcı bunu hata olarak değerlendirir.

CORS Debug Etme

CORS sorunlarını debug etmek zor olabilir. İşte sistematik yaklaşım:

# 1. Preflight isteğini elle test edin
curl -v -X OPTIONS https://api.example.com/api/users \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type, Authorization"

# Yanıtta şunları arayın:
# Access-Control-Allow-Origin: https://app.example.com
# Access-Control-Allow-Methods: POST
# Access-Control-Allow-Headers: Content-Type, Authorization

# 2. Gerçek isteği test edin
curl -v https://api.example.com/api/users \
  -H "Origin: https://app.example.com"

Spring Boot'ta debug loglama:

# CORS filter debug
logging.level.org.springframework.web.cors=DEBUG
logging.level.org.springframework.web.filter.CorsFilter=DEBUG

Yaygın CORS Hataları ve Çözümleri

Hata 1: Access to XMLHttpRequest blocked by CORS policy

Access to XMLHttpRequest at 'https://api.example.com/users' 
from origin 'https://app.example.com' has been blocked by CORS policy: 
No 'Access-Control-Allow-Origin' header is present on the requested resource.

Çözüm: Backend'de CORS yapılandırması yapın — addCorsMappings() veya CorsFilter ekleyin.

Hata 2: Access-Control-Allow-Credentials must be 'true'

The value of the 'Access-Control-Allow-Credentials' header in the response 
is '' which must be 'true' when the request's credentials mode is 'include'.

Çözüm: allowCredentials(true) ekleyin ve allowedOrigins("*") yerine açık origin belirtin.

Hata 3: Preflight (OPTIONS) 403 hatası

Spring Security yapılandırmanızda OPTIONS isteklerine izin verin:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.cors(Customizer.withDefaults()) // CORS etkinleştir
        .authorizeHttpRequests(auth -> auth
            .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
            .anyRequest().authenticated()
        );
    return http.build();
}

Hata 4: CORS geliştirmede çalışıyor ama production'da çalışmıyor

Nedeni genellikle:

  • Reverse proxy (Nginx) CORS header'larını siliyor

  • CDN (CloudFront) OPTIONS isteklerini cache'liyor

  • Production ortamında farklı origin URL kullanılıyor

Özet

  • CORS, farklı origin'lerden gelen HTTP isteklerini kontrollü şekilde kabul etmenizi sağlar. Same-Origin Policy'nin kontrollü gevşetilmesidir.

  • Global yapılandırma için WebMvcConfigurer.addCorsMappings() veya CorsFilter bean kullanın. Her controller'a @CrossOrigin eklemekten kaçının.

  • Production'da origin'leri açıkça belirtin, * kullanmaktan kaçının. Ortam bazlı yapılandırma için @Value ve profiller kullanın.

  • Spring Security ile birlikte kullanırken http.cors() eklemeyi ve OPTIONS isteklerine permitAll() vermeyi unutmayın.

  • Preflight cache (maxAge) ile gereksiz OPTIONS isteklerini azaltın — her API çağrısından önce preflight yapılması performansı düşürür.

  • CORS sadece tarayıcı güvenliğidir — API güvenliği için authentication ve authorization da şarttır.