REST API Sistemi (Jeton, Kapsam ve Hız Sınırı)
Jetonu panelden üretin, kapsamını seçin, curl ile deneyin. Veritabanında yalnızca SHA-256 özeti durur; kapsam denetimi rota düzeyinde, hız sınırı jeton başına ve yanıtlar data/meta/links zarfıyla sayfalanır.
Ekran Görüntüleri 5 görsel
Bu Örnek Ne Yapıyor?
- Jeton veritabanında düz metin durmaz; yalnızca SHA-256 özeti saklanır
- Açık metin jeton yalnızca üretildiği anda bir kez gösterilir
- Kapsam denetimi rotada yapılır, denetleyicide unutulabilecek bir if değil
- Hız sınırı jeton başına — IP başına değil; başkasının trafiği sizi engellemez
- X-RateLimit-Limit / -Remaining / -Reset başlıkları her yanıtta
- Sınır aşımında 429 ve Retry-After
- data + meta + links yanıt zarfı; sayfa boyutu tavanlanır
- İptal edilen jeton silinmez (revoked_at) — istek kayıtları bağlı kalır
- Hataların makine tarafından okunabilir bir code alanı vardır
Nerede İşe Yarar?
- Mobil uygulamasına veya başka bir servise API açacaklar
- Anahtarı düz metin saklamanın alternatifini öğrenmek isteyenler
- Kapsam, hız sınırı ve sayfalama kalıplarını doğru kurmak isteyenler
- JWT'ye ihtiyaç duymayan, sunucuda iptal edilebilir jeton isteyenler
Gereksinimler
- PHP 8.0+ · MySQL 5.7+ / MariaDB 10.3+ · pdo_mysql · mbstring · Apache mod_rewrite (Authorization başlığı geçirilmeli)
Nasıl Kurulur?
- Depoyu klonlayın veya ZIP olarak indirip web kökünüze açın
- database.sql dosyasını içe aktarın (veritabanını kendisi oluşturur)
- .env.example dosyasını .env adıyla kopyalayıp veritabanı bilgilerinizi yazın
- Tarayıcıdan açıp [email protected] / Admin1234 ile giriş yapın
- API Jetonları sayfasından kendi jetonunuzu üretip curl ile deneyin
Veritabanı şeması projedeki database.sql dosyasında.
Nasıl Çalışıyor?
REST API Sistemi
PHP 8 · PDO · MySQL · Jeton Yetkilendirme · Kapsam (Scope) · Hız Sınırı · Sayfalama · Çılgın Yazılım Tasarım Kalıbı
Jetonu panelden üretin, kapsamını seçin, curl ile deneyin.
🇹🇷 Türkçe · 🇬🇧 English
Canlı Demo
Kurulum yok, kayıt yok, indirme yok — tarayıcınızdan 3 saniyede deneyin.
▲ Görsele tıklayarak demoyu açabilirsinizDemo hesapları
| Rol | E-posta | Parola |
|---|---|---|
| Yönetici | [email protected] | Admin1234 |
| Kullanıcı | [email protected] | Demo1234 |
Demoda 60 saniyede neleri deneyebilirsiniz?
| # | Şunu deneyin | Perde arkasında ne oluyor? |
|---|---|---|
| 1 | API Jetonları sayfasında "Jeton üret" deyin, read kutusunu işaretleyin | Açık metin jeton yalnızca bu bir kez gösterilir. Veritabanında yalnızca SHA-256 özeti durur; sayfayı kapatırsanız jetonu bir daha kimse göremez |
| 2 | Jetonu kopyalayıp curl ile bir istek atın | curl -H "Authorization: Bearer " .../api/v1/users — JSON döner |
| 3 | Yanıtın meta bölümüne bakın | Toplam kayıt, sayfa, sayfa boyutu ve toplam sayfa sayısı orada. İstemcinin sayfalamayı tahmin etmesi gerekmez |
| 4 | links bölümüne bakın | Bir sonraki sayfanın tam adresi hazır gelir. İstemci adres kurmaz, takip eder |
| 5 | ?per=200 deneyin | 200 listede olmadığı için istek 20'ye düşer. Sayfa boyutu bir beyaz listedir; aksi hâlde tek istekle tüm tabloyu çekmek mümkün olurdu |
| 6 | Sadece read kapsamlı jetonla POST atmayı deneyin | 403 ve insufficient_scope döner. Kapsam kontrolü rotanın ara katmanında yapılır, denetleyicinin içinde unutulabilecek bir if değildir |
| 7 | Yanıt başlıklarına bakın (curl -i) | X-RateLimit-Limit, X-RateLimit-Remaining ve X-RateLimit-Reset gelir. İstemci sınıra çarpmadan önce yavaşlayabilir |
| 8 | Aynı jetonla 60'tan fazla istek atın | 429 ve Retry-After başlığı döner. Sayım jeton başına yapılır; başkasının trafiği sizi engellemez |
| 9 | Jetonun "İptal et" düğmesine basın, sonra tekrar istek atın | 401. Jeton silinmez, revoked_at doldurulur — geçmiş istek kayıtları bağlı kaldığı için "hangi jeton ne yaptı" sorusu cevapsız kalmaz |
| 10 | API Belgeleri sayfasını açın | Bütün uç noktalar, kapsamları, hata kodları ve kopyalanabilir curl örnekleri orada |
| 11 | Aynı sayfadaki Örnek Kullanım bölümünden dilinizi seçin | cURL, PHP, JavaScript ve Python örnekleri bu sunucunun gerçek adresiyle basılır. İndir düğmesi çalışır bir dosya verir; içine jeton yazılmaz, yer tutucu konur |
İpucu: Hata yanıtları da düzenlidir: her hatanın makine tarafından okunabilir bircodealanı (invalid_token,insufficient_scope,validation_failed,rate_limit_exceeded) ve insan tarafından okunabilir birmessagealanı vardır.
Demo alanı hakkında bilinmesi gerekenler
| Konu | Durum |
|---|---|
| Veriler | database.sql içindeki 51 kullanıcı + 3 örnek jeton + 28 istek kaydı. Gerçek kişi verisi yoktur. |
| Hazır jeton | Yoktur ve olmayacaktır. Depoda duran bir jeton, projeyi indiren herkesin bildiği bir jeton demektir. Kendinizinkini panelden üretin. |
| Sıfırlama | Demo veritabanı düzenli aralıklarla başlangıç hâline döner; ürettiğiniz jeton kalıcı değildir. |
| Hız sınırı | Pencere başına 60 istek. Örnek jetonların kayıtları sizin hakkınızı yemez; sayım jeton başınadır. |
APP_DEBUG | Canlıda kendiliğinden false — sunucu adından türetilir. |
| Bağımlılık | Sıfır. Composer yok, npm yok, CDN yok. |
Bu Proje Nedir?
"API yazalım" denince ortaya çoğu zaman şu çıkar: bir api.php dosyası, içinde if ($_GET['action'] == 'kullanicilar') ve sonunda echo json_encode($rows). Çalışır — ta ki şu sorular gelene kadar:
- Bu isteği kim yaptı? Anahtar yoksa cevap yok.
- Bu anahtar neyi yapabilir? Okuyabilen her istemci silebiliyorsa, raporlama betiğiniz bir hatada veritabanınızı boşaltabilir.
- Anahtar sızdı, ne yapacağım? Anahtarı düz metin sakladıysanız veritabanı sızıntısı doğrudan hesap sızıntısıdır.
- Bir istemci saniyede 300 istek atıyor, diğerleri bekliyor.
- 10.000 kayıt döndürdüm, yanıt 8 MB.
Bu proje bu beş soruyu birden cevaplayan bir API katmanı kuruyor. Jetonlar panelden üretilir, kapsamla sınırlanır (read / write), veritabanında yalnızca özeti durur, iptal edilebilir; her istek jeton başına hız sınırından geçer ve listeler her zaman sayfalanır.
Öne çıkan tarafı, API'nin tek başına değil, onu yöneten bir panelle birlikte gelmesidir: jetonu üretmek, kapsamını seçmek, son kullanımını görmek ve iptal etmek için phpMyAdmin açmanız gerekmez.
Kimler için uygun?
- Mobil uygulamasına veya başka bir servise API açacaklar
- Anahtarı düz metin saklamanın neden tehlikeli olduğunu ve alternatifini öğrenmek isteyenler
- Kapsam (scope), hız sınırı ve sayfalama kalıplarını doğru kurmak isteyenler
- JWT'ye ihtiyaç duymayan, sunucu tarafında iptal edilebilir jeton isteyenler
- Bootstrap 5 üzerine kurulu, tekrar kullanılabilir bir panel kalıbı arayanlar
Bu proje, Çılgın Yazılım Kütüphanesi altında yayınlanan açıklamalı, üretime hazır örneklerden biridir.
İçindekiler
- Canlı Demo
- Bu Proje Nedir?
- Ekran Görüntüleri
- Hızlı başlangıç
- Kritik Kararlar
- Neler Var?
- API Referansı
- Hata kodları
- Güvenlik: Neyi, Nasıl Kapattık?
- Kurulum
- Yapılandırma
- Dosya Yapısı
- Nasıl Çalışıyor?
- Veritabanı Şeması
- SSS
- Canlı Ortama Alırken
- Sorun Giderme
- Yol Haritası
- Katkı
- Lisans
Ekran Görüntüleri
| Giriş | Kontrol Paneli |
|---|---|
![]() | ![]() |
| API Jetonları | API Belgeleri |
|---|---|
![]() | ![]() |
Koyu tema
390px genişlikte mobil görünüm
Hızlı başlangıç
1 · Jeton üretin. Panele girin → API Jetonları → "Jeton üret". Kapsamı seçin (read, write veya ikisi).
Açık metin jeton yalnızca o an gösterilir:
cy_9f2b7c1d5e83a04f6b91c2d7e0a5f38b4c6d9e2a1f7b30c8d5e4a9b6c3f1d8272 · İstek atın.
curl -H "Authorization: Bearer cy_9f2b..." \
"http://localhost/rest-api-system/api/v1/users?per=10&page=2"3 · Yanıtı okuyun.
{
"data": [
{ "id": 11, "name": "Fatma", "surname": "YILDIZ",
"email": "[email protected]", "is_active": false,
"created_at": "2025-01-15T23:58:29+03:00" }
],
"meta": {
"total": 51,
"per_page": 10,
"current_page": 2,
"last_page": 6,
"from": 11,
"to": 20,
"has_more": true
},
"links": {
"self": "/rest-api-system/api/v1/users?per=10&page=2",
"next": "/rest-api-system/api/v1/users?per=10&page=3",
"prev": "/rest-api-system/api/v1/users?per=10&page=1"
}
}4 · Başlıklara bakın.
curl -i -H "Authorization: Bearer cy_9f2b..." .../api/v1/usersHTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 1757012400
Kritik Kararlar
1. Jeton veritabanında düz metin durmaz
Bir API anahtarı, bir paroladır. Düz metin saklarsanız veritabanı sızıntısı doğrudan hesap sızıntısına dönüşür — üstelik jeton parola gibi kullanıcı tarafından değiştirilmez, aylarca aynı kalır.
// app/Repositories/ApiTokenRepository.php
private static function hash(string $plain): string
{
return hash('sha256', $plain);
}Tabloda yalnızca
token_hash durur. Doğrulama, gelen jetonun özetini alıp özete göre arama yapar.
Neden password_hash() değil, SHA-256? İkisi farklı işler içindir. password_hash() bilerek yavaştır (bcrypt), çünkü paroları sözlük saldırısına karşı korur — insanlar 123456 seçer. API jetonu ise 32 bayt rastgeledir; sözlük saldırısına konu değildir, tuza ihtiyacı yoktur. Buna karşılık her API isteğinde doğrulanır: bcrypt kullanmak her isteğe 100 ms eklerdi. SHA-256 hem yeterli hem hızlıdır.
2. Kapsam kontrolü rotada yapılır, denetleyicide değil
// routes/web.php
$router->get('api/v1/users', UserApiController::class, 'index', ['api', 'scope:read']);
$router->post('api/v1/users', UserApiController::class, 'store', ['api', 'scope:write']);
$router->delete('api/v1/users/{id}',UserApiController::class,'destroy', ['api', 'scope:write']);Kapsam, denetleyicinin ilk satırına yazılan bir
if olsaydı, yeni bir uç nokta ekleyen kişi bir gün onu yazmayı unuturdu — ve bu unutma sessizce açık bırakırdı.
Rota tablosunda ise yetki, uç noktanın tanımının parçasıdır. Kimin neye erişebildiğini görmek için tek bir dosyaya bakmak yeterlidir.
3. Hız sınırı jeton başına, IP başına değil
IP başına sınır iki yönden de yanlış çalışır: aynı ofisten çıkan yüz kullanıcı tek IP'dedir (haksız yere engellenirler), tek bir saldırgan ise binlerce IP kullanabilir (hiç engellenmez).
Jeton başına sayım, sınırı kimliğe bağlar. Sizin trafiğiniz başkasının hakkını yemez.
SELECT COUNT(*) FROM api_requests
WHERE token_id = :id AND requested_at >= (NOW() - INTERVAL :window SECOND)Üç başlık her yanıtta gönderilir (
X-RateLimit-Limit, -Remaining, -Reset), böylece iyi niyetli istemci sınıra çarpmadan önce yavaşlayabilir. Sınır aşılınca 429 ve Retry-After döner.
Bir saatten eski satırlar kendiliğinden temizlenir; tablo sonsuza kadar büyümez.
4. İptal edilen jeton silinmez
UPDATE api_tokens SET revoked_at = NOW() WHERE id = :idSatır silinseydi ona bağlı
api_requests kayıtları da giderdi (ON DELETE CASCADE) ve "geçen ay hangi jeton ne yaptı?" sorusu cevapsız kalırdı. Bir güvenlik olayından sonra tam da o soruyu sormak istersiniz.
Doğrulama revoked_at IS NULL koşulunu arar; iptal edilmiş jeton anında geçersizdir.
5. Sayfa boyutu beyaz listeden geçer
public const PER_PAGE_OPTIONS = [10, 20, 50, 100];
public const DEFAULT_PER_PAGE = 20;?per=100000 yazan bir istemci — çoğu zaman kötü niyetle değil, dikkatsizlikle — tek istekte tüm tabloyu ister. Bu, sunucunun belleğini de yanıtın boyutunu da patlatır.
Bu bir tavan değil, beyaz listedir ve fark önemlidir. Tavan olsaydı per=100000 sessizce 100'e çekilirdi; istemci yanlış yazdığını hiç fark etmezdi. Beyaz listede, listede olmayan HER değer varsayılana (20) döner:
| İstenen | Dönen |
|---|---|
10 · 20 · 50 · 100 | aynısı |
3 | 20 — küçük değerler de listede değil |
250 · 100000 · abc | 20 |
Tek satırlık bir in_array kontrolü, "acaba bu sayı çok mu büyük?" diye düşünmek zorunda kalmadan bütün uç durumları kapatır. İstemcinin iyi niyetine bırakılmaz.
6. Yanıt zarfı: data + meta + links
Dizi doğrudan döndürülseydi ([{...},{...}]), sayfalama bilgisi eklenecek yer kalmazdı ve bir gün eklemek kırıcı bir değişiklik olurdu.
data içeriği, meta sayımı, links gezinmeyi taşır. İstemci bir sonraki sayfanın adresini kurmaz, links.next'i takip eder — sayfalama biçimini sonradan değiştirseniz bile istemci çalışmaya devam eder.
7. Hataların da makine tarafından okunabilir bir kodu var
{
"error": {
"code": "insufficient_scope",
"message": "Bu işlem için 'write' kapsamı gerekiyor.",
"details": { "required_scope": "write", "granted": ["read"] }
}
}İstemci
message metnini karşılaştırmaz — metin dile ve sürüme göre değişir. code sabittir ve programla ele alınabilir.
8. WWW-Authenticate gönderirken durum kodu sırası önemlidir
PHP, WWW-Authenticate başlığını görünce durum kodunu kendiliğinden 401'e çevirir. 403 dönmek istediğiniz bir yerde bu başlığı önce yazarsanız yanıt sessizce 401 olur.
Bu yüzden http_response_code() başlıklardan sonra çağrılır. (Aynı davranış Location: başlığı için de geçerlidir; orada da kod sessizce 302 olur.)
Neler Var?
API katmanı
- Bearer jeton doğrulama
- Kapsam:
read/write, rota düzeyinde - Jeton başına hız sınırı + üç bilgi başlığı
- Sayfalama:
meta+links, beyaz listeliper - Tutarlı hata zarfı (
code·message·details) - Doğru HTTP kodları: 200 · 201 · 204 · 400 · 401 · 403 · 404 · 405 · 422 · 429
201ile birlikteLocationbaşlığı- ISO-8601 tarihler, zaman dilimi bilgisiyle
Panel
- Jeton üretme, kapsam seçimi
- Açık metin yalnızca bir kez gösterilir
- Son kullanım tarihi ve IP
- Ömür boyu istek sayacı (Detaylar modalında)
- İptal etme (silmeden)
- API belgeleri sayfası, kopyalanabilir
curlörnekleri - Örnek Kullanım: cURL · PHP · JavaScript · Python
- Örnek dosyayı indirme (jeton yerine yer tutucu)
Ortak altyapı
- Oturum girişi, "beni hatırla", hız sınırı, CSRF
- CSP (
script-src 'self'),X-Frame-Options: DENY - Açık / koyu tema, hesaba kayıtlı
- Mobilde alt navigasyon, yatay kaydırma yok
- Sıfır bağımlılık
API Referansı
Bütün uç noktalar Authorization: Bearer başlığı ister.
GET /api/v1/users — kullanıcıları listele · kapsam: read
Sorgu parametreleri
| Ad | Tip | Varsayılan | Açıklama |
|---|---|---|---|
page | int | 1 | Sayfa numarası |
per | int | 20 | Sayfa boyutu. Yalnızca 10, 20, 50, 100 kabul edilir; başka her değer 20'ye döner |
Örnek
curl -H "Authorization: Bearer cy_..." \
"http://localhost/rest-api-system/api/v1/users?page=2&per=10"Yanıt · 200
{
"data": [ { "id": 11, "name": "Fatma", "surname": "YILDIZ",
"email": "[email protected]", "is_active": false,
"created_at": "2025-01-15T23:58:29+03:00" } ],
"meta": { "total": 51, "per_page": 10, "current_page": 2, "last_page": 6, "from": 11, "to": 20, "has_more": true },
"links": { "self": "...?per=10&page=2",
"next": "...?per=10&page=3",
"prev": "...?per=10&page=1" }
}
GET /api/v1/users/{id} — tek kullanıcı · kapsam: read
curl -H "Authorization: Bearer cy_..." \
"http://localhost/rest-api-system/api/v1/users/11"Yanıt · 200
{ "data": { "id": 11, "name": "Fatma", "surname": "YILDIZ",
"email": "[email protected]", "is_active": false,
"created_at": "2025-01-15T23:58:29+03:00" } }Bulunamazsa 404 ve
not_found kodu döner.
POST /api/v1/users — kullanıcı oluştur · kapsam: write
curl -X POST \
-H "Authorization: Bearer cy_..." \
-H "Content-Type: application/json" \
-d '{"name":"Ayse","surname":"Yilmaz","email":"[email protected]","password":"••••••••"}' \
"http://localhost/rest-api-system/api/v1/users"Yanıt · 201 —
Location başlığı yeni kaydın adresini taşır.
{ "data": { "id": 52, "name": "Ayse", "surname": "Yilmaz",
"email": "[email protected]", "is_active": true,
"created_at": "2026-09-03T04:12:00+03:00" } }Doğrulama hatası · 422
{
"error": {
"code": "validation_failed",
"message": "Gönderilen veri geçersiz.",
"details": {
"email": ["Bu e-posta zaten kayıtlı."],
"password": ["En az 8 karakter olmalı."]
}
}
}Hata alan bazlıdır: istemci hangi kutuyu kırmızı yapacağını bilir.
PATCH /api/v1/users/{id} — kullanıcı güncelle · kapsam: write
Yalnızca gönderdiğiniz alanlar değişir (kısmi güncelleme).
curl -X PATCH \
-H "Authorization: Bearer cy_..." \
-H "Content-Type: application/json" \
-d '{"is_active":false}' \
"http://localhost/rest-api-system/api/v1/users/11"Yanıt · 200 — güncellenmiş kaydın tamamı döner.
DELETE /api/v1/users/{id} — kullanıcı sil · kapsam: write
curl -X DELETE -H "Authorization: Bearer cy_..." \
"http://localhost/rest-api-system/api/v1/users/52"Yanıt · 204 — gövde yoktur. Silme işleminde döndürülecek bir kayıt kalmadığı için
204 No Content doğru koddur.
Hata kodları
| HTTP | code | Ne zaman |
|---|---|---|
| 400 | invalid_json | Gövde JSON olarak ayrıştırılamadı |
| 401 | unauthenticated | Authorization başlığı yok veya biçimi hatalı |
| 401 | invalid_token | Jeton bulunamadı, iptal edilmiş veya hesap pasif |
| 403 | insufficient_scope | Jetonun bu işlem için kapsamı yok |
| 403 | self_delete | Kendi hesabınızı API üzerinden silemezsiniz |
| 404 | not_found | Kayıt veya uç nokta yok |
| 405 | method_not_allowed | Adres var ama bu HTTP metodu tanımlı değil |
| 409 | email_taken | Bu e-posta başka bir hesapta kayıtlı |
| 422 | validation_failed | Alan bazlı doğrulama hatası (details içinde) |
| 422 | nothing_to_update | PATCH gövdesinde güncellenecek alan yok |
| 429 | rate_limit_exceeded | Hız sınırı aşıldı (Retry-After başlığıyla) |
| 500 | server_error | Beklenmeyen hata — ayrıntı istemciye gönderilmez, log'a yazılır |
Güvenlik: Neyi, Nasıl Kapattık?
| Açık | Tipik hatalı kod | Bu projede |
|---|---|---|
| Jeton sızıntısı | Jetonu düz metin saklamak | Yalnızca SHA-256 özeti saklanır; açık metin bir kez gösterilir |
| Yetki aşımı | Tek anahtarla her şeyi yapabilmek | Kapsam (read/write), rota düzeyinde zorunlu |
| İptal edilemeyen anahtar | Süresiz JWT | Jeton sunucuda tutulur; revoked_at ile anında geçersiz olur |
| Zamanlama saldırısı | if ($hash == $gelen) | Aramanın kendisi özet üzerinden yapılır; eşitlik karşılaştırmaları hash_equals() ile |
| SQL enjeksiyonu | "... WHERE id = $id" | Tüm sorgular hazır ifade; ATTR_EMULATE_PREPARES = false |
| Aşırı veri çekme | ?per=100000 | Sayfa boyutu beyaz listeden geçer; liste dışı değer 20'ye döner |
| Kaynak tüketimi | Sınırsız istek | Jeton başına hız sınırı + 429 + Retry-After |
| Hata sızıntısı | İstisna mesajını JSON'a basmak | 500 yanıtı ayrıntı taşımaz; ayrıntı log'a yazılır |
| Yanlış durum kodu | WWW-Authenticate'ten sonra 403 denemek | http_response_code() başlıklardan sonra çağrılır |
| Sabit yol kırılması | .htaccess içinde RewriteBase /api | Sabit yol yazılmadı; Apache tabanı dizinden türetir |
| Kullanıcı sayımı | "Böyle bir e-posta yok" | Girişte hem yanlış e-postada hem yanlış parolada aynı mesaj ve aynı süre |
| Bozuk UTF-8'de sessiz JSON kaybı | json_encode($v) | JSON_INVALID_UTF8_SUBSTITUTE |
Kurulum
Gereksinimler
| PHP | 8.0 veya üzeri |
| MySQL / MariaDB | 5.7+ / 10.3+ |
| Web sunucusu | Apache (mod_rewrite) veya Nginx |
| PHP eklentileri | pdo_mysql, mbstring |
Adımlar
git clone https://github.com/CilginYazilim/rest-api-system.git
cd rest-api-system
mysql -u root -p < database.sql
cp .env.example .env # Windows: copy .env.example .envAçın:
http://localhost/rest-api-system/ · Giriş: [email protected] / Admin1234
Sonra API Jetonları sayfasından kendi jetonunuzu üretin.
Apache'deAuthorizationbaşlığı kaybolabilir. Apache, CGI/FastCGI modunda bu başlığı güvenlik gerekçesiyle PHP'ye geçirmez; sonuç, jeton doğru olsa bile her isteğin401dönmesidir. Proje.htaccessdosyası başlığı bir ortam değişkenine kopyalayarak bunu çözer:
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]PHP bunu
$_SERVER['HTTP_AUTHORIZATION'] olarak görür. Nginx + PHP-FPM kullanıyorsanız karşılığı:
fastcgi_param HTTP_AUTHORIZATION $http_authorization;
Yapılandırma
APP_DEBUG=true # silerseniz: yerelde açık, canlıda kapalı
APP_URL=
APP_PRETTY_URLS=true
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=cy_rest_api
DB_USER=root
DB_PASS=
••••••••
API davranışını belirleyen değerler koddadır:
| Değer | Yeri | Varsayılan | Ne yapar |
|---|---|---|---|
| hız sınırı | `ApiRateLimiter::__construct` | `60` | Pencere içinde izin verilen istek |
| pencere | `ApiRateLimiter::__construct` | `60` sn | Sayım penceresi |
| sayfa boyutu | `Paginator::DEFAULT_PER_PAGE` | `20` | `per` verilmezse |
| izin verilen sayfa boyutları | `Paginator::PER_PAGE_OPTIONS` | `10, 20, 50, 100` | Liste dışı her değer `DEFAULT_PER_PAGE`'e döner |
| kapsamlar | `ApiToken::SCOPES` | `read`, `write` | Tanımlı kapsam listesi |
---
## Dosya Yapısı
rest-api-system/
│
├── index.php Ön denetleyici — TEK giriş noktası
├── database.sql Şema + 51 kullanıcı + 3 jeton + istek kayıtları
├── .env.example
│
├── app/
│ ├── Core/
│ │ ├── ApiAuth.php ★ Bearer doğrulama · requireScope()
│ │ ├── ApiRateLimiter.php ★ Jeton başına sayım · X-RateLimit-* başlıkları
│ │ ├── ApiResponse.php ★ data/meta/links zarfı · hata zarfı · HTTP kodları
│ │ ├── Paginator.php Sayfalama ve beyaz liste
│ │ ├── Middleware.php 'api' ve 'scope:read|write' ara katmanları
│ │ ├── Auth.php · Session.php · Csrf.php · RateLimiter.php
│ │ ├── Database.php PDO (EMULATE_PREPARES = false)
│ │ ├── Env.php .env okuyucu + isLocalHost()
│ │ └── ...
│ │
│ ├── Http/Controllers/
│ │ ├── Api/V1/UserApiController.php ★ index · show · store · update · destroy
│ │ ├── Api/PreferenceApiController.php
│ │ ├── TokenController.php Jeton üret / iptal et
│ │ ├── ApiDocController.php API belgeleri sayfası
│ │ └── Auth · Dashboard · User
│ │
│ ├── Models/ApiToken.php SCOPE_READ · SCOPE_WRITE
│ ├── Repositories/ApiTokenRepository.php ★ create() · findByPlain() · revoke()
│ └── Support/helpers.php
│
├── views/ Düzenler, jeton ve belge sayfaları
├── assets/ css · js · images
├── config/config.php
│ ├── Support/ApiExamples.php ★ Örnek kullanım kodlarını üretir
├── routes/web.php ★ Kapsamlar burada tanımlı
└── docs/ index.html · screenshots/
---
## Nasıl Çalışıyor?
İstemci
│ GET /api/v1/users?per=10&page=2
│ Authorization: Bearer cy_9f2b...
▼
index.php → Router::dispatch()
│
▼
Rota bulundu: ['api', 'scope:read']
│
├──────────── ARA KATMAN: api ─────────────────────────────┐
│ ApiAuth::authenticate() │
│ 1. Authorization başlığı var mı? yoksa → 401 │
│ 2. hash('sha256', $jeton) │
│ 3. SELECT ... WHERE token_hash = ? AND revoked_at IS NULL
│ bulunamadı / hesap pasif → 401 invalid_token │
│ 4. last_used_at, last_used_ip güncellenir, │
│ request_count += 1 (atomik) │
│ │
│ ApiRateLimiter::check($tokenId, $ip) │
│ SELECT COUNT(*) FROM api_requests │
│ WHERE token_id = ? AND requested_at >= NOW()-60sn │
│ sayı >= 60 → 429 + Retry-After │
│ INSERT INTO api_requests (...) │
│ X-RateLimit-Limit / -Remaining / -Reset başlıkları │
└──────────────────────────────────────────────────────────┘
│
├──────────── ARA KATMAN: scope:read ──────────────────────┐
│ ApiAuth::requireScope('read') │
│ jetonun kapsamları arasında yoksa → 403 │
│ insufficient_scope + details{required, granted} │
└──────────────────────────────────────────────────────────┘
│
▼
UserApiController::index()
│ Paginator: per beyaz listeden gecer (10/20/50/100)
│ UserRepository::page($offset, $limit)
▼
ApiResponse::collection($items, $paginator, 'api/v1/users')
│
▼
{ "data": [...], "meta": {...}, "links": {...} }
json_encode(..., JSON_INVALID_UTF8_SUBSTITUTE)
---
## Veritabanı Şeması
### `api_tokens`
| Sütun | Tip | İşi |
|---|---|---|
| `id` | INT UNSIGNED | Birincil anahtar |
| `user_id` | INT UNSIGNED | Jeton kimin adına çalışıyor (`ON DELETE CASCADE`) |
| `name` | VARCHAR(100) | "Mobil uygulama" gibi tanıtıcı ad |
| `token_hash` | CHAR(64) | **SHA-256 özeti** — açık metin hiçbir yerde saklanmaz |
| `scopes` | VARCHAR(100) | Virgülle ayrılmış kapsamlar (`read,write`) |
| `last_used_at` · `last_used_ip` | DATETIME · VARCHAR(45) | Son kullanım — şüpheli hareketi görmenin en kolay yolu |
| `request_count` | INT UNSIGNED | **Ömür boyu** istek sayısı. `api_requests`'ten değil bu sütundan okunur — o tablo yalnızca hız sınırı penceresidir ve bir saat sonra silinir (aşağıya bakın); toplamı oradan üretmek zamanla küçülen, yanlış bir sayı verirdi. Her istekte `request_count = request_count + 1` ile **atomik** artırılır |
| `revoked_at` | DATETIME | Dolu ise jeton geçersiz; satır **silinmez** |
| `created_at` | DATETIME | Üretilme anı |
### `api_requests` — hız sınırı penceresi
| Sütun | Tip | İşi |
|---|---|---|
| `id` | BIGINT UNSIGNED | Birincil anahtar |
| `token_id` | INT UNSIGNED | Hangi jeton (`ON DELETE CASCADE`) |
| `ip` | VARCHAR(45) | İsteği yapan adres (IPv6 için 45 karakter) |
| `requested_at` | DATETIME | İstek anı (indeksli) |
| Karar | Neden |
|---|---|
| Sayım **jeton başına** | IP başına sayım aynı ofisteki yüz kullanıcıyı haksız yere engeller, IP değiştiren saldırganı ise hiç engellemez |
| Bir saatten eski satırlar silinir | Tablo hız sınırı penceresidir, arşiv değildir; sonsuza kadar büyümemeli |
| `revoked_at`, satır silme yerine | Silinen jetonun istek kayıtları da giderdi; olay incelemesinde tam o kayıtlara bakılır |
| `scopes` metin sütunu, ayrı tablo değil | Kapsam sayısı iki; ilişki tablosu her istekte fazladan bir `JOIN` maliyeti demek olurdu |
| `token_hash` `CHAR(64)` | SHA-256 çıktısı her zaman 64 onaltılık karakterdir; sabit uzunluk hem küçük hem hızlıdır |
---
## SSS
<details>
<summary><b>Neden JWT kullanmadınız?</b></summary>
JWT'nin asıl faydası **durumsuz** olmasıdır: sunucu jetonu saklamaz, imzasını doğrular ve geçer. Bu, birden çok servise dağılmış mimarilerde çok değerlidir.
Ama aynı özellik en büyük dezavantajını doğurur: **iptal edilemez**. Sızan bir JWT, süresi dolana kadar geçerlidir. Bunu çözmek için bir "iptal listesi" tutarsınız — ve o an durumsuzluğu kaybedersiniz.
Tek uygulamalı bir sistemde jetonu veritabanında tutmanın maliyeti bir indeksli sorgudur; karşılığında **anında iptal**, kapsam yönetimi ve son kullanım bilgisi elde edersiniz. Bu değiş tokuş burada mantıklıdır.
JWT örneği isterseniz kütüphanedeki [JWT ile REST API](https://cilginyazilim.com/kutuphane/php-rest-api-jwt) örneğine bakın.
</details>
<details>
<summary><b>Jetonu kaybettim, nereden görebilirim?</b></summary>
Göremezsiniz — bu bilerek böyledir. Veritabanında yalnızca özet vardır ve özetten açık metni geri üretmek mümkün değildir.
Yeni bir jeton üretin ve eskisini iptal edin. Bu, "jetonumu unuttum" durumunun sızıntıdan ayırt edilemediği her sistemde doğru davranıştır.
</details>
<details>
<summary><b>Her istek `401` dönüyor, jeton doğru</b></summary>
Neredeyse her zaman `Authorization` başlığının PHP'ye ulaşmamasıdır. Apache bazı yapılandırmalarda bu başlığı düşürür.
Proje `.htaccess` dosyası başlığı `RewriteRule ... [E=HTTP_AUTHORIZATION:...]` ile bir ortam değişkenine kopyalayarak çözer. Kendi sunucunuzda dosyanın okunduğundan emin olun (`AllowOverride All`). Nginx + PHP-FPM için:
```nginx
fastcgi_param HTTP_AUTHORIZATION $http_authorization;Doğrulamak için başlığı geçici olarak yazdırın:
var_dump($_SERVER['HTTP_AUTHORIZATION'] ?? 'YOK');
Hız sınırını nasıl değiştiririm?
ApiRateLimiter yapıcısındaki iki değeri değiştirin: $limit (istek sayısı) ve $window (saniye).
Farklı jetonlara farklı sınır vermek isterseniz api_tokens tablosuna bir rate_limit sütunu ekleyin ve sınırlayıcıya o değeri geçirin — kodun geri kalanı değişmez.
Yeni bir uç nokta nasıl eklerim?
İki adım:
- Denetleyiciye metodu yazın (
app/Http/Controllers/Api/V1/). - Rotayı kapsamıyla birlikte tanımlayın:
$router->get('api/v1/siparisler', SiparisApiController::class, 'index', ['api', 'scope:read']);Kapsamı yazmayı unutursanız uç nokta jeton ister ama kapsam denetlemez. Rota tablosunu gözden geçirirken bu göze çarpar — kapsamı denetleyicinin içine gizlemek yerine rotada tutmanın sebebi tam olarak budur.
Dikkat: sabit yollar (api/v1/users) desenli yollardan (api/v1/users/{id}) önce tanımlanmalıdır.
API sürümünü nasıl yükseltirim?
api/v2/... rotalarını ekleyin ve denetleyicileri Api/V2/ altına koyun. v1 çalışmaya devam eder.
Sürüm numarasını adrese koymanın sebebi budur: istemciler kendi hızlarında geçer. v1'i kapatacağınız tarihi önceden duyurun ve api_tokens.last_used_at ile kimin hâlâ eski sürümü kullandığını görün.
Canlı Ortama Alırken
- [ ]
.enviçindeAPP_DEBUG=false(veya satırı tümüyle silin) - [ ] HTTPS zorunlu olsun — Bearer jeton düz HTTP'de açıkça taşınır
- [ ] Hız sınırını trafiğinize göre ayarlayın
- [ ]
Authorizationbaşlığının PHP'ye ulaştığını doğrulayın - [ ] Demo jetonlarını iptal edin veya silin
- [ ]
api_requeststablosunun temizlendiğini doğrulayın (bir saatlik pencere) - [ ] Veritabanı için root olmayan bir kullanıcı açın
- [ ]
config/,app/,routes/,views/klasörlerinin.htaccessdosyaları yerinde mi? - [ ] Demo hesaplarının parolalarını değiştirin veya hesapları silin
Sorun Giderme
| Belirti | Sebep | Çözüm |
|---|---|---|
Her istek 401 | Authorization başlığı PHP'ye ulaşmıyor | CGIPassAuth On / fastcgi_param HTTP_AUTHORIZATION |
403 insufficient_scope | Jetonun kapsamı yetmiyor | write kapsamlı yeni jeton üretin |
429 sürekli geliyor | Hız sınırı aşıldı | Retry-After kadar bekleyin veya sınırı yükseltin |
404 — uç nokta var ama bulunmuyor | Desenli rota sabit rotadan önce tanımlanmış | routes/web.php içinde sırayı düzeltin |
405 method_not_allowed | Adres doğru, metot tanımlı değil | Rota tablosunda o metot var mı bakın |
| Tarihler saatsiz geliyor | Zaman dilimi ayarı | config/config.php → app.timezone |
| Türkçe karakterler bozuk | Bağlantı karakter kümesi | Database.php içinde charset=utf8mb4 olduğunu doğrulayın |
| Tüm adresler 404 | mod_rewrite kapalı | Açın veya APP_PRETTY_URLS=false yapın |
Yol Haritası
- [ ] Jeton başına ayarlanabilir hız sınırı
- [ ] Jetona son kullanma tarihi (
expires_at) - [ ]
If-None-Match/ETagile koşullu istek - [ ] Webhook giden çağrıları (imzalı)
- [ ] OpenAPI (Swagger) tanım dosyası üretimi
Katkı
Hata bildirimi ve öneriler için issue açabilirsiniz.
Lisans
MIT — ticari projelerinizde de özgürce kullanabilirsiniz.
Kaynak Kod soldaki ağaçtan bir dosya seçin
-
app
-
Core
- ApiAuth.php 4.2 KB
- ApiRateLimiter.php 4.4 KB
- ApiResponse.php 8.5 KB
- Auth.php 13.9 KB
- Autoloader.php 2.2 KB
- Config.php 1.9 KB
- Csrf.php 2.8 KB
- Database.php 3.5 KB
- Env.php 4.3 KB
- Flash.php 2.6 KB
- Middleware.php 3.2 KB
- Paginator.php 8.6 KB
- RateLimiter.php 3.9 KB
- Request.php 8.1 KB
- Response.php 7 KB
- Router.php 11.3 KB
- Session.php 6.3 KB
- Validator.php 8.8 KB
- View.php 3.5 KB
-
Http
-
Controllers
-
Api
-
V1
- UserApiController.php 9.5 KB
- PreferenceApiController.php 2.5 KB
-
- ApiDocController.php 5.7 KB
- AuthController.php 3.2 KB
- DashboardController.php 1.2 KB
- TokenController.php 6.4 KB
- UserController.php 3.1 KB
-
- Controller.php 1.3 KB
-
-
Models
- ApiToken.php 3.5 KB
- User.php 8.1 KB
-
Repositories
- ActivityRepository.php 3.8 KB
- ApiTokenRepository.php 7.9 KB
- RememberTokenRepository.php 5.8 KB
- UserRepository.php 11.1 KB
-
Support
- ApiExamples.php 20.5 KB
- helpers.php 12.5 KB
- .htaccess 851 B
-
-
assets
-
css
- admin.css 58.6 KB
- bootstrap.min.css 227.5 KB
- cilginyazilim.css 29.8 KB
- feature.css 6.6 KB
-
images
- logo.png 70.4 KB
-
js
- app.js 20.2 KB
- bootstrap.bundle.js 203.2 KB
- jquery-3.7.0.js 278.3 KB
- login.js 1.8 KB
- tokens.js 6.6 KB
- users.js 4.7 KB
-
-
config
- .htaccess 196 B
- config.php 5 KB
- menu.php 1.2 KB
-
docs
-
screenshots
- 01-giris.png 371.3 KB
- 02-kontrol-paneli.png 71.4 KB
- 03-api-jetonlari.png 72.4 KB
- 04-api-belgeleri.png 17.9 KB
- 05-koyu-tema.png 72.5 KB
- 06-mobil.png 41.1 KB
- index.html 3.7 KB
-
-
routes
- .htaccess 197 B
- web.php 5.5 KB
-
views
-
api
- docs.php 8 KB
-
auth
- login.php 7.4 KB
-
dashboard
- _feature.php 1.5 KB
- index.php 4 KB
-
errors
- 404.php 844 B
- 500.php 565 B
-
layouts
- admin.php 6.8 KB
- auth.php 2.9 KB
- plain.php 1.3 KB
-
partials
- api_examples.php 4.2 KB
- bottomnav.php 2.1 KB
- head_extra.php 489 B
- pagination.php 4.9 KB
- sidebar.php 2.7 KB
- topbar.php 3.5 KB
-
tokens
- index.php 15.2 KB
-
users
- index.php 6.8 KB
- .htaccess 262 B
-
- .gitattributes 2.3 KB
- .gitignore 1.2 KB
- .htaccess 7 KB
- CHANGELOG.md 10.8 KB
- database.sql 21.1 KB
- index.php 5.8 KB
- LICENSE 1.1 KB
- README.en.md 34.5 KB
- README.md 35.8 KB
Güvenlik gereği kaynak dosyalardaki parola, API anahtarı ve benzeri gizli
değerler gösterilmeden önce maskelenir (••••••••).
Sık Sorulan Sorular
JWT'nin asıl faydası durumsuz olmasıdır ve bu, birden çok servise dağılmış mimarilerde çok değerlidir. Ama aynı özellik en büyük dezavantajını doğurur: iptal edilemez. Sızan bir JWT süresi dolana kadar geçerlidir; bunu çözmek için iptal listesi tutarsanız durumsuzluğu zaten kaybedersiniz. Tek uygulamalı bir sistemde jetonu veritabanında tutmanın maliyeti bir indeksli sorgudur.
Göremezsiniz ve bu bilerek böyledir. Veritabanında yalnızca özet vardır ve özetten açık metni geri üretmek mümkün değildir. Yeni bir jeton üretip eskisini iptal edin. Bu, "jetonumu unuttum" durumunun sızıntıdan ayırt edilemediği her sistemde doğru davranıştır.
Neredeyse her zaman Authorization başlığının PHP'ye ulaşmamasıdır; Apache CGI/FastCGI modunda bu başlığı düşürür. Proje .htaccess dosyası başlığı bir ortam değişkenine kopyalayarak çözer. Kendi sunucunuzda dosyanın okunduğundan emin olun. Nginx + PHP-FPM için fastcgi_param HTTP_AUTHORIZATION satırını ekleyin.
Depoda duran bir jeton, projeyi indiren herkesin bildiği bir jeton demektir; herkese açık bir kapı olurdu. Kendi jetonunuzu panelden üretin. Örnek satırlar yalnızca listenin bütün durumlarını (etkin, salt okunur, iptal edilmiş) göstermek için vardır ve açık metinleri hiçbir yerde yazılı değildir.
ApiRateLimiter yapıcısındaki iki değeri değiştirin: istek sayısı ve pencere uzunluğu. Farklı jetonlara farklı sınır vermek isterseniz api_tokens tablosuna bir rate_limit sütunu ekleyip sınırlayıcıya o değeri geçirin; kodun geri kalanı değişmez.
İki adım: denetleyiciye metodu yazın ve rotayı kapsamıyla birlikte tanımlayın. Kapsamı yazmayı unutursanız uç nokta jeton ister ama kapsam denetlemez; rota tablosunu gözden geçirirken bu göze çarpar. Dikkat: sabit yollar desenli yollardan önce tanımlanmalıdır.




Yorumlar 0 konuşma
Bu kod örneğine henüz yorum yapılmamış. Takıldığınız bir yer veya merak ettiğiniz bir ayrıntı varsa ilk soruyu siz sorun.
Soru Sor veya Yorum Yaz
Yorumunuz onaylandıktan sonra yayınlanır. Teknik sorularınıza ekibimiz yanıt verir.