· Admin

Laravel 12'de Sıfırdan Login ve Register Sistemi

Laravel'de authentication yapmak için Breeze, Jetstream, Fortify gibi hazır paketler var. Çoğu projede bunları kullanırsınız — 5 dakikada kurulur, güvenlidir, test edilmiştir. Ama bazen kendi auth sisteminizi yazmak istersiniz: paketin getirdiği dosya yığınını istemezsiniz, tamamen özel bir akış lazımdır, ya da sadece nasıl çalıştığını anlamak istersiniz.

Bu yazıda Laravel 12'de sıfırdan login, register, dashboard ve logout akışını yazacağız. Paket yok, scaffold yok — her şey elle.

Neden Kendi Auth'umu Yazayım?

Dürüst olmak gerekirse: çoğu projede yazmamalısınız. Breeze gayet yeterli. Ama şu durumlarda kendiniz yazarsınız:

  • Admin paneli için ayrı, basit bir auth lazım (Breeze'in view'ları gereksiz)
  • API-only projede sadece token auth var, web auth'a ihtiyaç yok ama bir admin girişi lazım
  • Mevcut bir kullanıcı tablosuna (legacy DB) bağlanmanız gerekiyor
  • Öğrenmek istiyorsunuz — en geçerli sebep

Route'lar

Önce ihtiyacımız olan endpoint'leri tanımlayalım:

use App\Http\Controllers\AuthController;

// Guest route'ları — giriş yapmamış kullanıcılar için
Route::middleware('guest')->group(function () {
    Route::get('login', [AuthController::class, 'showLogin'])->name('login');
    Route::post('login', [AuthController::class, 'login']);
    Route::get('register', [AuthController::class, 'showRegister'])->name('register');
    Route::post('register', [AuthController::class, 'register']);
});

// Auth route'ları — giriş yapmış kullanıcılar için
Route::middleware('auth')->group(function () {
    Route::get('dashboard', [AuthController::class, 'dashboard'])->name('dashboard');
    Route::post('logout', [AuthController::class, 'logout'])->name('logout');
});

Birkaç önemli detay:

  • guest middleware'i: Giriş yapmış kullanıcı login sayfasına giremesin. Yoksa dashboard'dayken /login'e gidince garip durumlar oluşur.
  • Logout POST olmalı, GET değil. GET ile logout yapmak CSRF açığı yaratır — birisi <img src="/logout"> koyar, siz o sayfayı açtığınızda çıkış yaparsınız.
  • ->name('login') önemli — Laravel auth middleware'i bu isme bakarak redirect yapıyor.

Controller

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;

class AuthController extends Controller
{
    public function showLogin()
    {
        return view('auth.login');
    }

    public function login(Request $request)
    {
        $credentials = $request->validate([
            'email' => ['required', 'email'],
            'password' => ['required'],
        ]);

        if (Auth::attempt($credentials, $request->boolean('remember'))) {
            $request->session()->regenerate();

            return redirect()->intended('dashboard');
        }

        return back()->withErrors([
            'email' => 'E-posta veya şifre hatalı.',
        ])->onlyInput('email');
    }

    public function showRegister()
    {
        return view('auth.register');
    }

    public function register(Request $request)
    {
        $validated = $request->validate([
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'confirmed', Password::defaults()],
        ]);

        $user = User::create([
            'name' => $validated['name'],
            'email' => $validated['email'],
            'password' => Hash::make($validated['password']),
        ]);

        Auth::login($user);

        return redirect()->route('dashboard');
    }

    public function dashboard()
    {
        return view('dashboard');
    }

    public function logout(Request $request)
    {
        Auth::logout();

        $request->session()->invalidate();
        $request->session()->regenerateToken();

        return redirect('/');
    }
}

Her metodu açıklayalım:

login() — Dikkat Edilecekler

Auth::attempt() verilen credential'larla kullanıcıyı bulmaya çalışır. Şifreyi otomatik olarak Hash::check() ile karşılaştırır — DB'de bcrypt hash'i saklanır, siz plain text gönderirsiniz, Laravel karşılaştırmayı yapar.

$request->boolean('remember') — "Beni hatırla" checkbox'ı. true gelirse kullanıcıya uzun ömürlü remember token atanır.

$request->session()->regenerate()Bu satırı atlamayın. Session fixation saldırısını önler. Giriş öncesi ve sonrası farklı session ID kullanılır.

redirect()->intended('dashboard') — Kullanıcı /admin/settings'e gitmeye çalışıp login'e yönlendirildiyse, giriş yaptıktan sonra dashboard'a değil /admin/settings'e gider. UX açısından çok önemli.

withErrors(['email' => '...']) — Hangi alanın hatalı olduğunu söylüyoruz ama "şifre yanlış" veya "kullanıcı bulunamadı" gibi spesifik hata vermiyoruz. Bu bilinçli — saldırgana hangi bilginin doğru olduğunu söylememek için.

register() — Şifre Validasyonu

Password::defaults() kullanıyoruz. Bunu AppServiceProvider'da tanımlarsınız:

use Illuminate\Validation\Rules\Password;

public function boot(): void
{
    Password::defaults(function () {
        return Password::min(8)
            ->letters()
            ->numbers()
            ->uncompromised();
    });
}

uncompromised() şifreyi Have I Been Pwned API'sine karşı kontrol eder. "123456", "password" gibi sızdırılmış şifreler reddedilir. Production'da mutlaka aktif olsun.

'password' => ['required', 'confirmed', ...]confirmed kuralı, formda password_confirmation alanını arar ve eşleşmesini kontrol eder.

logout() — Üç Adım

Auth::logout();                          // Kullanıcı oturumunu kapat
$request->session()->invalidate();       // Session'ı tamamen yok et
$request->session()->regenerateToken();  // CSRF token'ı yenile

Sadece Auth::logout() yeterli değil. Session'ı invalidate etmezseniz eski session ID hâlâ geçerli kalabilir. Token'ı yenilemezseniz eski CSRF token'ı ile istek atılabilir.

View'lar

Basit ama işlevsel Blade template'leri. Tailwind kullanıyorum ama siz istediğiniz CSS ile yapabilirsiniz.

resources/views/auth/login.blade.php

<!DOCTYPE html>
<html lang="tr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Giriş Yap</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
    <div class="bg-white p-8 rounded-lg shadow-md w-full max-w-md">
        <h1 class="text-2xl font-bold mb-6 text-center">Giriş Yap</h1>

        @if($errors->any())
            <div class="bg-red-50 text-red-600 p-3 rounded mb-4 text-sm">
                {{ $errors->first() }}
            </div>
        @endif

        <form method="POST" action="{{ route('login') }}">
            @csrf

            <div class="mb-4">
                <label for="email" class="block text-sm font-medium text-gray-700 mb-1">E-posta</label>
                <input type="email" name="email" id="email" value="{{ old('email') }}"
                    class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
                    required autofocus>
            </div>

            <div class="mb-4">
                <label for="password" class="block text-sm font-medium text-gray-700 mb-1">Şifre</label>
                <input type="password" name="password" id="password"
                    class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
                    required>
            </div>

            <div class="mb-4 flex items-center">
                <input type="checkbox" name="remember" id="remember" class="mr-2">
                <label for="remember" class="text-sm text-gray-600">Beni hatırla</label>
            </div>

            <button type="submit"
                class="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 transition">
                Giriş Yap
            </button>
        </form>

        <p class="text-center text-sm text-gray-500 mt-4">
            Hesabınız yok mu? <a href="{{ route('register') }}" class="text-blue-600 hover:underline">Kayıt Ol</a>
        </p>
    </div>
</body>
</html>

resources/views/auth/register.blade.php

<!DOCTYPE html>
<html lang="tr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Kayıt Ol</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
    <div class="bg-white p-8 rounded-lg shadow-md w-full max-w-md">
        <h1 class="text-2xl font-bold mb-6 text-center">Kayıt Ol</h1>

        @if($errors->any())
            <div class="bg-red-50 text-red-600 p-3 rounded mb-4 text-sm">
                <ul class="list-disc list-inside">
                    @foreach($errors->all() as $error)
                        <li>{{ $error }}</li>
                    @endforeach
                </ul>
            </div>
        @endif

        <form method="POST" action="{{ route('register') }}">
            @csrf

            <div class="mb-4">
                <label for="name" class="block text-sm font-medium text-gray-700 mb-1">Ad Soyad</label>
                <input type="text" name="name" id="name" value="{{ old('name') }}"
                    class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
                    required autofocus>
            </div>

            <div class="mb-4">
                <label for="email" class="block text-sm font-medium text-gray-700 mb-1">E-posta</label>
                <input type="email" name="email" id="email" value="{{ old('email') }}"
                    class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
                    required>
            </div>

            <div class="mb-4">
                <label for="password" class="block text-sm font-medium text-gray-700 mb-1">Şifre</label>
                <input type="password" name="password" id="password"
                    class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
                    required>
            </div>

            <div class="mb-4">
                <label for="password_confirmation" class="block text-sm font-medium text-gray-700 mb-1">Şifre Tekrar</label>
                <input type="password" name="password_confirmation" id="password_confirmation"
                    class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
                    required>
            </div>

            <button type="submit"
                class="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 transition">
                Kayıt Ol
            </button>
        </form>

        <p class="text-center text-sm text-gray-500 mt-4">
            Zaten hesabınız var mı? <a href="{{ route('login') }}" class="text-blue-600 hover:underline">Giriş Yap</a>
        </p>
    </div>
</body>
</html>

Güvenlik Kontrol Listesi

Kendi auth sisteminizi yazarken şunları atlamayın:

  1. Session regenerate — Login sonrası $request->session()->regenerate()
  2. CSRF koruması — Her formda @csrf
  3. Logout POST ile — GET logout yapmayın
  4. Hata mesajları genel — "Şifre yanlış" değil, "E-posta veya şifre hatalı"
  5. Rate limiting — Login route'una throttle:5,1 middleware'i ekleyin
  6. Password::defaults()uncompromised() dahil
  7. HTTPS — Production'da mutlaka. APP_URL=https://...

Rate limiting'i eklemeyi unutanlar çok:

Route::post('login', [AuthController::class, 'login'])
    ->middleware('throttle:5,1'); // Dakikada max 5 deneme

Bu olmadan birisi brute force ile şifre deneyebilir.

Breeze vs Kendi Auth

Dürüst karşılaştırma:

Breeze Custom Auth
Kurulum süresi 5 dakika 30-60 dakika
Email doğrulama Hazır Kendiniz yazarsınız
Şifre sıfırlama Hazır Kendiniz yazarsınız
Profil düzenleme Hazır Kendiniz yazarsınız
Test coverage Tam Siz yazarsınız
Esneklik Orta Tam
Dosya sayısı 20+ view/controller Sadece ihtiyacınız olan

Gördüğünüz gibi Breeze çok şeyi hazır veriyor. Custom auth yazarsanız email doğrulama, şifre sıfırlama gibi özellikleri de kendiniz eklemeniz gerekir. Bu yüzden çoğu projede Breeze kuruyorum ve ihtiyaç duymadığım view'ları siliyorum — sıfırdan yazmaktan daha hızlı.

Ama auth'un nasıl çalıştığını bilmek önemli. Bir gün Breeze'in yapmadığı bir şey lazım olacak — OTP login, sosyal login entegrasyonu, multi-tenant auth — o zaman bu temeli bilmek hayat kurtarır.