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.

PHP 8 PDO MySQL Bootstrap 5 Ajax Oturum Girişi
Seviye
İleri
Dosya
86
Kod satırı
~21.620
Proje boyutu
1.9 MB
Veritabanı
cy_rest_api
Lisans
MIT
İnceleme
23
Beğeni
1
Yayın

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?

  1. Depoyu klonlayın veya ZIP olarak indirip web kökünüze açın
  2. database.sql dosyasını içe aktarın (veritabanını kendisi oluşturur)
  3. .env.example dosyasını .env adıyla kopyalayıp veritabanı bilgilerinizi yazın
  4. Tarayıcıdan açıp [email protected] / Admin1234 ile giriş yapın
  5. API Jetonları sayfasından kendi jetonunuzu üretip curl ile deneyin

Veritabanı şeması projedeki database.sql dosyasında.

Nasıl Çalışıyor?

Çılgın Yazılım

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.

PHP
MySQL
REST
Composer
License

🇹🇷 Türkçe · 🇬🇧 English

▶ Canlı Demo · Kaynak Kütüphanesi · cilginyazilim.com


Canlı Demo

Kurulum yok, kayıt yok, indirme yok — tarayıcınızdan 3 saniyede deneyin.

Canlı Demoyu Aç
Kaynak Kodu İncele
ZIP İndir

REST API sistemi canlı demo önizlemesi

▲ Görsele tıklayarak demoyu açabilirsiniz

Demo hesapları

RolE-postaParola
Yönetici[email protected]Admin1234
Kullanıcı[email protected]Demo1234

Demoda 60 saniyede neleri deneyebilirsiniz?

#Şunu deneyinPerde arkasında ne oluyor?
1API Jetonları sayfasında "Jeton üret" deyin, read kutusunu işaretleyinAçı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
2Jetonu kopyalayıp curl ile bir istek atıncurl -H "Authorization: Bearer " .../api/v1/users — JSON döner
3Yanıtın meta bölümüne bakınToplam kayıt, sayfa, sayfa boyutu ve toplam sayfa sayısı orada. İstemcinin sayfalamayı tahmin etmesi gerekmez
4links bölümüne bakınBir sonraki sayfanın tam adresi hazır gelir. İstemci adres kurmaz, takip eder
5?per=200 deneyin200 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
6Sadece read kapsamlı jetonla POST atmayı deneyin403 ve insufficient_scope döner. Kapsam kontrolü rotanın ara katmanında yapılır, denetleyicinin içinde unutulabilecek bir if değildir
7Yanı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
8Aynı jetonla 60'tan fazla istek atın429 ve Retry-After başlığı döner. Sayım jeton başına yapılır; başkasının trafiği sizi engellemez
9Jetonun "İptal et" düğmesine basın, sonra tekrar istek atın401. Jeton silinmez, revoked_at doldurulur — geçmiş istek kayıtları bağlı kaldığı için "hangi jeton ne yaptı" sorusu cevapsız kalmaz
10API Belgeleri sayfasını açınBütün uç noktalar, kapsamları, hata kodları ve kopyalanabilir curl örnekleri orada
11Aynı sayfadaki Örnek Kullanım bölümünden dilinizi seçincURL, 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 bir code alanı (invalid_token, insufficient_scope, validation_failed, rate_limit_exceeded) ve insan tarafından okunabilir bir message alanı vardır.

Demo alanı hakkında bilinmesi gerekenler

KonuDurum
Verilerdatabase.sql içindeki 51 kullanıcı + 3 örnek jeton + 28 istek kaydı. Gerçek kişi verisi yoktur.
Hazır jetonYoktur ve olmayacaktır. Depoda duran bir jeton, projeyi indiren herkesin bildiği bir jeton demektir. Kendinizinkini panelden üretin.
SıfırlamaDemo 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_DEBUGCanlıda kendiliğinden false — sunucu adından türetilir.
BağımlılıkSı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


Ekran Görüntüleri

GirişKontrol Paneli
Giriş ekranıKontrol paneli
API JetonlarıAPI Belgeleri
API jetonlarıAPI belgeleri
Koyu tema Koyu tema Mobil görünüm 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_9f2b7c1d5e83a04f6b91c2d7e0a5f38b4c6d9e2a1f7b30c8d5e4a9b6c3f1d827

2 · İ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/users

HTTP/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 = :id

Satı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:

İstenenDönen
10 · 20 · 50 · 100aynısı
320 — küçük değerler de listede değil
250 · 100000 · abc20

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 listeli per
  • Tutarlı hata zarfı (code · message · details)
  • Doğru HTTP kodları: 200 · 201 · 204 · 400 · 401 · 403 · 404 · 405 · 422 · 429
  • 201 ile birlikte Location baş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

AdTipVarsayılanAçıklama
pageint1Sayfa numarası
perint20Sayfa 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 · 201Location 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ı

HTTPcodeNe zaman
400invalid_jsonGövde JSON olarak ayrıştırılamadı
401unauthenticatedAuthorization başlığı yok veya biçimi hatalı
401invalid_tokenJeton bulunamadı, iptal edilmiş veya hesap pasif
403insufficient_scopeJetonun bu işlem için kapsamı yok
403self_deleteKendi hesabınızı API üzerinden silemezsiniz
404not_foundKayıt veya uç nokta yok
405method_not_allowedAdres var ama bu HTTP metodu tanımlı değil
409email_takenBu e-posta başka bir hesapta kayıtlı
422validation_failedAlan bazlı doğrulama hatası (details içinde)
422nothing_to_updatePATCH gövdesinde güncellenecek alan yok
429rate_limit_exceededHız sınırı aşıldı (Retry-After başlığıyla)
500server_errorBeklenmeyen hata — ayrıntı istemciye gönderilmez, log'a yazılır

Güvenlik: Neyi, Nasıl Kapattık?

AçıkTipik hatalı kodBu projede
Jeton sızıntısıJetonu düz metin saklamakYalnızca SHA-256 özeti saklanır; açık metin bir kez gösterilir
Yetki aşımıTek anahtarla her şeyi yapabilmekKapsam (read/write), rota düzeyinde zorunlu
İptal edilemeyen anahtarSüresiz JWTJeton 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=100000Sayfa boyutu beyaz listeden geçer; liste dışı değer 20'ye döner
Kaynak tüketimiSınırsız istekJeton başına hız sınırı + 429 + Retry-After
Hata sızıntısıİstisna mesajını JSON'a basmak500 yanıtı ayrıntı taşımaz; ayrıntı log'a yazılır
Yanlış durum koduWWW-Authenticate'ten sonra 403 denemekhttp_response_code() başlıklardan sonra çağrılır
Sabit yol kırılması.htaccess içinde RewriteBase /apiSabit 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

PHP8.0 veya üzeri
MySQL / MariaDB5.7+ / 10.3+
Web sunucusuApache (mod_rewrite) veya Nginx
PHP eklentileripdo_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 .env

Açın: http://localhost/rest-api-system/ · Giriş: [email protected] / Admin1234

Sonra API Jetonları sayfasından kendi jetonunuzu üretin.

Apache'de Authorization baş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ğin 401 dönmesidir. Proje .htaccess dosyası 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:

  1. Denetleyiciye metodu yazın (app/Http/Controllers/Api/V1/).
  2. 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

  • [ ] .env içinde APP_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
  • [ ] Authorization başlığının PHP'ye ulaştığını doğrulayın
  • [ ] Demo jetonlarını iptal edin veya silin
  • [ ] api_requests tablosunun temizlendiğini doğrulayın (bir saatlik pencere)
  • [ ] Veritabanı için root olmayan bir kullanıcı açın
  • [ ] config/, app/, routes/, views/ klasörlerinin .htaccess dosyaları yerinde mi?
  • [ ] Demo hesaplarının parolalarını değiştirin veya hesapları silin

Sorun Giderme

BelirtiSebepÇözüm
Her istek 401Authorization başlığı PHP'ye ulaşmıyorCGIPassAuth On / fastcgi_param HTTP_AUTHORIZATION
403 insufficient_scopeJetonun kapsamı yetmiyorwrite kapsamlı yeni jeton üretin
429 sürekli geliyorHız sınırı aşıldıRetry-After kadar bekleyin veya sınırı yükseltin
404 — uç nokta var ama bulunmuyorDesenli rota sabit rotadan önce tanımlanmışroutes/web.php içinde sırayı düzeltin
405 method_not_allowedAdres doğru, metot tanımlı değilRota tablosunda o metot var mı bakın
Tarihler saatsiz geliyorZaman dilimi ayarıconfig/config.phpapp.timezone
Türkçe karakterler bozukBağlantı karakter kümesiDatabase.php içinde charset=utf8mb4 olduğunu doğrulayın
Tüm adresler 404mod_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 / ETag ile 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
Dosya seçilmedi
İncelemek istediğiniz dosyayı soldaki ağaçtan seçin.

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.

Yayınlanmaz; yalnızca yanıt bildirimi için kullanılır.
En az 10 karakter.

İlgili Kod Örnekleri