Ziel: eine einfache Datenbank basierte Benutzerverwaltung erstellen
Verwendete Techniken: PHP, PDO
Schwierigkeitsgrad: für Anfänger geeignet, Grundkenntnisse über die Funktionsweisen von PHP und SQL vorausgesetzt
Anmerkungen: verwendet PDO, wegen dem Sicherheitsfaktor. Keine anwendung von OOP um das ganze möglichst 'primitiv' zu halten.
Kommentare: Der Code ist mit Zeilenkommentaren ausgestattet. Diese sind nicht entsprechend der Konvention in Deutsch und dienen der Erklärung.
Fehlerbehandlung: Fehler werden in ein Array geschrieben, außerdem gibt es einen 'globalen' Statuscode der Erfolg/Misserfolg oder Systemfehler darstellen kann. Wenn debug deaktiviert ist, werden diese Fehler am Ende des Scripts gelöscht und nur der Statuscode übergeben.
Debugging: Wenn aktiviert wird das Fehler-Array mit übergeben.
1. Passwort Speicherung mit aktueller Technik
Wir benutzen neue Technik zur Speicherung von Passwörtern um es Hackern möglichst schwer zu machen die Passwörter auszulesen, wenn sie bereits im Besitz des Password-Hashes sind.
Der Passwort-Hash ist eine Einwegrepräsentation des Passworts. Er kann bis zu 255 Zeichen lang werden und wird in der Datenbank in einem VARCHAR(255) Feld gespeichert.
Die Funktion password_hash() generiert uns einen, mit dem zweiten Parameter legen wir fest das der neueste Algorithmus benutzt werden soll. Die Ausgabe sieht meist nicht gleich aus, dies liegt in der 'Natur' der Funktion.
$entered_password = 'test';//Eingegebenes Passwort
$hashed_password = password_hash( $entered_password, PASSWORD_DEFAULT );
echo $hashed_password;//out: $2y$10$alSE7w6VMVsWMhoy1EIv5OXpK/Xz2CJxeO8nukDYVV2XTzZIMMavq
Um Hackern die Suppe zu 'verpfeffern' fügen wir zu unserem Passwort noch einen Pfeffer hinzu; er sorgt dafür das Hacker zugriff auf das Dateisystem haben müssen und die Passwörter nicht via 'rainbow table' auslesen können (also durch vergleichen mit eigenen Hashes. Der Pfeffer ist eine Zeichenkette, die an das Passwort angehängt wird und somit das eingegebene Passwort serverseitig 'erweitert'. Der Pfeffer sollte möglichst sicher aufbewahrt werden. Das Passwort qwertz wird mit unserem Pfeffer #h$na+= erweitert. Eine rainbow table enthält den Hash von quertz aber sicher nicht den Hash von qwertz#h$na+=. Bei der späteren überprüfung des Passworts muss der Pfeffer natürlich auch angehängt werden. Vorallem aber sorgt der Pfeffer dafür das der Hacker auch Zugriff auf das Dateisystem hat, um den Pfeffer zu ermitteln, denn sog. Wörterbuch Attacken werden auch schon durch den Salt verhindert.
Hier gibt es eine schöne Erklärung: http://www.martinstoeckli.ch/hash/de/
$entered_password = 'test';//Eingegebenes Passwort
$pepper = 'G$#(bPx!';
$hashed_password = password_hash( $entered_password.$pepper, PASSWORD_DEFAULT );
echo $hashed_password;//out: $2y$10$7V5rI8B5NIUX3dEVKX/Gnen3.ZIq6T06qXOLSBhwVnFukP4PLO6Lm
Für alles folgende brauchen wir nun ein paar kleine Helferlein um mit Fehlern gut klar zu kommen.
Zuerst eine Funktion die Fehler notiert und dann eine die den Status des Scriptes bereitstellt.
Wir definieren uns numerische Statuscodes die wir zur Beschreibung des Status verwenden:
//Statuscodes
define('SYS_ERROR' , E_ERROR );// 1 -> SYSTEM FEHLER
define('SYS_WARNING', E_WARNING );// 2 -> SYSTEM WARNUNG
define('SUCCESS' , 0 );// 0 -> ERFOLG
define('FAIL' , 300 );// 300 -> MISSERFOLG
//Basierend auf FAIL // 301 -> Passwort zu KURZ
//Basierend auf FAIL // 302 -> UID zu KURZ
//Basierend auf FAIL // 311 -> falscher BENUTZERNAME
//Basierend auf FAIL // 312 -> falsches PASSWORT
//Basierend auf FAIL // 313 -> Benutzer EXISTIERT BEREITS
Dann eine Funktion die die Fehler in ein Breitgestelles Array schreibt (Was Bestandteil des Feedback ist).
$feedback = array( //Speicher-array
'state_code' => SUCCESS, //Standart-Statuscode
'errors' => array(), //Array in dem Fehler gespeichert werden
);
/**
* function error($msg,$code)
* @param int | string $msg Specific Error-MESSAGE
* @param int $code (Specific) Error-CODE
*/
function error($msg,$code){
global $feedback;//Das 'globale' $feedback Array Bereitstellen
$feedback['errors'][] = array(//Ein neues Array im Fehler array erstellen
'error_msg' => $msg, //Die Fehlermeldung unter 'error_msg' speichern
'error_code' => $code, //Den Fehlercode unter 'error_code' speichern
);
}// error()
Alles anzeigen
Jeztzt können wir einen Fehler folgendermaßen speichern:
Damit wir auch noch an die Fehler drankommen bevor das Script beendet wird function stop()
Diese setzt den 'globalen' Status des Scripts und ist somit die letzte Aktion
$debug = false; //Debug an(true) / aus(false) schalten
/**
* function stop($code)
* @param int $code Globaler Statuscode für die Rückgabe
*/
function stop($code){
global $feedback, $debug; //Das 'globale' $feedback Array und die Debugvariable Bereitstellen
$feedback['state_code'] = $code; //Statuscode setzen
if(isset($debug) && !$debug) { unset($feedback['errors']); } //Wenn kein debug gefordert ist, Fehlermeldungen löschen (ess wird nur der globale Statuscode übertragen)
echo json_encode($feedback); //JSON repräsentation von $feedback ausgeben
exit($code); // Funktion mit Statuscode stoppen
}// stop()
Alles anzeigen
Nun können wie an die Datenbankverbindung. Wir stellen die Verbindung mit der Datenbank via PDO her.
$user = 'dbclient'; //Datenbank Benutzername
$pass = ''; //Datenbank Passwort
$db = 'dbclient_forum'; //Datenbank Name
$host = 'localhost'; //Datenbank Host
$opt = array(); //Optionale Einstellungen
$dbh = new PDO('mysql:host='.$host.';dbname='.$db, $user, $pass, $opt);//Verbindung herstellen und in $dbh speichern
Um Fehler auffangen zu können verwenden wir ein try/catch
$user = 'dbclient'; //Datenbank Benutzername
$pass = ''; //Datenbank Passwort
$db = 'dbclient_forum'; //Datenbank Name
$host = 'localhost'; //Datenbank Host
$opt = array( //Optionen (Optionale Einstellungen für die Verbindung)
PDO::ATTR_PERSISTENT => true, //PDO::ATTR_PERSISTENT ^= Verbindung aufrecht erhalten
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'",//PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'" =^ UTF-8 Compatibilität
);
//Datenbankverbindung (DataBaseHandle) herstellen
try{// Versuche
$dbh = new PDO('mysql:host='.$host.';dbname='.$db, $user, $pass, $opt);
}
catch (PDOException $e) {//Wenn nicht klappt
error('PDO Error! - '.$e->getMessage(), $e->getCode());//Fehler speichern
stop(SYS_ERROR);//Abbrechen (mit Verweis auf SYSTEMfehler)
}
Alles anzeigen
Ihr müsst kurz in PHPMyAdmin oder in die Konsole/Terminal und euch eine entsprechende Tabelle in der Datenbank erstellen
CREATE TABLE IF NOT EXISTS `tut_loginsys_users` (
`uid` varchar(50) NOT NULL,
`password` varchar(255) NOT NULL,
PRIMARY KEY (`uid`),
UNIQUE KEY `uid` (`uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Jetzt benötigen wir eine 'User ID' (UID) für die Benutzer Identfikation, in unserem fall ist der Benutzername die eindeutige Identifizierungsmöglichkeit.
Jetzt wird der Passworthash zusammen mit der UID in der Datenbank gespeichert.
Wenn dies fehlschlägt, liegt es entweder daran das der Benutzername schon vorhanden ist oder an einem anderen Fehler.
$entered_password = 'test';//Eingegebenes Passwort
$hashed_password = password_hash( $entered_password, PASSWORD_DEFAULT );
$entered_username = 'testuser';//Eingegebener Benutzername (UID)
//Daten in die DB schreiben
$sth = $dbh->prepare("INSERT INTO `tut_login_users` (`uid`,`password`) VALUES(:uid, :password);");//Statement vorbereiten
if (!$sth->execute(array(//Das Statement ausführen
':uid' => $entered_username,//UID übergeben
':password' => $hashed_password,//Passworthash übergeben
))) {
if (preg_match('/^Duplicate entry/', $sth->errorInfo()[2])) {//Wenn der Benutzername schon Existiert und der Fehler daher rührt
error('Username already exist!', FAIL+13);
stop(FAIL);//Beenden (mit Verweis auf BENUTZERfehler)
}
else{//Wenn ein anderer Fehler mit dem STatement aufgetreten ist
error('Statement Error! - '.$sth->errorInfo()[2].' #query: '.$sth->queryString, $sth->errorInfo()[0]);
stop(SYS_WARNING);//Beenden (mit Verweis auf SYSTEMwarnung)
}
}
else{
stop(SUCCESS);//Beenden (mit Verweis auf ERFOLG)
}
Alles anzeigen
Fertig, der Benutzer test mit passwort test ist nun in der Datenbank gespeichert
2. Passwort Validieren mit aktueller Technik
Wir benutzen hier auch die Funktionen aus der Registrierung und dazu starten wir noch eine Session:
Um das Passwort zu validieren reicht es nicht die Eingabe auch zu hashen und dann zu vergleichen, da der Algo. verschiedene Hashes für den gleichen Wert errechnet.
Wir nutzen also password_verify(). Als erster Parameter kommt das eingegebene Passwort, als zweiter Parameter der gespeicherte Hash. Den Pfeffer sollte man auchnicht vergessen, da sonnst die Passwörter nie übereinstimmen.
$entered_password = 'test';//Eingegebenes Passwort
$stored_password = '$2y$10$7V5rI8B5NIUX3dEVKX/Gnen3.ZIq6T06qXOLSBhwVnFukP4PLO6Lm';//Gespeichertes Passwort
echo password_verify( $entered_password, $stored_password );//out: 0 // <- da der Pfeffer fehlt
Jetzt brauchen wir nur noch eine Abfrage aus der DB wie der UID zugehörige Passworthash ist.
Hier wird geprüft ob der Benutzer Existiert und ob das Passwort stimmt.
$entered_password = 'test';//Eingegebenes Passwort
$entered_username = 'testuser';//Eingegebener Benutzername
$pepper = 'G$#(bPx!';
//Benutzerdaten aus der DB holen
$sth = $dbh->prepare("SELECT `password` FROM `tut_loginsys_users` WHERE `uid` = :uid;");
if (! $sth->execute(array(//Das Statement ausführen
':uid' => $entered_username, //Die UID übergeben
))) {//Bei Fehlern
error('Statement Error! - '.$sth->errorInfo()[2].' #query: '.$sth->queryString, $sth->errorInfo()[0]);
stop(SYS_WARNING);//Beenden (mit verweis auf SYSTEMwarnung)
}
if (! $password_hashed = $sth->fetch(PDO::FETCH_ASSOC)['password']){ //Gehashtes Passwort aus dem Statement auslesen, wenn dies nicht gelingt ist die UID nicht vorhanden
error('Username did not exist!', FAIL+11);
stop(FAIL);//Beenden (mit verweis auf EINGABEfehler)
}
if (! password_verify($entered_password.$pepper, $password_hashed)) { //Passwort+Pfeffer mit dem aus der DB vergleichen
error('Password did not fetch!', FAIL+12);
stop(FAIL);//Beenden (mit verweis auf EINGABEfehler)
}
else{
$_SESSION['login'] = true;//Session setzen
stop(SUCCESS);//Beenden (mit verweis auf ERFOLG)
}
Alles anzeigen
Fertig, nun können wir Benutzer anlegen und Passwörter überprüfen.
Außerdem können wir anhand der Session prüfen ob der Benutzer eingeloggt ist.
3. Logout
Das logout ist recht simpel gehalten mit einem einfachen
4. Zusammenfassung in ein Script
Alles zusammen in einem Script gibt die Möglichkeit das ganze via AJAX aufzurufen oder auch ganz normal via HTTP oder eben via Include (parameter setzen).
Die Variablen befinden sich jetzt alls im array $X, dies erschwert zwar die Leserlichkeit des Codes, erleichtert aber den Zugruff in Funktionen auf Variablen.
Lest noch Zeilen 7-9 und dann benutzt es!
<?php
/**
* @author Wolf Wortmann
* @license Feel free to use, modify and redistribute this code. But please keep this license and the copyright notice.
* @copyright (c) Copyright 2015 Wolf Wortmann <wolf@wolfgang-m.de> / <http://elementcode.de>
*
* @description Some functions for a Usermanagement (login/logout/register).
* You should modify the filter() function (line 113 - 130) such as line 48 / 49.
* Modify too logout() (line 193-195) such as succeed login (line 156) if you need.
*
* Parameters with [!] are required!
* @param string | int $_POST['username'] [!] Username/ID for login/registration
* @param string | int $_POST['password'] [!] Password for login/registration
* @param string | int $_POST['json'] Format of the feedback (if isset it's JSON; else it's a String) (recomended for AJAX requests)
* @param string $_POST['action'] Action (login|logout|register) (fallback is login)
* @param string | int $_GET['include'] You musst caal loginsys() (line 205) manually (recomended for include)
*/
//Einstellungen
//Datenbank
$X['pdo']['user'] = 'dbclient'; //Username
$X['pdo']['pass'] = ''; //Password
$X['pdo']['db'] = 'dbclient_forum'; //Database
$X['pdo']['host'] = 'localhost'; //Host
$X['pdo']['opt'] = array( //Optionen (Optionale Einstellungen für die Verbindung)
PDO::ATTR_PERSISTENT => true, //PDO::ATTR_PERSISTENT ^= Verbindung aufrecht erhalten
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'",//PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'" =^ UTF-8 Compatibilität
);
//Statuscodes
define('SYS_ERROR' , E_ERROR );// 1 -> SYSTEM FEHLER
define('SUCCESS' , 0 );// 0 -> ERFOLG
define('FAIL' , 300 );// 300 -> MISSERFOLG
//based on FAIL // 301 -> Passwort zu KURZ
//based on FAIL // 302 -> UID zu KURZ
//based on FAIL // 311 -> falscher BENUTZERNAME
//based on FAIL // 312 -> falsches PASSWORT
//based on FAIL // 313 -> Benutzer EXISTIERT BEREITS
//Rückgabe-array Struktur
$X['feedback'] = array(//Speicher-array
'state_code' => SUCCESS, //Standart-Statuscode
'errors' => array(), //Array in dem Fehler gespeichert werden
);
//Sicherheitsrelevante Einstellungen
$X['secure']['pepper'] = 'pepper#1';//"geheimer" Pfeffer siehe <http://www.martinstoeckli.ch/hash/de/hash_pepper.php>
$X['secure']['debug'] = true; //Debug an(true) / aus(false) schalten
error_reporting(E_ALL); //PHP Fehlerberichte an(E_ALL) / aus(0) schalten
session_start(); //Die Session starten
//Parameter
$X['param']['uid'] = filter($_POST['username'], 'username'); //BenutzerID
$X['param']['pass'] = filter($_POST['password'], 'password'); //Passwort
$X['param']['json'] = isset($_POST['json']) ? true : false; //JSON feedback
$X['param']['action'] = filter($_POST['action']); //Action (login|logout|register)
//Datenbankverbindung (DataBaseHandle) herstellen
try{// Versuche
$X['dbh'] = new PDO('mysql:host='.$X['pdo']['host'].';dbname='.$X['pdo']['db'], $X['pdo']['user'], $X['pdo']['pass'], $X['pdo']['opt']);
}
catch (PDOException $e) {//Wenn nicht klappt
error('PDO Error! - '.$e->getMessage(), $e->getCode());//Fehler speichern
stop(SYS_ERROR);//Abbrechen (mit verweis auf SYSTEMfehler)
}
//Funktionen
/**
* function stop($code)
* @param int $code Globaler Statuscode für die Rückgabe
*/
function stop($code){
global $X;//Das 'globale' $X Array Bereitstellen
$X['feedback']['state_code'] = $code; //Statuscode setzen
if($X['secure']['debug'] === false){unset($X['feedback']['errors']); }//Wenn kein debug gefordert ist, Fehlermeldungen löschen (ess wird nur der globale Statuscode übertragen)
echo json_encode($X['feedback']); //JSON repräsentation von $X['feedback'] zurückgeben
}// stop()
/**
* function error($msg,$code)
* @param int | string $msg Specific Error-MESSAGE
* @param int $code (Specific) Error-CODE
*/
function error($msg,$code){
global $X;//Das 'globale' $X Array Bereitstellen
$X['feedback']['errors'][] = array(//Ein neues Array im Fehler array erstellen
'error_msg' => $msg, //Die Fehlermeldung unter 'error_msg' speichern
'error_code' => $code, //Den Fehlercode unter 'error_code' speichern
);
}// error()
/**
* function pretty_print_r($var)
* helper function for prettyfied print_r
* @param mixed $var Variable for printing
*/
function pretty_print_r($var){
echo "<pre><strong>var_dump()</strong><br><br>";
var_dump($var);
echo "<br><strong>print_r()</strong><br><br>";
print_r($var);
echo "</pre>";
}
/**
* function filter($string,$type = 'none')
* @param string $string string to be filtered
* @param string $type type-special Filter
* @return string $r Filtered string
*/
function filter($string,$type = 'none'){
$string = trim($string);
switch ($type) {
case 'username':
$r = htmlspecialchars(
str_replace(' ', '-', $string)
);
break;
case 'password':
$r = $string;
break;
default:
$r = $string;
break;
}
return $r;
}
/**
* function login()
* The login function
*/
function login(){
global $X;
$sth = $X['dbh']->prepare("SELECT `password` FROM `tut_loginsys_users` WHERE `uid` = :uid;");
if (! $sth->execute(array(//Das Statement ausführen
':uid' => $X['param']['uid'], //Die 'uid' (userID) übergeben
))) {//Bei Fehlern
error('Statement Error! - '.$sth->errorInfo()[2].' #query: '.$sth->queryString, $sth->errorInfo()[0]);
stop(SYS_ERROR);//Beenden (mit verweis auf SYSTEMfehler)
}
else{
if (! $password_hashed = $sth->fetch(PDO::FETCH_ASSOC)['password']){ //Gehashtes Passwort aus dem Statement auslesen
error('Username did not exist!', FAIL+11);
stop(FAIL);//Beenden (mit verweis auf EINGABEfehler)
}
else{
if (! password_verify($X['param']['pass'].$X['secure']['pepper'], $password_hashed)) { //Passwort+Pfeffer mit dem aus der DB vergleichen
error('Password did not fetch!', FAIL+12);
stop(FAIL);//Beenden (mit verweis auf EINGABEfehler)
}
else{
$_SESSION['login'] = true;//Session setzen
stop(SUCCESS);//Beenden (mit verweis auf ERFOLG)
}
}
}
}
/**
* function register()
* The register function
*/
function register(){
global $X;
$password_hash = password_hash($X['param']['pass'].$X['secure']['pepper'],PASSWORD_DEFAULT);//Passwort Hash generieren (mit Pfeffer)
$sth = $X['dbh']->prepare("INSERT INTO `tut_loginsys_users` (`uid`,`password`) VALUES(:uid, :password);");//Statement vorbereiten
if (!$sth->execute(array(//Das Statement ausführen
':uid' => $X['param']['uid'],//UID übergeben
':password' => $password_hash,//Passworthash übergeben
))) {
if (preg_match('/^Duplicate entry/', $sth->errorInfo()[2])) {//Wenn der Benutzername schon Existiert und der Fehler daher rührt
error('Username already exist!', FAIL+13);
stop(FAIL);//Beenden (mit verweis auf BENUTZERfehler)
}
else{//Wenn ein anderer Fehler mit dem STatement aufgetretet ist
error('Statement Error! - '.$sth->errorInfo()[2].' #query: '.$sth->queryString, $sth->errorInfo()[0]);
stop(SYS_ERROR);//Beenden (mit verweis auf SYSTEMfehler)
}
}
else{
stop(SUCCESS);//Beenden (mit verweis auf ERFOLG)
}
}
/**
* function logout()
* The logout function
*/
function logout(){
$_SESSION['login'] = false;
}
/**
* function loginsys($uid,$password,$action = 'login', $json = false)
* manualy caal loginsys() (via include)
* @param string | int $uid UserID
* @param string | int $password Password
* @param string $action Action (login|logout|register)
* @param bool $json Feedback format
*/
function loginsys($uid,$password,$action = 'login', $json = false){
$X['param']['uid'] = filter($uid, 'username'); //BenutzerID
$X['param']['pass'] = filter($password, 'password');//Passwort
$X['param']['ajax'] = $ajax; //Ajax request
$X['param']['json'] = $json; //JSON feedback
$X['param']['action'] = $action; //Action (login|logout|register)
start_loginsys();
}
/**
* function start_loginsys()
* Start's the whole process
*/
function start_loginsys(){
global $X;
ob_start();
switch ($X['param']['action']) {
case 'register':
register();
break;
case 'logout':
logout();
break;
default: //login
login();
break;
}
$feed = ob_get_contents();
ob_end_clean();
if ($X['param']['json'] === true) {
echo $feed;
}
else{
$feed = json_decode($feed, true);
if ($feed['state_code'] == 0) {
echo "Succeed!";
}
elseif ($feed['state_code'] >= 300) {
echo "Check your Input!";
}
else{
echo "System error, try later again!";
}
}
}
//Wenn das script nicht mit ?include aufgerufen wurde, starten
if (!isset($_GET['include'])) {
start_loginsys();
}
?>
Alles anzeigen
Wenn etwas unbeantwortet geblieben ist, oder schlecht beantwortet wurde, FRAGT nach, ansonnsten viel Spas!