💻 Bilgisayar, 💾 Programlama

Basit Bir Oturum Açma Sistemi

CİDDEN ÖNEMLİ NOT: Bu kod parçası güncel haliyle GÜVENLİK AÇIĞI İÇERMEKTEDİR. DEĞİŞİKLİK YAPMADAN KULLANMAYINIZ! Aksi halde sorumluluğun S’sini almayacağımı bizzat belirtirim.

Birkaç gün önce hem PHP Session’ların ne olduğunu ve nasıl kullanıldığını öğrenmek, hem yakuter’in anlattığı ezSQL sınıflarının nasıl kullanıldığını öğrenmek, hem de belki ileride bir projede kullanırım kalsın bir köşede dediğim için gerçekten çok basit bir oturum açma sistemi yazdım.

Sistem, girilen kullanıcı adı ve şifreyi veritabanında arıyor, eğer doğruysa oturum açıyor. Oturumun açık olup olmadığı proje esnasında is_logged() isimli fonksiyon ile kolaylıkla kontrol edilebiliyor.

Projede oturum açılıp açılmadığı kullanıcının tarayıcısına yazılan bir çerez (cookie) yerine, sunucu bazlı sessionlar tarafından belirleniyor. Bu da cookie desteklemeyen tarayıcılarda bile sorun yaratmamasını sağlayacaktır.

Proje Dosyaları

index.php
Başlangıç işlerini yapar. Kullanıcı hangi sayfayı istiyor olursa olsun bu sayfadan geçmek zorundadır.

<?php 

/* Projenin diğer dosyaları doğrudan çağrılınca çalışmamaları için */
/* Other php's of project shouldn't call directly. */
define('DUYURU',1);

/* Session */
session_start();
header('Content-Type: text/html; charset=utf-8');

/* Veritabanı ve Fonksiyonlar*/
/* Database and Functions */
require_once("./db/settings.php"); /* Genel ve veritabanı ayarlarını tutar.  General and database connection settings. */
require_once("functions.php"); /* Projenin gerekli fonksiyonlarını içerir. Functions needed by this project. */ 

/* Post-Get İşleyici: Eğer sayfaya get ya da post verileriyle gelinirse gerekli fonksiyonlara yönlendirme yapılır. */
/* Post-Get Processor: If user sent data includes POST or GET data, then user redirects to proper function. */
if (isset($_GET['action'])) {
	
	switch ($_GET['action']) {
		case 'logout':
			logout();
			break;
		case 'login':
			login();
			break;
	}
	
}
/* Post-Get İşleyici Bitti*/
/* End of Post-Get Processor */

/* Çıktı Başlangıcı */
/* Projenin bu adımından sonra istediğiniz çıktıyı alabilrisiniz. Oturum 
   açılıp açılmadığını projinizin herhangi bir noktasında is_logged() fonksiyonu 
   ile yapabilirsiniz.
   
   is_logged: Eğer oturum açıldıysa 1, oturum açılmadıysa 0 döndürür. Örnek kod
   aşağıdadır.   
*/

/* Start of Output */
/* At this stage of project. You can output whatevet you want. If you want to check
   if user logged in or not, just use is_logged() function.
   
   is_logged: If user logged returns 1, else returns 0. Sample code is bellow.
*/

if (!is_logged()) { ?>
	<form name="loginform" id="loginform" action="index.php?action=login" method="post">
		<p>
			<label>Kullanıcı adı<br />
			<input type="text" name="user" id="user" class="input" value="" size="20" tabindex="10" /></label>
		</p>
		<p>
			<label>Şifre<br />
			<input type="password" name="pass" id="pass" class="input" value="" size="20" tabindex="20" /></label>
		</p>
		<input name="key" type="hidden" value="<?php echo(md5(session_id())); ?>"/>
		<p class="submit">
			<input type="submit" id="wp-submit" value="Giriş" tabindex="100" />
		</p>
	</form>
<?php } else { ?>
	Tebrikler oturum açtın. Şimdi <a href="index.php?action=logout">kapat</a>. (:
<?php } ?>

functions.php
Proje tarafından gerekli fonksiyonları içerir.

<?php

if (!defined('DUYURU')) { 
	/* You can't reach this page directly. */
	/* Bu sayfaya doğrudan erişilemez. */
	die('Yok artık Lebron James!');
}

function is_logged() {
	/* Return values: 
		1 = LOGGED;
		0 = GUEST 
	*/
	if (isset($_SESSION['logged'])) {
		return $_SESSION['logged'];
	} else {
		return 0;
	}
}

function login() {
	/* Zaten açık oturumu bir daha açamayız. */
	/* Can't login, if person already logged in. */
	if (!$_SESSION['logged'] == 1) {
		/* Input values; 
			$_POST['user'] = user_name
			$_POST['pass'] = password
			$_POST['key'] = session in md5
		*/	
		/* Return values: 
			1: Login OK, $_SESSION['logged'] = 1;
		    -1: Login Failed.
		*/
		global $db, $prefix, $mesaj;
		
		if (!(isset($_POST['user']) && isset($_POST['pass']) && isset($_POST['key']))) {
			$mesaj = 'invalid_data';
			return -1;	
		}
		
		$session = htmlspecialchars($_POST['key']);
		$user = htmlspecialchars($_POST['user']);
		$pass = htmlspecialchars($_POST['pass']);
		
		if (htmlspecialchars(md5(session_id())) != $session) {
			$mesaj = 'session_invalid';
			return -1;
		}	
		
		if ($user == NULL || $user == "" || $pass == NULL || $pass == "") {
			$mesaj = 'invalid_data';
			return -1;
		}
		
		$sonuc = $db->get_row('SELECT pass FROM '.$prefix.'user WHERE name = "'.$user.'"');
		if (md5($pass) == $sonuc->pass && $_SESSION['logged'] != 1) {
			$sonuc = $db->get_row('SELECT ID, name, real_name, login_count FROM '.$prefix.'user WHERE name = "'.$user.'"');
			$_SESSION['name'] = $sonuc->name;
			$_SESSION['real_name'] = $sonuc->real_name;
			$_SESSION['ID'] = $sonuc->ID;
			$db->query('UPDATE '.$prefix.'user SET login_count = "'.++$sonuc->login_count.'" WHERE ID = "'.$_SESSION['ID'].'"');
			$db->query('UPDATE '.$prefix.'user SET last_login = NOW() WHERE ID = "'.$_SESSION['ID'].'"');

			$_SESSION['logged'] = 1;
			header('Location: index.php');
			return 1;
		}

		$mesaj = 'invalid_data';
		return -1;
	}
}

function logout() {
	/* Açık olmayan oturumun nesini sonlandıracağım! */
	/* Can't logoff, if person is not login. */
	if ($_SESSION['logged'] == 1) {
		global $mesaj;
		$_SESSION['logged'] = 0;
		$mesaj = 'logged_out';
	}
}
?>

db/settings.php
Veritabanı bağlantı ayarlarını içerir. ezSQL ile bağlantı kurar.

<?php

if (!defined('DUYURU')) {
	/* You can't reach this page directly. */
	/* Bu sayfaya doğrudan erişilemez. */
	die('Yasak. :)');
}

/* ez_SQL sınıfları çağırılır. */
/* ez_SQL classes */
include ("./db/core.php");
include ("./db/mysql.php");

/* Sunucu Ayarları */
$db_server = 'localhost';
$db_name = 'veritabanı_adı';
$db_user = 'veritabanı_kullanıcısı';
$db_passwd = 'veritabanı_şiresi';
$prefix = "log_"; /* Veritaanı öneki. */
$version = 0.1; /* Proje sürümü - Project version */ 

$db = new ezSQL_mysql($db_user,$db_passwd,$db_name,$db_server);

/* GEREKLİ VERİTABANI TABLOSU YAPISI - REQUIRED DATABASE TABLE STRUCTURE */

/* 
CREATE TABLE IF NOT EXISTS `log_user` (
  `ID` int(11) NOT NULL auto_increment,
  `name` text NOT NULL,
  `pass` text NOT NULL,
  `real_name` text NOT NULL,
  `last_login` datetime NOT NULL default '0000-00-00 00:00:00',
  `login_count` int(11) NOT NULL default '0',
  PRIMARY KEY  (`ID`)
) ENGINE=XtraDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=2 ;

INSERT INTO `yse_user` (`ID`, `name`, `pass`, `real_name`, `last_login`, `login_count`) VALUES
(1, 'Umut', '202cb962ac59075b964b07152d234b70', 'Umut Benzer', '2008-07-09 01:56:10', 0);

*/

/* Bu tablo ile kullanıcı adı Umut şifre 123 olan bir test kullanıcısı yaratılır. */
/* With this table a test user created with username Umut pass 123 */

?>

Projenin tüm dosyalarını (ve ezSQL sınıflarını) buradan indirebilirsiniz. Dosya, veritabanı tablosunu yaratmak için gerekli SQL satırlarını da içerir.

Sistem Gereksinimleri: PHP 4+, MySQL

Basit Bir Oturum Açma Sistemi 5 yorum aldı.

  1. Sevgili Umut,

    Yazdığın kodu denedim, çalışıyor. Tabii ki en iyi kod çalışan koddur. Fakat ölçeklenebilirlik, genel yazıma uyum ve en önemlisi güvenlik açısından belirtmem gereken bazı noktalar var. Kodu satır satır inceledim. Umarım yazacaklarımı “vasıfsız eleştiri” ya da ukalalık olarak algılamazsın.

    Öncelikle “genel kod yazım kuralları” için Zend’in buradaki http://framework.zend.com/manual/1.12/en/coding-standard.naming-conventions.html kaynağı çok önemli. Kodu “ileride bizden başkasının da değiştirmesi gerekebileceği”ni göz önünde bulundurara yazmalıyız. Bunun en iyi yolu ise herkes tarafından kabul edilen kod yazım kurallarına uymaktır. Değişken isimlerinde altçizgi (underscore) yerine camelCase kullanmak artık yerleşmiş bir davranıştır. Örneğin, is_logged yerine isLogged uygundur.

    Şimdi teknik kısımlara geçelim.

    Include dosyalarının doğrudan çağrılmasını engellemek için eski ve uygun bir yöntemdir define kullanmak. Fakat tanımdan bağımsız bir karşılaştırma koşulu için şu kod daha sık kullanılır:

    if (basename(__FILE__) == basename($_SERVER[‘SCRIPT_FILENAME’])) {
    echo ‘doğrudan erişmeyin’;
    }

    Kodda ilerlediğimizde bu sefer require’lar gözüme çarpıyor. Daha doğrusu dosya yolları. İlk require’da dosya önünde ./ prefixi varken ikincide dosya direkt çağrılıyor. Her ikisi de index.php’ye relative olarak verilmiş dosya yolları. İlkinde kullanılan prefix ikincide neden kullanılmadı ya da ilkinde neden kullanıldı? Gerçi bu soru gereksiz çünkü çözüm zaten apayrı. Dosya yollarını relative vermek HTML’den kalma eski bir alışkanlık. Ben de daha yakın zamanda bıraktım, bırakmak zorunda kaldım.

    Projede SEO yöntemleri uygulayacağını varsayalım. Bu durumda adres büyük ihtimalle http://www.bilmemne.com/giris/ olacak. Peki burda birşey include etmek zorunda kalırsan ne olacak?

    require_once(“./db/settings.php”);

    Bu kodu kullandığında sunucu /giris adında bir klasörde db adında bir başka klasör arayacak. Tabii ki böyle bir klasör yok. Program çalışmayacak. Bu durumu çözmek için gerekli projelerde Apache üzerinden virtual host tanımladıktan sonra gerek HTML’de gerekse PHP’de tüm yolları absolute olarak tanımlamak. Mesela kökteki db klasöründen settings.php dosyasını çağıracaksan;

    require_once(“/db/settings.php”);

    Yazmak gerekecek. Bu arada aklıma gelen bir noktayı daha yazmalıyım. Gerek PHP performans testleri gerekse PHP yazım kuralları tırnak işaretlerinin kullanım alanlarını belirlemiştir. Eğer içerisinde herhangi bir değişken kullanmayacaksan mutlaka tek tırnak kullanmalısın. Konuyla ilgili daha fazla bilgi burada (http://tr2.php.net/types.string).

    Devam edelim.

    QueryString ile action adında bir değer gönderiliyor mu diye kontrol ediliyor. Fakat bu değerin boş olup olmadığını kontrol etmiyorsun. Yani ben,

    index.php?action=

    şeklinde bir adresle de oradaki if döngüsüne girmesini sağlıyorum programın. Bunu düzeltmek için;

    if (isset($_GET[‘action’]))

    şeklindeki satırı

    if (isset($_GET[‘action’]) && !empty($_GET[‘action’]))

    ile değiştirmelisin.

    Kodda ilerlediğimizde çalışmayı etkilemeyen fakat ölçeklenebilirliği ve bakımı (maintenance) zorlaştıran önemli bir sorun var. Separation of presentation and content https://en.wikipedia.org/wiki/Separation_of_presentation_and_content, yani içerikle sunumu ayırmak. Fakat incelediğimiz örnekte bu durum içerikle sunumu ayırMAmak oluyor.

    “Mutlaka ama mutlaka ama kesinlikle mutlaka” kodla sunumu ayırmalıyız. Çok mecburi durumlarda tabii ki bazı HTML etiketlerini PHP ile gönderebiliriz fakat normal şartlarda istersen MVC https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller tabanlı Zend Framework (http://framework.zend.com/) ‘ü kullanarak ya da PHP’nin resmi template motoru Smarty (http://www.smarty.net/) ‘i kullanarak, kod yani logic ile sunumu ayırmalısın. “Ufak projeler için böyle şeylere ne gerek var” diyenleri çok duydum. Biliyorum ki her standarda uymak projenin gecikmesine bile sebep olur ama burda önemli olan doğru “davranış biçimini” kazanmak. Yoksa birçok projeyi bir haftada bile bitirebiliriz. Neyse.

    is_logged fonksiyonun döndürdüğü değerler programın bel kemiğini oluşturuyor fakat daha iyi olabilir. Mesela,

    if (isset($_SESSION[‘logged’])) {
    return $_SESSION[‘logged’];
    } else {
    return 0;
    }

    Yerine,

    if (isset($_SESSION[‘logged’])) {
    return true;
    } else {
    return false;
    }

    Çok daha doğru olur. Çünkü, madem session’da tuttuğun değer ya 0 ya da 1 alıyor, o zaman tekrar return değeri olarak onu döndürmenin bir anlamı yok.

    Formda session ID’nin md5’li halini neden gönderdiğinle ilgili en ufak bir fikrim yok. Sadece aklıma şu geliyor: “Login olmaya çalışan adam, bilgilerini benim sitemdeki formdan mı gönderdi. Yoksa başka sunucudan mı?”. Eğer amaç buysa $_SERVER[‘HTTP_REFERER’]’i kullanabilirsin. Fakat burada ) da belirtildiği gibi REFERER bilgisi de çok güvenilir değil.

    En önemli konu tabii ki güvenlik. Kodda formdan gelen datalar için htmlspecialchars ) kullanılmış. Bu fonksiyon özel karakterleri HTML karşılıklarına çevirmekten başka birşey yapmaz. Mesela, & işaretini &amp şeklinde verir sadece. SQL Injection ‘a karşı bir koruma sağlamıyor. Eğer dahili PHP fonksiyonu kullanmayı düşünüyorsun başvurman gereken fonkstion addslashes ). Fakat onda da magic_quotes_gpc ) gibi sorunlar olduğu biliniyor. Yani yanlışlıkla iki kere uygulama ihtimali var. Ayrıca GOOGLE’ın bile zamanında yaşadığı (http://shiflett.org/blog/2006/jan/addslashes-versus-mysql-real-escape-string) UTF-8 sorunu var.

    SQL Injection’a karşı kullanman gereken temel işlem SQL sorgularındaki değişkenlere mysqli_real_escape_string ) uygulamaktır. Yada prepared statements ) kullanabilirsin. Böylece escape etmene gerek bile kalmaz. MySQL senin yerine her şeyi halleder.

    Yukarıdaki paragrafta MySQLi’den bahsettim. Konusu açılmışken veritabanına da değinelim. Neden ezSql kullandığını çözemedim. ezSql, çok çok eskiden yazılmş bir MySql interpreter class’ı ve üstelik yavaş. MySQLi ise MySQL veritabanlarına ulaşmak için object oriented yöntemler kullanıyor. Ayrıca prepared statements, transactions gibi normal MySQL komutlarıyla başaramayacağınız önemli işlevler sunuyor. İleride normal komutların yerine geçeceği de benim duyumlarım arasında.

    Son olarak güzel haberlerle bitirelim. İnternetimiz olduğu sürece sürekli başvurduğumuz PHP kaynağı tabii ki PHP’nin kendi sitesi. Bu siteden hem fonksiyonların açıklamalarına hem de örneklerine ulaşabiliyoruz. Fakat her zaman çok hızlı çalıştığı söylenemez. İşte bu sebeple tüm PHP.net sitesi Gazi Üniversitesi tarafından sürekli mirror işlemi ile yerel ve yurtiçi sunuculara aktarılıyor. adresinden ulaşabilirsiniz.

    PHP programcılarının çok sık başvurduğu bir diğer kaynak da PHP Classes. PHP Classes da Gazi Üniversitesi tarafından günlük olarak mirror işlemi ile kendi sunucularına alıyor. Hızlı bir şekilde her zaman adresinden ulaşabilirsiniz.

    Umut, istediğin zaman en basit işlem için bile her zaman yardım ederim.

    Kolay gelsin.

    1. merhaba;
      php bilginize güvenerek size bir konuda danışmak isterim.
      sitemde bir kullanıcı giriş bölümüm var. eski server da sorunsuz çalışan login kısmı yeni bir server a geçtiğimden itibaren çalışmıyor. herşey aynı koda da bir değişiklik yok ancak çalışmıyor.

      register_global on durumda ama bir turlu login olmuyor. hata vermiyor ama yine login ekranına geri dönüyor.

      önerilerinizi bekliyorum.

      teşekkürler…
      Levent İLHAN

  2. Abi valla sağol ya, tam beklediğim gibi bir eleştiri… Şimdi yazdıklarının hepsini madde madde gözden geçirip elimdeki kodu düzeltiyorum.

    Teşekkürler. =)

    1. merhaba;
      php bilginize güvenerek size bir konuda danışmak isterim.
      sitemde bir kullanıcı giriş bölümüm var. eski server da sorunsuz çalışan login kısmı yeni bir server a geçtiğimden itibaren çalışmıyor. herşey aynı koda da bir değişiklik yok ancak çalışmıyor.
      register_global on durumda ama bir turlu login olmuyor. hata vermiyor ama yine login ekranına geri dönüyor.
      önerilerinizi bekliyorum.
      teşekkürler…
      Levent İLHAN

      1. Merhaba. Maalesef sunucu yapılandırmasını ve kodu görmeden bir yorum yapmam doğru olmaz. Ancak anlattıklarınızdan şu sonuç çıkıyor: Aradaki tek fark sunucu. O halde sorun sunucunun ayarlarından veya php'nin sürüm farklılığından kaynaklanıyor olabilir. Bu iyi bir başlangıç noktası olabilir. İyi akşamlar

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir