Her SaaS projesi, kimlik doğrulama ile başlar. Geliştiriciler, her seferinde aynı seçimle karşı karşıya gelir: bir başlangıç kiti kullanmak ve bir ay içinde ondan çıkmak mı, yoksa sıfırdan inşa etmek ve temel ürünü olmayan bir şey üzerinde haftalar harcamak mı.
Ben de Kohana.io – küçük işletmeler için bir SaaS CRM/ERP – üzerinde çalıştım ve bu döngüden sıkıldım. Bu nedenle, temel e-posta girişinden QR kodu kimlik doğrulamasına ve gerçek zamanlı yönetici güvenlik uyarılarına kadar her şeyi yöneten kapsamlı bir kimlik doğrulama modülü inşa ettim.
Şimdi bunu LaraFoundry – Laravel için açık kaynaklı bir SaaS çerçevesi – içine aktarıyorum, böylece kimsenin bunu yeniden inşa etmesi gerekmeyecek.
Peki, bu nasıl çalışıyor?
Kimlik Doğrulama Yığını
Kimlik Doğrulama Yığını
| Yöntem | Kullanım Durumu |
|---|---|
| Email/Şifre | Standart giriş, hız sınırlama ile |
| OAuth (Google, Facebook, Twitter) | Tek tıklamayla sosyal giriş |
| QR Kod | Cihazlar arası giriş (telefonla tarama) |
| PIN Kodu | Paylaşılan iş istasyonları için oturum kilidi |
| 2FA (TOTP) | Yönetici hesabı koruması |
| IP Beyaz Listeleme | Yönetici erişim kısıtlaması |
Teknoloji yığını: Laravel 12, Inertia.js v2, Vue 3, Özel SCSS
Ana paketler: Laravel Socialite v5, PragmaRX Google2FA, SimpleSoftwareIO QrCode, Jenssegers Agent
1. E-posta/Şifre Kimlik Doğrulama
1. E-posta/Şifre Kimlik Doğrulama
Temel. Ancak önemli üretim kalitesindeki eklemelerle.
Hız Sınırlama
Hız Sınırlama
Giriş denemeleri, e-posta+IP kombinasyonu başına 5 ile sınırlıdır:
// LoginRequest.php
public function ensureIsNotRateLimited(): void
{
if (!RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout($this));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
public function throttleKey(): string
{
return Str::transliterate(
Str::lower($this->string('email')) . '|' . $this->ip()
);
}
Oturum Takibi
Oturum Takibi
Başarılı her giriş, detaylı bir oturum kaydı oluşturur:
$request->user()->userSessions()->create([
'session_id' => $request->session()->getId(),
'ip_address' => $request->getClientIp(),
'user_agent' => $request->userAgent(),
'last_activity' => now(),
'pin_locked' => false,
'user_device_type' => self::getUserDeviceType(),
'user_device_name' => Agent::device(),
'user_os' => Agent::platform(),
'user_browser' => Agent::browser(),
]);
Bu, kullanıcılara tüm giriş yaptıkları cihazları görebilecekleri bir “Aktif Oturumlar” görünümü sağlar ve tanımadıkları oturumları temizlemelerine olanak tanır – Google ve GitHub’ın sunduğu gibi.
Beni Hatırla
Beni Hatırla
Hem e-posta hem de OAuth girişi için çalışır. OAuth için, hatırlama tercihleri yönlendirmeden önce oturumda saklanır ve geri dönüşten sonra uygulanır:
// OAuth yönlendirmeden önce
if ($request->boolean('oauth_remeber_me')) {
session(['oauth_remember_me' => true]);
}
// OAuth geri dönüşünden sonra
$remember = session('oauth_remember_me', false);
Auth::login($user, $remember);
2. OAuth Kimlik Doğrulaması
2. OAuth Kimlik Doğrulaması
Kutudan çıkan üç sağlayıcı: Google, Facebook ve Twitter. Laravel Socialite v5 ile desteklenmektedir.
public function callback(string $provider, UserLogoService $userLogo)
{
if (!in_array($provider, ['google', 'facebook', 'twitter'])) {
return redirect()->route('guest_index')
->with('message-error', 'Invalid OAuth provider');
}
$socialUser = Socialite::driver($provider)->user();
$user = User::updateOrCreate(
['email' => $socialUser->email],
[
'provider_id' => $socialUser->id,
'provider_name' => $provider,
'name' => $socialUser->name,
'email_verified_at' => now(), // OAuth = otomatik olarak onaylı
'provider_token' => $socialUser->token,
'provider_refresh_token' => $socialUser->refreshToken,
'last_login_at' => now(),
'last_activity_at' => now,
]
);
Auth::login($user, $remember);
}
Ana Tasarım Kararları:
- E-posta benzersiz tanımlayıcıdır – Bir kullanıcı e-posta ile kaydolduğunda ve daha sonra aynı e-posta ile Google ile giriş yaptığında, hesap otomatik olarak bağlanır.
- OAuth e-postaları otomatik olarak onaylıdır – Google zaten onayladığından onay e-postası göndermeye gerek yoktur.
- Sağlayıcı token’ları saklanır – Gelecek entegrasyonlar için yararlıdır (örneğin, Google’dan rehber alma).
- Oturum başına giriş yöntemi izlenir – Böylece “Aktif Oturumlar” görünümü, kullanıcının yerel olarak mı yoksa OAuth ile mi giriş yaptığını gösterir.
3. QR Kod Kimlik Doğrulaması
3. QR Kod Kimlik Doğrulaması
En çok gurur duyduğum özellik. WhatsApp Web gibi çalışır: zaten kimliği doğrulanmış telefonunuzdan bir QR kodunu tarayarak masaüstünde giriş yapın.
Mimarisi
Mimarisi
┌──────────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Masaüstü Tarayıcı │ │ Sunucu │ │ Mobil (Kimliği Doğrulanmış) │
│ (Ziyaretçi) │ │ │ │ │
├──────────────────┤ ├──────────────┤ ├──────────────────┤
│ 1. QR İsteği │───────>│ 2. Oluştur │ │ │
│ ││ │ │ │ │
│ │ │ ││ 7. "Başarılı" Göster │
│ 8. Anket algılar │
Token Üretimi
Token Üretimi
public function codeGenerate()
{
$token = Uuid::uuid4();
session()->put('token', Crypt::encrypt($token));
$signInRequest = SignInRequest::create([
=> $token,
=> now()->addMinutes(5),
=> request()->ip(),
=> request()->userAgent(),
]);
session()->put(, Crypt::encrypt($signInRequest->id));
$qrCodeUrl = route(, [
=> Crypt::encrypt($signInRequest->id),
=> $token,
]);
$qrCode = base64_encode(
QrCode::size(400)->format'svg')->generate($qrCodeUrl)
);
return response()->json([
=> $qrCode,
=> $signInRequest->expires_at,
]);
}
Kimliği Doğrulanmış Cihazdan Onay
Kimliği Doğrulanmış Cihazdan Onay
public function verifyLogin(string $id, string $token)
{
// Yöneticiler QR girişini onaylayamaz (güvenlik)
if (auth()->user()->is_admin) {
return response()->json([
=> ], 404);
}
$signInRequest = SignInRequest::query()
->where(, , now())
->where(, Crypt::decrypt($id))
->where(, $token)
->first();
if (!$signInRequest) {
return response()->json([
=> ], 404);
}
$signInRequest->update([
=> auth()->id(),
=> true,
=> now(),
=> request()->ip(),
=> request()->userAgent(),
]);
return response()->json([ ], 200);
}
Tarayıcı Anketi
Tarayıcı Anketi
public function pollLogin()
{
$signInRequest = SignInRequest::query()
->where(, Crypt::decrypt(session()))
->where(, Crypt::decrypt(session()))
->where(, , now())
->first();
if ($signInRequest &&$signInRequest->approved) {
Auth::login(User::find($signInRequest->user_id));
->session()->regenerate();
// Oturum kaydı oluştur...
return response()->json([=> true]);
}
return response()->json([=> false]);
}
Güvenlik önlemleri:
- Token’lar 5 dakikada bir sona erer
- İstek kimlikleri şifrelenir – URL’lerde asla ham olarak ifşa edilmez
- Yöneticiler QR girişlerine onay veremez (yetki yükselmesini önler)
- Hem istek yapan hem de onaylayan cihazlar parmak izi ile tanımlanır
- Giriş sonrası oturum yeniden oluşturulur
4. PIN Kodu Ekran Kilidi
4. PIN Kodu Ekran Kilidi
Bu özellik, gerçek bir ihtiyaçtan doğmuştur. Kohana.io’da, depo işçileri ve satış yöneticileri sık sık paylaşılan bilgisayarlar kullanıyor. Her mola sonrası çıkış yapıp geri giriş yapmak zor. Oturum zaman aşımı, kaydedilmemiş çalışmaları yok ediyor.
Nasıl Çalışır
Nasıl Çalışır
Kullanıcılar profillerinden 4 haneli bir PIN etkinleştirir:
public function enable(Request $request)
{
$request->validate([=> ]);
$request->user()->update([
=> bcrypt(->pin)
]);
}
CheckPinLockMiddleware her istekte çalışır:
public function handle(Request $request, Closure $next)
{
$user = $request->user();
if (!$user || !$user->pin_code) {
return $next($request);
}
$session = $user->userSessions()
->where(, $request->session()->getId())
->first();
// Zaten DB'de kilitli mi?
if ($session && $session->pin_locked) {
return redirect()->route();
}
// Aktif olmayan süre kontrolü
= config(, 1800);
if ($session && $session->last_activity
->diffInSeconds(now()) > ) {
$session->update([=> true]);
return redirect()->route();
}
$session?->update([=> now()]);
return $next($request);
}
Neden Bu, Oturum Zaman Aşımından Daha İyi
Neden Bu, Oturum Zaman Aşımından Daha İyi
| Özellik | Oturum Zaman Aşımı | PIN Kilidi |
|---|---|---|
| Açma Hızı | Tam şifre + olası 2FA | 4 haneli |
| Oturum Verisi | Kaybolur | Korunur |
| Kayıtlı Olmayan Çalışma | Gider | Güvende |
| Cihaz Başına Kontrol | Hayır | Evet |
| Arka Planda İsteklerle Atlatma | Kolay | İmkansız (DB’de saklanır) |
Kilitleme durumu veritabanında saklanır, oturumda değil. Bu sayede arka plan API çağrıları veya WebSocket bağlantıları istemeyerek de olsa etkinlik zaman aşımı sayacını sıfırlayamaz.
5. Yönetici Güvenliği – 3 Katmanlı Sistem
5. Yönetici Güvenliği – 3 Katmanlı Sistem
Katman 1: IP Beyaz Listeleme
Katman 1: IP Beyaz Listeleme
private function checkAdminByIP(): bool
{
if (empty(config())) {
return true;
}
$allowedIps = explode(, config());
return in_array(request()->ip(), );
}
Yanlış IP? Anında zorla çıkış. Hata sayfası yok. GetVisitorStatusAction 'forcelogout' döner ve kontrolör bunu işler:
if ((->getVisitorStatus)() === ) {
return app(AuthenticatedSessionController::class)
->destroy(request());
}
Katman 2: Google Authenticator 2FA
Katman 2: Google Authenticator 2FA
E-posta + şifre girişinden sonra, yöneticiler bir 2FA ekranı görür. Require2FA middleware bu durumu tüm yönetici rotalarında zorunlu kılar:
public function handle(Request $request, Closure $next)
{
if (->getVisitorStatus)()===) {
if (!session()) {
if (!->routeIs()) {
return Inertia::location(route());
}
}
}
return ();
}
Katman 3: Gerçek Zamanlı Bildirimler
Katman 3: Gerçek Zamanlı Bildirimler
Her başarısız yönetici giriş denemesi, hem e-posta hem de Telegram bildirimini tetikler:
public static function notifyAdminOnLoginFail(= )
{
Notification::route(, config())
->route(, config())
->notify(new AdminLoginAttemptNotification());
}
Bildirimin içinde: IP + coğrafi konum, cihaz parmak izi, kullanıcı aracı ve hangi adımın başarısız olduğu bilgileri yer alır (giriş, 2FA veya IP).
Tetikleme Noktaları:
- Yönetici e-posta ile yanlış şifre
- Yanlış 2FA kodu
- Doğru kimlik bilgileri ancak yetkisiz IP
6. Ziyaretçi Durum Sistemi
6. Ziyaretçi Durum Sistemi
GetVisitorStatusAction, her şeyi bir araya getiren merkezi parçadır:
public function __invoke(): string
{
if (!auth()->check()) return ;
$user = auth()->user();
if ($user->is_admin && $this->checkAdminByEmail()) {
if ($this->checkAdminByIP()) {
return ;
} else {
AdminHelper::notifyAdminOnLoginFail();
return ;
}
}
if ($user->user_blocked_at) return ;
if ($user->user_deleted_at) return ;
return ;
}
Altı olası durum vardır. Middleware’ler, kontrolörler ve ön uçta Inertia paylaşılan veriler ile kullanılır. Sistemdeki her bileşen, mevcut kullanıcının kim olduğunu ve ne yapabileceğini tam olarak bilir.
Rota Mimarisi
Rota Mimarisi
// Ziyaretçi rotaları
Route::middleware()->group(function () {
Route::post(, [RegisteredUserController::class, ]);
Route::post(, [AuthenticatedSessionController::class, ]);
// OAuth
Route::controller(OAuthLoginController::class)
->prefix()->name()->group(function () {
Route::get(, );
Route::get(, );
});
// Şifre sıfırlama
Route::controller(ResetPasswordController::class)
->prefix()->name()->group(function () {
Route::post(, );
Route::get(, );
Route::post(, );
});
});
// QR Kod (karma middleware)
Route::controller(LoginWithQrCode::class)
->prefix()->name()->group(function () {
Route::post(, )->middleware();
Route::post(, )->middleware();
Route::get(, )
->middleware([, ]);
});
// Kimlik doğrulanmış rotalar
Route::middleware([])->group(function () {
// E-posta doğrulama
Route::controller(EmailVerificationController::Orijinal Makale


