✋🏼🔥 Görselleştirilmiş CS: CORS
Bu yazı Lydia Hallie’nin CS Visualized: CORS adlı makalesinin bir çevirisidir.
Konsolunda şu koca kırmızı Access to fetched has been blocked by CORS policy
hatasıyla karşılaşmak her geliştiricinin bir şekilde başına dert olmuştur! 😬 Her ne kadar bu hatadan hızlıca kurtulmanın bazı yolları olsa da bu sefer işleri olduğu gibi kabul etmeyelim. Bunun yerine CORS’un tam olarak ne yaptığına ve aslında neden bizim dostumuz olduğuna bir bakalım.
❗️Bu blog yazısında HTTP’nin temellerini anlatmayacağım. HTTP istekleri ve yanıtları hakkında daha detaylı bilgi edinmek istiyorsanız bir süre önce bu konu hakkında da kısa bir yazı paylaştım. Örneklerimde de HTTP/2 yerine HTTP/1.1 kullandım, bu CORS’u etkilemiyor.
Frontend tarafında çoğunlukla farklı bir yerden aldığımız datayı göstermek isteriz. Bunu gösterebilmemiz için önce tarayıcının server’a datayı çekmek için bir istek yollaması gerekir. Client, server’ın datayı ona gönderebilmesi için gerekli olan tüm bilgilerle beraber bir HTTP isteği yollar.
www.mywebsite.com
adlı sitemiz için api.website.com
’daki bir server’dan bazı kullanıcı bilgileri çekmeye çalıştığımızı düşünelim.
Harika! Server’a bir HTTP isteği attık ve ardından istediğimiz JSON datası ile beraber bir yanıt döndü.
Şimdi birebir aynı isteği farklı bir domain ile deneyelim. İsteği www.mywebsite.com
yerine www.anotherdomain.com
’daki bir web sitesinden yapacağız.
Bir dakika, nasıl yani? Tam olarak aynı isteği yolladık ancak bu sefer tarayıcı tuhaf bir hata gösteriyor?
Az önce CORS’u görev başında görmüş olduk! 💪🏼 Şimdi bu hatanın nasıl oluştuğunu ve tam olarak ne anlama geldiğine bir bakalım.
✋🏼 Same-Origin Policy
Web, same-origin policy adında bir ilkeyi zorunlu tutar. Varsayılan bir durum olarak, yalnızca isteğimiz ile aynı yerde bulunan kaynaklara ulaşabiliriz! Örneğin, https://mywebsite.com/image1.png
’de bulunan bir görseli almakta hiçbir sıkıntı olmayacaktır.
Bir kaynak, farklı bir (sub)domain, protocol ya da port’ta bulunuyorsa cross-origin olur!
Harika, ancak neden “same-origin policy” diye bir şey var?
Same-origin policy diye bir şey olmadığını varsayalım ve yanlışlıkla teyzenizin Facebook’tan gönderdiği virüslü bir linke tıkladınız diyelim. Bu link sizi içinde bankanızın web sitesini gösteren bir iframe gömülü “kötü bir web sitesine” yönlendirdi ve cookieler aracılığıyla başarıyla giriş yapmanızı sağladı! 😬
Bu “kötü web sitesinin” geliştiricileri, sizin adınıza hesaplarına para göndermek için web sitesinin bu iframe’e erişmesini ve bankanızın web sitesinin DOM içeriğiyle etkileşime girmesini mümkün kıldı!
Evet… Bu çok büyük bir güvenlik riski! Kimsenin her şeye erişebilir olmasını istemeyiz. 😧
Neyse ki, same-origin policy burada bize yardımcı oluyor! Bu politika, yalnızca aynı kökenden (same-origin) olan kaynaklara erişebilmemizi sağlar.
Bu durumda,www.evilwebsite.com
kaynağı www.bank.com
'dan cross-origin kaynaklara erişmeye çalıştı! Same-origin policy bunun olmasını engelledi ve kötü web sitesinin geliştiricilerinin banka verilerimize erişememesini sağladı 🥳
Tamam, peki… Ama bunun CORS ile ne ilgisi var?
🔥 Client-side CORS
Same-origin policy yalnızca script dosyaları için geçerli olsa da, tarayıcılar bu ilkeyi JavaScript istekleri için “genişletmiştir”: varsayılan olarak, yalnızca aynı kökenden (same-origin) getirilen kaynaklara erişebiliriz!
Hmm, bizim sık sık cross-origin kaynaklara da erişmemiz gerekiyor ama 🤔 Belki verileri yüklemek için frontend’in backend API’ı ile etkileşime girmesi gerekiyor? İşte bu yüzden tarayıcı, cross-origin isteklere güvenli bir şekilde izin vermek için CORS adlı bir mekanizma kullanır! 🥳
CORS, Cross-Origin Resource Sharing (Kökenler Arası Kaynak Paylaşımı) anlamına gelir. Tarayıcı aynı kökende bulunmayan kaynaklara erişmemize izin vermese de, bir yandan bu kaynaklara güvenli bir şekilde eriştiğimizden emin olurken bir yandan da bu güvenlik kısıtlamalarını biraz değiştirmek için CORS’u kullanabiliriz 🎉
Kullanıcı aracıları (bir tarayıcı gibi), HTTP response’undaki CORS’a özgü başlıkların değerlerine bağlı olarak, normal şartlar altında engellenecek olan cross-origin isteklerine izin vermek için CORS mekanizmasını kullanabilir! ✅
Cross-origin bir istek yapıldığında, client HTTP isteğimize otomatik olarak fazladan bir header daha ekler: Origin. Origin header’ının değeri, isteğin geldiği kaynaktır!
Tarayıcının cross-origin kaynaklara erişime izin vermesi için server yanıtından, bu server’ın cross-origin isteklere izin verip vermediğini belirten belirli headerlar bekler!
💻 Server-side CORS
Bir server geliştiricisi olarak, HTTP yanıtına her biri Access-Control-*
ile başlayan ekstra başlıklar ekleyerek cross-origin isteklere izin verildiğinden emin olabiliriz. 🔥 Bu CORS response başlıklarının değerlerine bağlı olarak, tarayıcı artık same-origin policy tarafından engellenecek olan belirli cross-origin yanıtlara izin verebilir!
Kullanabileceğimiz birkaç CORS başlığı olmasına rağmen, tarayıcının cross-origin erişimine izin vermek için ihtiyaç duyduğu tek bir başlık vardır: Access-Control-Allow-Origin
!
Bu başlığın değeri, hangi kökenlerin server’dan talep ettikleri kaynaklara erişmesine izin verildiğini belirtir.
https://mywebsite.com
'un erişmesi gereken bir server’ı geliştiriyorsak, o domain değerini Access-Control-Allow-Origin
başlığına ekleyebiliriz!
Mükemmel! 🎉 Bu başlık artık server’ın client’a gönderdiği yanıta eklendi. Bu başlığı ekleyerek, isteği https://mywebsite.com
'dan gönderdiğimiz takdirde, same-origin policy https://api.mywebsite.com
kaynağında bulunan verileri almamızı kısıtlamayacak!
Tarayıcı içindeki CORS mekanizması, Access-Control-Allow-Origin
header değerinin, istek tarafından gönderilen Origin
değerine eşit olup olmadığını kontrol eder 🤚🏼
Mevcut durumda isteğimizin kökeni Access-Control-Allow-Origin
response başlığında listelenen https://mywebsite.com
'dur!
Mükemmel! 🎉 Cross-origin kaynakları başarıyla alabildik! Peki, Access-Control-Allow-Origin
başlığında listelenmeyen bir kökenden bu kaynaklara erişmeye çalıştığımızda ne olur? 🤔
Ahh evet, CORS bazen çok sinir bozucu olan şu meşhur korkunç hatayı gönderir! Şimdi bakınca bunun aslında gayet mantıklı olduğunu görebiliyoruz.
The 'Access-Control-Allow-Origin' header has a value
'https://www.mywebsite.com' that is not equal
to the supplied origin.
Yukarıdaki durumda, kaynak https://www.anotherwebsite.com
idi. Ancak server’da Access-Control-Allow-Origin
başlığındaki izin verilen kökenler listesinde bu sağlanan köken yok! CORS, isteği başarıyla engelledi ve böylece kodumuzda getirilen verilere erişemiyoruz 😃
CORS izin verilen köken listesi için joker
*
karakterini eklememize izin verir. Bu, tüm kökenlerden gelen isteklerin istenen kaynaklara erişmesi gerektiği anlamına gelir, bu yüzden dikkatli olun!
Access-Control-Allow-Origin
sağlayabileceğimiz birçok CORS başlığından sadece bir tanesidir. Bir server geliştiricisi, belirli isteklere izin vermek (ya da iptal etmek) için sunucunun CORS politikalarını genişletebilir! 💪🏼
Diğer bir yaygın başlık, Access-Control-Allow-Methods
başlığıdır! CORS, yalnızca listelenen yöntemlerle gönderilmişse cross-origin isteklerine izin verecektir.
Bu durumda, yalnızca GET
, POST
veya PUT
yöntemine sahip isteklere izin verilecektir! PATCH
ya da DELETE
gibi diğer yöntemler engellenecektir ❌
Diğer olası CORS başlıklarının ne olduğunu ve ne için kullanıldığını merak ediyorsanız, bu listeye göz atın.
PUT
, PATCH
ve DELETE
isteklerinden bahsetmişken, CORS aslında bu istekleri farklı şekilde ele alır! 🙃 Bu “basit olmayan” istekler, preflight request adı verilen bir şeyi başlatır!
🚀 Preflighted Requests
CORS’un iki tür isteği vardır; basit bir istek ve preflighted (önceden kontrol edilmiş) bir istek. Bir isteğin basit mi yoksa preflighted mı olduğu, istek içindeki bazı değerlere bağlıdır (endişelenmeyin, bunu ezberlemenize gerek yok :D).
İstek bir GET
ya da POST
yöntemi olduğunda ve herhangi özel bir header’a sahip olmadığında basittir! PUT
, PATCH
ya da DELETE
yöntemiyle yapılan istekler gibi diğer tüm istekler önceden kontrol edilecektir.
Bir isteğin basit bir istek olabilmesi için hangi gereksinimleri karşılaması gerektiğini merak ediyorsanız, MDN’in faydalı bir listesi var!
Tamam ama bu preflighted istek ne anlama geliyor ve neden böyle bir şey oluyor?
Asıl istek gönderilmeden önce client, preflighted bir istek oluşturur! Preflighted istek, Access-Control-Request-*
başlıklarında yapmak üzere olduğumuz gerçek istek hakkında bilgi içerir 🔥
Bu, sunucuya, tarayıcının yapmaya çalıştığı gerçek istek hakkında bilgi verir: isteğin yöntemi nedir, ek başlıklar nelerdir, vb.
Server bu preflighted isteği alır ve CORS başlıklarıyla birlikte boş bir HTTP yanıtı gönderir! Tarayıcı, CORS başlıkları dışında hiçbir veri içermeyen preflight yanıtını alır ve HTTP isteğine izin verilip verilmeyeceğini kontrol eder! ✅
Durum buysa, tarayıcı asıl isteği server’a gönderir ve server istediğimiz verilerle yanıt döner!
Ancak durum böyle değilse, CORS preflighted isteği engeller ve asıl istek asla gönderilmez ✋🏼 Preflighted istekler, herhangi bir CORS politikasının (henüz) etkin olmadığı serverlardaki kaynaklara erişmemizin veya kaynakları değiştirmemizi engellemenin harika bir yoludur! Serverlar artık potansiyel olarak istenmeyen cross-origin isteklerinden korunuyor 😃
💡 Server’a gidiş-dönüş sayısını azaltmak için, CORS taleplerimize bir
Access-Control-Max-Age
başlığı ekleyerek önceden kontrol edilmiş yanıtları önbelleğe alabiliriz (caching)! Preflighted bir yanıtı bu şekilde önbelleğe alabiliriz ve tarayıcı yeni bir preflighted istek göndermek yerine bunu kullanabilir!
🍪 Credentials
Çerezler, authorization başlıkları ve TLS sertifikaları varsayılan olarak yalnızca same-origin isteklerde ayarlanır! Ancak, bu kimlik bilgilerini (credentials) cross-origin isteğimizde de kullanmak isteyebiliriz. Belki de kullanıcıyı tanımlamak için server’ın kullanabileceği isteğe çerezler eklemek istiyoruzdur!
CORS varsayılan olarak credentials’ı içermese de, bunu Access-Control-Allow-Credentials
CORS başlığını ekleyerek değiştirebiliriz! 🎉
Cross-origin isteğimize çerezleri ve diğer authorization başlıklarını dahil etmek istiyorsak, istekte withCredentials
alanını true
olarak ayarlamamız ve yanıta Access-Control-Allow-Credentials
başlığını eklememiz gerekir.
Her şey hazır! Artık cross-origin isteğimize credentials’ı da dahil edebiliriz 🥳
CORS hatalarının zaman zaman sinir bozucu olabileceği konusunda hemfikir olabileceğimizi düşünmeme rağmen, tarayıcıda güvenli bir şekilde cross-origin istekleri yapmamızı sağlaması inanılmaz (hatta biraz da sevgiyi hak ediyor 😆)
Açıkçası, same-origin policy ve CORS hakkında bu blog yazısında ele alabildiğimden çok daha fazlası var! Neyse ki, bu konuda daha fazlasını okumak istiyorsanız bu ve W3 spesifikasyonu gibi birçok harika kaynak var. 💪🏼
Ve her zaman olduğu gibi, bana ulaşmaktan çekinmeyin! 😊