Suchen
Inside Wiki
Nützliche Links




 
phpforum.de bei Facebook
 
phpforum.de bei Twitter
 

Zurück   PHP Forum: phpforum.de > phpforum.de Wiki > phpforum.de Wiki

PHP Wiki Dieses Wiki sammelt Lösungen, zu Problemen, welche immer wieder im Forum auftauchen.

 
 
Artikel-Optionen Ansicht
  #1  

Standard Login und geschützte Bereiche

 

Tutorials - Inhalte

Login und geschützte Bereiche



Zugangsdaten



Zuersteinmal müssen wir uns überlegen, wie genau die Zugangskontrolle zum System erfolgen soll. Am gängigsten ist es, als Zugangskontrolle einen Account-Namen und ein Passwort voraus zu setzen. Häufig wird als Account-Name dabei auch einfach die E-Mail-Adresse verwendet.

Alternativen hierzu sind Client-Zertifikate, die Einbindung eines Key-Card-Systems oder die Anbindung einer externen Autorisierungslösung (speziell im Bereich Single-Sign-On, also z.B. OpenID oder Microsoft Passport).

Allen diesen Systemen gemein ist, dass zwei Parteien über eine Art "Shared Secret" verfügen. Kennt ein Benutzer dieses "gemeinsame Geheimnis", oder kann er durch einen vertrauenswürdigen Dritten nachweisen, dass er es kennt, so gehen wir als Seitenbetreiber davon aus, dass es sich bei dem gegenwärtigen Benutzer um die Person handelt, mit der wir unser Geheimnis ausgemacht haben.

Für diesen Artikel bleiben wir aber beim einfachsten Fall, nämlich der Angabe eines Benutzernamens und eines Passwortes.

Grundsätzlich sollte dieses Passwort immer so geheim wie möglich bleiben, sprich es sollten so wenige Menschen wie möglich kennen. Ihr kennt das z.B. von den PIN-Nummern Eurer Bankkarten - die kennt auch nur Ihr, den Mitarbeiter hinter dem Bankschalter gehen die nichts an. Gleiches gilt natürlich auch für den Betreiber einer Datenbank oder für jeden Administrator oder Angreifer Eures Servers. Um dies zu realisieren, sollte das Passwort niemals im Klartext irgendwo gespeichert werden, sondern es sollte stattdessen höchstens ein Hash irgendwo abgelegt werden.

Vom ein oder anderen wird jetzt der Einwand kommen, man könne ja das Passwort auch einfach irgendwo verschlüsselt ablegen. Diese Idee ist auf den ersten Blick gut, da man vergessene Passwörter auf diesem Wege einfacher wieder entschlüsseln könnte. Auf den zweiten Blick fällt aber auf, dass man zur Passwort-Verifizierung bei jedem symmetrischen Verschlüsselungsverfahren auch das Verschlüsselungspasswort im Klartext auf dem Server verwalten müsste. Jede Betriebsmitarbeiter oder Angreifer hätte damit leichtes Spiel und könnte alle verschlüsselten Passwörter leicht wieder entschlüsseln.

Wenn Ihr sowas also unbedingt realisieren wollt, dann benötigt Ihr ein asymetrisches Verschlüsselungsverfahren - das macht eine derartige Implementierung durchaus möglich. Das ganze wäre allerdings relativ komplex zu realisieren, und der Aufwand wird in einem Großteil aller Fälle die Vorteile überwiegen.

Tipp:
Lasst die Finger davon, und bleibt bei einem Hash mit hinreichend aktuellem Hashverfahren. Gegenwärtig bietet sich da die Verwendung eines der Sha-Verfahren an, idealerweise eines derer mit den größeren Hashes, also SHA-256 oder SHA-512


Und wohin nun damit?



Prinzipiell haben wir uns jetzt also entschlossen, dass wir einen Account-Namen und ein Passwort zur Authentifizierung unserer Benutzer verwenden wollen. Außerdem haben wir entschieden, dass wir das Passwort nicht im Klartext, sondern nur als Hash auf unserem Server oder Webspace vorhalten wollen.

Tja, und wohin nun damit? Hier gibt es im wesentlichen zwei Möglichkeiten:
  • Eine Datenbank
  • Eine Datei

Beiden gemeinsam ist die Methodik des Zugriffes. In beiden Fällen muss aus der Eingabe eines Benutzernamens und eines Passwortes ein einfacher Ja-/Nein-Wert generiert werden. Die zugehörige Frage zum Ja/Nein ist dabei "Kennen wir den?".

Da wir diese Frage wahrscheinlich häufiger stellen werden, sollten wir den zugehörigen Code auch gleich kapseln, d.h. ihn in einer Funktion oder gar Klasse und Methode ablegen. Für diesen Artikel machen wir es uns einfach und verwenden ne gewöhnliche Funktion:

PHP Quellcode:
/**
 * Diese Funktion ermittelt, ob ein Benutzername und ein Passwort bekannt sind.
 * @param String $name Benutzername
 * @param String $password Passwort dazu
 * @return boolean
 */

function confirmNameAndPassword($name,$password)
{
  // auf die konkrete Implementierung gehen wir gleich noch im Einzelnen ein
}


Wie schon angesprochen gibt es jetzt zwei Möglichkeiten, Zugangsdaten zu speichern,
nämlich eine Datenbank oder eine Datei. Die beiden Folgekapitel beschreiben diese im Einzelnen.

Tipp:
Sollte bei eurem Webspace keine Datenbank mit inbegriffen sein, so solltet ihr prüfen, ob ggf. die Erweiterung SQLite aktiviert ist. Diese Erweiterung stellt eine SQL-Zugriffsschicht für Dateien zur Verfügung, ohne das dazu ein Datenbankserver notwendig wäre.



Zugangsdaten in der Datenbank am Beispiel MySQL



Vorüberlegungen



Zuersteinmal müssen wir uns überlegen, wie genau die Daten in der Datenbank abgelegt werden sollen. Da wir es hier prinzipiell mit Benutzeraccounts zu tun haben, schlage ich eine Tabelle Accounts vor, die im Mindestfall eine numerische Datensatz-ID, den Benutzernamen und den Passwort-Hash enthält. Im Hinblick auf spätere Erweiterungen, nehmen wir auch schon einmal die E-Mail-Adresse des jeweiligen Benutzers mit in die Tabelle auf.

Erstellt werden kann diese Tabelle wie folgt:

MySQL Quellcode:
CREATE TABLE `Accounts`
(
  `id`          INT(11) AUTO_INCREMENT PRIMARY KEY,
  `username`    VARCHAR(50) NOT NULL,
  `pwHash`      VARCHAR(512) NOT NULL,
  `email`       VARCHAR(255) NOT NULL,
  UNIQUE(`username`)
);


Oben kann man sehen, dass der Username als Unique markiert ist. Mit dieser Markierung stellt die Datenbank gleich selbständig sicher, dass es keinen Benutzernamen zwei mal in der Datenbank geben kann. Außerdem beschleunigen Unique-Constraints den Suchzugriff auf den Benutzernamen, was grad bei größeren Benutzermengen später mal extrem sinnvoll sein kann.

Implementierung


Folgende Implementierung wäre somit eine sinnvolle Variante:

PHP Quellcode:
/**
 * Diese Funktion ermittelt, ob ein Benutzername und ein Passwort bekannt sind.
 * @param MySQLi $dbLink Link zur Datenbank
 * @param String $name Benutzername
 * @param String $password Passwort dazu
 * @return boolean
 */

function MySQLiConfirmNameAndPassword(mysqli $dbLink,$name,$password)
{
  /* Query vorbereiten:
   *   Frage an die DB: Wie viele User mit diesem Namen und PW gibt es?
   */

  $sql='SELECT COUNT(*) FROM Accounts WHERE username=? AND pwHash=?';
  if(!($stmt=$dbLink->prepare($sql)))
  {
    // Abbruch im Falle eines Fehlers
    trigger_error('Could not prepare statement!',E_USER_ERROR);
    // Da wir den eingesetzten Error-Handler nicht kennen, kann es sein, dass
    // das Skript weiter läuft. In diesem Fall darf kein Login erlaubt werden,
    // also geben wir hier False zurück.
    return false;
  }

  /* Jetzt kennt unser Datenbank-Server schon mal die INSERT-Anweisung.
   * Nun müssen wir die zugehörigen Werte (also den Benutzernamen und den
   * Passwort-Hash ggf. generieren, und danach ebenfalls an den Datenbank-
   * Server übermitteln.
   */


  /* Hash d. PW ermitteln
   *   Zur Ermittlung des Passwort-Hashes verwenden wir die Hash-Funktion.
   *   Die Funktion hash() erlaubt den Zugriff auf eine große Anzahl von
   *   Hash-Verfahren durch eine einzelne Funktion. In diesem Beispiel
   *   verwenden wir SHA512 als Hashmethode. SHA512 ist allerdings nicht auf
   *   allen Servern verfügbar. Alternativ kommen auch SHA1 oder SHA256 als
   *   Hashverfahren in Frage. Prinzipiell gilt: Je größer die Zahl hinter
   *   dem SHA, desto besser ist das Hashverfahren. (Achtung: das gilt nur
   *   für die genannten drei Verfahren.)
   *
   *   Sollte Euer Server die hash()-Funktion nicht kennen, so gibt es zumindest
   *   für SHA1 auch eine eigene Funktion.
   *   Mehr Infos hat das Manual unter http://www.php.net/sha1.
   */

  $inputPwHash=hash('SHA512',$password);

  /* Username:
   *   Dieser Parameter wurde der Funktion schon so übergeben. Wir
   *   setzen diesen also nur noch ein.
   */


  /* Fragen wir unsere Datenbank:
   *   bind_param() verknüpft eine vorher preparierte SQL-Anweisung oder Abfrage
   *   mit zugehörigen Werten. Der erste Parameter der Funktion gibt dabei an,
   *   wie die übergebenen Werte codiert werden sollen.
   *   Ein "s" entspricht dabei etwa dem altbekannten mysql_real_escape_string(),
   *   ein "i" einem Cast nach Int, usw.
   *   Es ist möglich, der Funktion mehrere Parameter zu übergeben. In diesem
   *   Fall muss die Buchstabenfolge im ersten Parameter der Typenfolge in den
   *   folgenden Parametern entsprechen. Ist der erste Buchstabe also ein s,
   *   dann wird der zweite Parameter als String behandelt. Ein Folgebuchstabe
   *   bezieht sich dann auf den dritten Parameter, usw.
   */

  $stmt->bind_param('ss',$name,$inputPwHash);

  /* So, die SQL-Anweisung/Abfrage und die Parameter sind jetzt beim Datenbank-
   * Server. Der braucht jetzt noch das GO, um diese Abfrage auch wirklich zu
   * tätigen:
   */

  $stmt->execute();
 
  /* Wenn das nicht geklappt hat
   *   Nach jeder Abfrage sollte man den Erfolg kontrollieren.
   *   Bei MySQLi geht das am einfachsten, in dem man die errno-Eigenschaft des
   *   Statement-Objektes auswertet. Ist diese Eigenschaft ungleich 0, so ist ein
   *   Fehler aufgetreten.
   *   Mehr Infos bietet dann die error-Eigenschaft. Diese enthält die Fehlermeldung
   *   in Textform.
   *   ACHTUNG: Fehlertexte sollten auf Produktivservern NIEMALS an den Client
   *            übermittelt werden. Ein potentieller Angreifer kann dadurch
   *            evtl. schädliche Rückschlüsse über mögliche Angriffsvektoren
   *            ziehen.
   */

  if($stmt->errno)
  {
    // Abbruch
    trigger_error('Statement execution failed!',E_USER_ERROR);
    return false;
  }
  /* Ansonsten: Ergebnis auslesen
   *   Wir sagen MySQLi dazu mit der bind_result()-Methode, in welchen Variablen
   *   die Ergebnisse bereit gestellt werden sollen. Mit jedem Aufruf der fetch()-
   *   Methode iteriert die MySQLi-Extension dann in diesen Variablen durch
   *   die Ergebnisliste.
   */

  $stmt->bind_result($numberOfMatches);
  $stmt->fetch();
  /* Aufräumen */
  $stmt->close();
 
  /* und zurück geben, ob es den Account mit diesem PW gibt:
   *   Wir greifen dabei jetzt auf die oben zugewiesene Variable
   *   $numberOfMatches zurück, die das Ergebnis des COUNT(*) aus der
   *   SQL-Abfrage beinhaltet.
   */

  return ($numberOfMatches==1);
}


Anmerkungen



Irgendwas ist seltsam? Sicherlich - viele andere Tutorials sprechen auch heutzutage noch von der alten MySQL-Extension, wenn sie über den Zugriff auf das Datenbankmanagementsystem MySQL sprechen. Die alte MySQL-Extension kennt weder sowas wie prepared Statements, noch gibt es für die alte Extension eine objektorientierte Zugriffsschicht.

Warum nun also ausgerechnet MySQLi? Und ist jetzt eigentlich alles falsch, was lange Jahre lang richtig war? Nein, falsch ist es nicht, nur veraltet. Die alte MySQL-Extension hat einige gravierende Nachteile gegenüber den neueren Alternativen wie MySQLi und PDO.

Erschwerend kommt hinzu, dass das PHP-Handbuch explizit von der Verwendung der alten Extension abrät. Wenn man also heutzutage ein Skript erstellt, dann sollte im Sinne der Zukunftssicherheit nicht mehr auf die alte MySQL-Extension gesetzt werden.

Einer der vielen Vorteile der beiden genannten Extensions ist die Möglichkeit, die oben angesprochenen Prepared Statements zu verwenden. Performancevorteile bringen die uns zwar an dieser Stelle nicht (dazu sind sie an anderer allerdings durchaus in der Lage), sie entledigen uns aber des lästigen Problems des Risikos von SQL Injections, da sie die saubere Filterung der Query-Parameter für uns übernehmen. Auf irgendwelche Escape-Funktionen oder irgendwelche Castings können wir an dieser Stelle also verzichten.

Und warum steht da eigentlich andauernd was von trigger_error()? Die Funktion trigger_error() bietet Euch die Möglichkeit, die Fehlermeldungen Eurer Skripte direkt an die Fehlerbehandlungsroutinen von PHP zu übermitteln. Auf Produktivsystemen unterdrücken diese Fehlerbehandlungsroutinen meist die Darstellung der Fehlermeldungen im Browser und sendet sie stattdessen in ein Fehlerlog. Auf Entwicklungssystemen ist dies zumeist umkehrt, und die Fehlermeldungen werden direkt im Browser dargestellt, was natürlich dann auch die Fehlerbehebung deutlich einfacher macht.

All das könnte man natürlich selbst entwickeln oder man könnte etablierte Logging-Klassen (z.B. Zend_Log) verwenden. Eine andere Möglichkeit wäre der Einsatz von Exceptions.

Für diesen Artikel wurde allerdings bewusst auf imperative/prozedurale Programmierung gesetzt, da grade für den Einsteiger die Verwendung objektorientierter Programmieransätze häufig verwirrend erscheint.

Auch bewusst verzichtet wurde auf die Verwendung von Namespaces. Unter Verwendung von Namespaces ist es möglich, auf das Prefix im Funktionsnamen zu verzichten, was wiederum zur Folge hätte, dass die Unterschiede in der Nutzung der Datenbank- und Dateibasierten Speicherfunktionen weiter sinken. Namespaces und prozedurale Programmierung werfen aber im Zusammenspiel wieder neue Probleme auf, die am elegantesten durch die oben ausgeschlossenen objektorientierten Programmieransätze zu umschiffen sind.

Tipp:
Es gibt in diesem Wiki auch noch einen Artikel zur Objektorientierten Ausgestaltung eines Loginsystems: Login-System / Authentifizierung


Zugangsdaten in einer Datei



Vorüberlegungen


Alternativ zur Datenbank können wir unsere Zugangsdaten natürlich auch einfach in einer Datei ablegen. In dieser Datei verwalten wir wieder die selben Informationen wie oben, nämlich Benutzer-ID, Benutzername, Passwort-Hash und E-Mail-Adresse.

Als Aufbau kommen hier mehrere Möglichkeiten in Frage. Am einfachsten ist es, wenn wir die Daten einfach in einem Array halten, welches wir für die Speicherung einfach jeweils serialisieren.

Tipp:

Für größere Datenmengen ist dieses Verfahren denkbar ungeeignet, da alle Daten für jeden Lese- oder Schreibprozess immer komplett in den Speicher geladen werden müssen. Effektiver ist hier meist der Einsatz einer XML-Datei im Zusammenspiel mit einem Event-Basierenden Parser (Siehe Libexpat-basierende PHP-XML-Extension.) Alternativ käme auch eine reine Datendatei mit fester Spaltenbreite in Frage. Letztere stellt die wahrscheinlich skalierbarste Form der Ablage mehrere Datensätze in einer Datei dar.


Wichtig bei der Ablage von Zugangsdaten in Dateien ist die Wahl des Speicherortes. Hier muss sicher gestellt werden, dass niemand unbefugtes über irgendein Protokoll, sei es nun HTTP oder FTP, auf die Datei mit den Zugangsdaten zugreifen kann.

Es bietet sich hier an, die Dateien außerhalb des Wurzelverzeichnisses des Webspaces abzulegen, also außerhalb des Document-Root. Da dies vielfach bei gemietetem Webspace nicht möglich ist, besteht alternativ die Möglichkeit, die Datei in einem Extra-Verzeichnis abzulegen, welches dann (zumindest wenn der Webserver ein Apache ist) mit der folgenden .htaccess-Datei abgesichert werden kann:

Code (Apache):
Order Deny,Allow
Deny From All


Tipp:
Sollte es für Euch und auf Eurem Webspace keine Möglichkeit geben, die Datei mit den Zugangsdaten dem öffentlichen Zugriff zu entziehen, dann lasst die Finger vom Datei-basierenden Speichern.


Implementierung



So, wir haben gesagt, wir wollen Array serialisieren. Dies geht in PHP denkbar einfach mittels dieser Funktionen:

DOKU-VORLESE-SERVICE(TM)
string serialize(mixed value)
Generates a storable representation of a value

DOKU-VORLESE-SERVICE(TM)
mixed unserialize(string str[, array options])
Creates a PHP value from a stored representation


Für diesen Artikel gehen wir davon aus, dass das verwendete Array dem folgenden
Aufbau entspricht:

Code:
array (
  0 =>
  array (
    'ID' => 1,
    'USERNAME' => 'someone',
    'PW_HASH' => '66b27417d37e024c46526c2f6d358a754fc552f3...',
    'EMAIL' => 'somebody@somewhere.net',
  ),
  1 =>
  array (
    'ID' => 2,
    'USERNAME' => 'nobody',
    'PW_HASH' => 'a9993e364706816aba3e25717850c26c9cd0d89d...',
    'EMAIL' => 'none@nowhere.net',
  ),
)


Unsere Funktion von oben könnte man damit auf die folgende Art realisieren:

PHP Quellcode:
/**
 * Diese Funktion ermittelt, ob ein Benutzername und ein Passwort bekannt sind.
 * @param String $filename Name und Pfad der Datendatei
 * @param String $name Benutzername
 * @param String $password Passwort dazu
 * @return boolean
 */

function FileConfirmNameAndPassword($filename,$name,$password)
{

  /* Daten laden:
   *   Zuerst einmal müssen wir wissen, ob wir auf das angegebene Datenfile
   *   überhaupt Zugriff haben. Wir prüfen also erst mal ab, ob die Datei
   *   existiert und für PHP lesbar ist.
   */

  if(!(file_exists($filename) && is_readable($filename)))
  {
    /* ist das nicht der Fall, brechen wir mit einem Fehler ab. */
    trigger_error('Failed to load password storage file!',E_USER_ERROR);
    return false;
  }
  /* Die Daten laden wir erstmal in eine Variable */
  $rawData=file_get_contents($filename);

  /* nun versuchen wir, die Daten in ein PHP-Array zu überführen.
   * Da eine potentielle Fehlermeldung an dieser Stelle Informationen über
   * Passworthashes enthalten könnte, unterdrücken wir die Ausgabe der Fehlermeldung.
   */

  $accountList=@unserialize($rawData);

  /* Haben wir denn jetzt wirklich ein Array? */
  if(!is_array($accountList))
  {
    /* Wenn Nein: Abbruch mit Fehler */
    trigger_error('Failed to load password storage file! Format error!',
                  E_USER_ERROR);
    return false;
  }

  /* Hash d. PW ermitteln
   *   Zur Ermittlung des Passwort-Hashes verwenden wir die Hash-Funktion.
   *   Die Funktion hash() erlaubt den Zugriff auf eine große Anzahl von
   *   Hash-Verfahren durch eine einzelne Funktion. In diesem Beispiel
   *   verwenden wir SHA512 als Hashmethode. SHA512 ist allerdings nicht auf
   *   allen Servern verfügbar. Alternativ kommen auch SHA1 oder SHA256 als
   *   Hashverfahren in Frage. Prinzipiell gilt: Je größer die Zahl hinter
   *   dem SHA, desto besser ist das Hashverfahren. (Achtung: das gilt nur
   *   für die genannten drei Verfahren.)
   *
   *   Sollte Euer Server die hash()-Funktion nicht kennen, so gibt es zumindest
   *   für SHA1 auch eine eigene Funktion.
   *   Mehr Infos hat das Manual unter http://www.php.net/sha1.
   */

  $inputPwHash=hash('SHA512',$password);

  /* Durchlaufen unseres Datenbestandes
   *   Wir durchlaufen dazu alle Datensätze des Arrays und vergleichen Passwort-Hashes
   *   und Benutzernamen. Zum Durchlaufen der Datensätze verwenden wir die Funktion
   *   array_filter(). Diese Funktion durchläuft alle Datensätze einer Menge und
   *   wendet auf jedes Element eine übergebene Funktion an. Wann immer die Funktion
   *   true zurück liefert, wird das jeweilige Array-Element ins Zielarray übertragen,
   *   wann immer False zurück gegeben wird, wird das Element verworfen.
   *   Im Ergebnis-Array liefert array_filter() also alle die Werte zurück, wo
   *   beide Wertpaare übereinstimmen.
   */

  $matches=array_filter($accountList,
                        function($elem) use ($name,$inputPwHash)
                        {
                          return isset($elem['USERNAME'])
                                 && isset($elem['PW_HASH'])
                                 && ($elem['USERNAME']==$name)
                                 && ($elem['PW_HASH']==$inputPwHash);
                        });
  /* und zurück geben, ob es den Account mit diesem PW gibt:
   * Da $matches ein Array mit allen passenden Accounts ist, können wir hier
   * einfach wieder über die Anzahl an Datensätzen prüfen.
   */

  return (count($matches)==1);
}


Anmerkungen



Diese Funktion geht analog zum Beispiel mit der MySQL-Datenbank im übrigen davon aus, dass im ersten Parameter ein Verweis zum Speicherort, in diesem Fall Dateiname- und Pfad, übergeben werden.

Angemerkt sei außerdem, dass das Beispiel oben PHP 5.3 voraussetzt, da Closures für die Suche nach dem passenden Benutzer zum Einsatz kommen. Für den Fall, dass dies bei Euch nicht der Fall ist, kann der array_filter-Aufruf auch wie folgt geschrieben werden:

PHP Quellcode:
// Durchlaufen unseres Datenbestandes
$matches=array_filter($accountList,
                      create_function('$elem',
                                      'return
                                         isset($elem[\'USERNAME\'])
                                         && isset($elem[\'PW_HASH\'])
                                         && ($elem[\'USERNAME\']==\''
.addslashes($name).'\')
                                         && ($elem[\'PW_HASH\']==\''
.addslashes($inputPwHash).'\');'
                                     )
                      );


Eine dritte Möglichkeit der Implementierung der selben Suche wäre dann natürlich auch noch die Verwendung einer einfachen Schleife.

Immer her mit den Benutzern



Okay - Benutzer verifizieren können wir jetzt. Aber Moment mal: Benutzer? Welche Benutzer? Angelegt haben wir ja noch keine.

Auch das sollten wir dann vielleicht mal machen. Analog zum Auslesen nehmen wir auch hier wieder eine Funktion zur Hand, um die ganze Angelegenheit später leichter in eigene Projekte einbinden zu können:

PHP Quellcode:
/**
 * Diese Funktion legt einen neuen Benutzer an und gibt dessen ID zurück.
 * Im Fehlerfall gibt diese Funktion False zurück.
 * @param String $name Benutzername
 * @param String $password Passwort dazu
 * @param String $email Die E-Mail-Adresse dazu
 * @return int
 */

function createAccount($name,$password,$email)
{
  // auf die konkrete Implementierung gehen wir gleich noch im Einzelnen ein
}


Auch hier müssen wir je nach gewähltem Speicherort wieder im Detail leicht unterschiedlich vorgehen. Die folgenden beiden Abschnitte beschreiben also wieder die Implementierungen je nach Speicherort.

Zugangsdaten in der Datenbank am Beispiel MySQL



Vorüberlegungen



Wir orientieren uns wieder am Datenbank-Aufbau von oben. Ebenfalls wie oben ergänzen wir die Funktion um einen Parameter zur Verknüpfung mit dem konkreten Speicherort.

Implementierung



PHP Quellcode:
/**
 * Diese Funktion legt einen neuen Benutzer an und gibt dessen ID zurück.
 * Im Fehlerfall gibt diese Funktion False zurück.
 * @param MySQLi $dbLink Link zur Datenbank
 * @param String $name Benutzername
 * @param String $password Passwort dazu
 * @param String $email Die E-Mail-Adresse dazu
 * @return int
 */

function MySQLiCreateAccount(mysqli $dbLink,$name,$password,$email)
{
  /* Query vorbereiten:
   *   Es werden drei Werte in die Tabelle Accounts eingefügt: username, Passwort-Hash
   *   und die E-Mail-Adresse. Die User-ID wird per Serialwert automatisch beim
   *   einfügen generiert.
   */

  $sql='INSERT INTO Accounts (username,pwHash,email) VALUES(?,?,?)';

  if(!($stmt=$dbLink->prepare($sql)))
  {
    // Abbruch im Falle eines Fehlers
    trigger_error('Could not prepare statement!',E_USER_ERROR);
    return false;
  }
 
  /* Jetzt kennt unser Datenbank-Server schon mal die INSERT-Anweisung.
   * Nun müssen wir die zugehörigen Werte (also den Benutzernamen, den Passwort-Hash
   * und die E-Mail-Adresse ggf. generieren, und danach ebenfalls an den Datenbank-
   * Server übermitteln.
   */

 
  /* Hash d. PW ermitteln
   *   Zur Ermittlung des Passwort-Hashes verwenden wir die Hash-Funktion.
   *   Die Funktion hash() erlaubt den Zugriff auf eine große Anzahl von
   *   Hash-Verfahren durch eine einzelne Funktion. In diesem Beispiel
   *   verwenden wir SHA512 als Hashmethode. SHA512 ist allerdings nicht auf
   *   allen Servern verfügbar. Alternativ kommen auch SHA1 oder SHA256 als
   *   Hashverfahren in Frage. Prinzipiell gilt: Je größer die Zahl hinter
   *   dem SHA, desto besser ist das Hashverfahren. (Achtung: das gilt nur
   *   für die genannten drei Verfahren.)
   *
   *   Sollte Euer Server die hash()-Funktion nicht kennen, so gibt es zumindest
   *   für SHA1 auch eine eigene Funktion.
   *   Mehr Infos hat das Manual unter http://www.php.net/sha1.
   */

  $inputPwHash=hash('SHA512',$password);
 
  /* Username und E-Mail:
   *   Diese beiden Parameter wurden der Funktion schon so übergeben. Wir
   *   setzen diese also nur noch ein.
   */


  /* Sagen wir's unserer Datenbank:
   *   bind_param() verknüpft eine vorher preparierte SQL-Anweisung oder Abfrage
   *   mit zugehörigen Werten. Der erste Parameter der Funktion gibt dabei an,
   *   wie die übergebenen Werte codiert werden sollen.
   *   Ein "s" entspricht dabei etwa dem altbekannten mysql_real_escape_string(),
   *   ein "i" einem Cast nach Int, usw.
   *   Es ist möglich, der Funktion mehrere Parameter zu übergeben. In diesem
   *   Fall muss die Buchstabenfolge im ersten Parameter der Typenfolge in den
   *   folgenden Parametern entsprechen. Ist der erste Buchstabe also ein s,
   *   dann wird der zweite Parameter als String behandelt. Ein Folgebuchstabe
   *   bezieht sich dann auf den dritten Parameter, usw.
   */

  $stmt->bind_param('sss',$name,$inputPwHash,$email);
  /* So, die SQL-Anweisung/Abfrage und die Parameter sind jetzt beim Datenbank-
   * Server. Der braucht jetzt noch das GO, um diese Abfrage auch wirklich zu
   * tätigen:
   */

  $stmt->execute();
 
  /* Wenn das nicht geklappt hat
   *   Nach jeder Abfrage sollte man den Erfolg kontrollieren.
   *   Bei MySQLi geht das am einfachsten, in dem man die errno-Eigenschaft des
   *   Statement-Objektes auswertet. Ist diese Eigenschaft ungleich 0, so ist ein
   *   Fehler aufgetreten.
   *   Mehr Infos bietet dann die error-Eigenschaft. Diese enthält die Fehlermeldung
   *   in Textform.
   *   ACHTUNG: Fehlertexte sollten auf Produktivservern NIEMALS an den Client
   *            übermittelt werden. Ein potentieller Angreifer kann dadurch
   *            evtl. schädliche Rückschlüsse über mögliche Angriffsvektoren
   *            ziehen.
   */

  if($stmt->errno)
  {
    /* Abbruch. Achtung: Auch wenn ein Nutzername schon vorhanden
     * ist, erfolgt hier der Abbruch.
     */

    return false;
  }
  /* Ansonsten: ID auslesen
   * Wir verwenden hierzu die insert_id()-Methode. Diese liefert den letzten
   * generierten Wert einer Increment-Spalte zurück.
   */

  $id=$dbLink->insert_id();
  /* Aufräumen */
  $stmt->close();
 
  /* und die ID zurück geben: */
  return $id;  
}


Anmerkungen



Insbesondere die Auswertung von $stmt->errno sollte man sich für eigene Projekte eventuell noch einmal anschauen. Eine logische Erweiterung ist, hier die Fehlernummer 1062 getrennt von allen anderen zu betrachten. Der Fall Fehler #1062 tritt immer dann ein, wenn die Unique-Constraint auf unsere Spalte mit den Account-Namen verletzt wurde. Alle anderen Fehlernummern können auf schwerwiegendere Probleme hinweisen, die gegebenenfalls gelogt werden sollten.

Zugangsdaten in einer Datei



Vorüberlegungen



Analog zu oben ergänzen wir wieder den ersten Parameter und gehen vom selben Datenaufbau aus.

Implementierung


PHP Quellcode:
/**
 * Diese Funktion legt einen neuen Benutzer an und gibt dessen ID zurück.
 * Im Fehlerfall gibt diese Funktion False zurück.
 * @param String $filename Name und Pfad der Datendatei
 * @param String $name Benutzername
 * @param String $password Passwort dazu
 * @param String $email Die E-Mail-Adresse dazu
 * @return int
 */

function FileCreateAccount($filename,$name,$password,$email)
{

  /* Daten laden:
   *  Bevor wir irgendwas laden können, müssen wir erstmal sicher stellen, dass
   *  wir dies a) tun können und b) mit dem geladenen auch etwas anfangen
   *  können. Zuerst ist also zu prüfen, ob die Datei selbst existiert, wir sie
   *  lesen dürfen, und ob wir sie nach Veränderungen überhaupt auch wieder
   *  zurück schreiben dürfen.
   */

  if(!(file_exists($filename) && is_readable($filename) && is_writable($filename)))
  {
    /* Ist eines der oben definierten Kriterien nicht erfüllt, brechen wir mit
     * einem Fehler ab. */

    trigger_error('Failed to load password storage file!',E_USER_ERROR);
    return false;
  }

  /* Bei Skripten die Schreibend auf Dateien zugreifen besteht das Risiko von
   * Race Conditions. Gegen diese müssen wir uns Absichern. Hier im Skript sollen
   * Dazu Semaphoren zum Einsatz kommen, so diese denn verfügbar sind.
   * Mehr Informationen zu beiden Themen folgen im weiteren Verlauf dieses Artikels
   */


  if(function_exists('sem_get'))
  {
    // Wir verwenden Semaphoren für das Locking:
    $sem=sem_get(ftok($filename,'P'), 1);
 
    // "Lock beantragen", blociert bis Verfügbarkeit
    sem_acquire($sem);
  }else
    trigger_error('No semaphores available. Proceeding without LOCK!',E_USER_WARNING);
 

  /* Die Daten laden wir erstmal in eine Variable */
  $rawData=file_get_contents($filename);

  /* nun versuchen wir, die Daten in ein PHP-Array zu überführen.
   * Da eine potentielle Fehlermeldung an dieser Stelle Informationen über
   * Passworthashes enthalten könnte, unterdrücken wir die Ausgabe der Fehlermeldung.
   */

  if(!empty($rawData))
    $accountList=@unserialize($rawData);
  else
    $accountList=array(); // Nur im Falle der ersten Initialisierung.

  /* Haben wir denn jetzt wirklich ein Array? */
  if(!is_array($accountList))
  {
    /* Wenn Nein: Abbruch mit Fehler */
    trigger_error('Failed to load password storage file! Format error!',
                  E_USER_ERROR);
    return false;
  }


  /* Hash d. PW ermitteln
   *   Zur Ermittlung des Passwort-Hashes verwenden wir die Hash-Funktion.
   *   Die Funktion hash() erlaubt den Zugriff auf eine große Anzahl von
   *   Hash-Verfahren durch eine einzelne Funktion. In diesem Beispiel
   *   verwenden wir SHA512 als Hashmethode. SHA512 ist allerdings nicht auf
   *   allen Servern verfügbar. Alternativ kommen auch SHA1 oder SHA256 als
   *   Hashverfahren in Frage. Prinzipiell gilt: Je größer die Zahl hinter
   *   dem SHA, desto besser ist das Hashverfahren. (Achtung: das gilt nur
   *   für die genannten drei Verfahren.)
   *
   *   Sollte Euer Server die hash()-Funktion nicht kennen, so gibt es zumindest
   *   für SHA1 auch eine eigene Funktion.
   *   Mehr Infos hat das Manual unter http://www.php.net/sha1.
   */

  $inputPwHash=hash('SHA512',$password);

  /* Dateien und Arrays kennen keine Serialwerte.
   *   Da wir trotzdem einen benötigen, basteln wir uns selber einen.
   *   Alternativ bestünde auch die Möglichkeit, die ID über den Schlüssel
   *   des Account-Arrays zu verwalten.
   */

  $maxID=-1;
 
  /* Durchlaufen unseres Datenbestandes
   *   Motivation ist, zu ermitteln, ob es den neuen Namen schon gibt
   *   und gleichzeitig den bisher höchsten ID-Wert auszulesen.
   */

  $matches=array_filter($accountList,
                        function($elem) use ($name,$inputPwHash,&$maxID)
                        {
                          if($elem['ID']<$maxID)
                            $maxID=$elem['ID'];
                           
                          return ($elem['USERNAME']==$name);
                        });
 
  // wenn es den Namen noch nicht gibt, dann anlegen
  if(!count($matches))
  {
    $accountList[]=array('ID'       =>  ($retVal=($maxID+1)), // nebenbei: Rückg. d. F. merken
                         'USERNAME' =>  $name,
                         'PW_HASH'  =>  $inputPwHash,
                         'EMAIL'    =>  $email);
  }else
    $retVal=false; // Rückgabewert auf False setzen
 
  // und jetzt noch schnell die Datei zurück schreiben:
  file_put_contents($filename,serialize($accountList));
                       
  // release lock
  if(function_exists('sem_remove'))
    sem_remove($sem);
   
  // und zurück geben, ob es den Account mit diesem PW gibt:
  return $retVal;
}


Tipp:

Eine sinnvolle Erweiterung wäre es, diese Funktion an eine Email-Aktivierung, also eine Prüfung der Email-Adresse zu koppeln. Das ist dann allerdings die Hausaufgabe für den geneigten Leser


Anmerkungen


Als wir das erste mal auf Dateien zugegriffen haben, erfolgte der Zugriff nur lesend. In diesem Fall ist das anders, da wir hier neue Daten in eine Datei einfügen wollen. Man stelle sich vor, dass nach dem Auslesen unseres Arrays aus der Datei, aber vor dem Schreiben, ein anderes Skript ebenfalls einen Benutzer anlegt. Dieser Nutzer würde nun unter Umständen verloren gehen, und das gilt es zu verhindern.

Schauen wir uns das Problemszenario noch einmal genauer an:

Skript #1Skript #2
Array aus Datei lesen
Neuen Benutzer "Meier" in Array schreibenArray aus Datei lesen
Array in Datei zurück schreibenNeuen Benutzer "Müller" in Array schreiben
Array in Datei zurück schreiben
Wie klar zu erkennen ist, liest Skript #2 noch das Array ohne "Meier" aus der Datei aus und ergänzt es um den User "Müller". Danach wird das Array ohne "Meier" zurück in die Datei geschrieben, und überschreibt so das Array von Skript #1.

In der Fachwelt wird das beschriebene Szenario als Race Condition bezeichnet. Um diesem Problemfall Herr zu werden, muss sichergestellt werden, dass die Skriptinstanzen komplett durchlaufen, bevor eine andere gleichartige Skriptinstanz irgendwie dazwischen pfuschen kann.

Bewerkstelligen lässt sich sowas am einfachsten durch Locking, also das sperren der Datei für weitere Zugriffe, während ein Skript grade darauf zugreift.

Wir verwenden hier Semaphoren zur Sperre der Datendatei gegenüber anderen Skripten. Semaphoren sind ein Werkzeug, um unterschiedliche Prozesse miteinander kommunizieren zu lassen.

Alternativ bestünde noch die Möglichkeit, Sperrdateien, Shared Memory oder die Funktion flock() zum Einsatz kommen zu lassen. Leider steckt das Thema Locking und Interprozesskommunikation bei PHP noch ziemlich in den Kinderschuhen, so dass keine allgemeingültige Empfehlung für eine der unterschiedlichen Methoden getroffen werden kann. Nicht alle Methoden sind auf allen Systemen überhaupt verfügbar, und ihre jeweils ureigenen Nachteile bringen sie auch alle mit sich.

Grade dieser Punkt ist auch einer der Hauptgründe, warum vom direkten Speichern von Bewegungsdaten in Dateien bei PHP eher abgeraten werden muss: Grade bei stark frequentierten Seiten steigt das Risko von Race Conditions ungemein, und eine wirklich allgemeingültige Lösung zur Absicherung gibt es leider nicht.

Und wo kann ich mich jetzt einloggen?



Sicherlich - bisher können wir nur User anlegen und wieder auf sie zugreifen. Nun müssen wir diese Daten noch irgendwie verwenden, damit auch ein Login möglich ist.

Grundsätzlich ist ein solcher Login nichts anderes als eine einmalige Authentifizierung des Nutzers, die für nachfolgende Requests die Autorisierung zum Zugriff auf ein System sicher stellt. Kling fies, oder?

Eigentlich ist das ganze aber ganz einfach:

Wir benötigen eine Möglichkeit, Informationen über mehrere Seitenaufrufe hinweg einem Benutzer zu zu ordnen. Hierzu verwenden wir das PHP-eigene Session-Handling. Als gute Lektüre zur Einführung sei hier auch der Wiki-Artikel Session Grundfunktion ans Herz gelegt.

Im Schnelldurchlauf:

Wann immer am Anfang eines Skriptes irgendwo ein session_start() auftaucht, steht das superglobale Array $_SESSION zur Verfügung. Alles was wir da rein schreiben steht uns - bei normaler Konfiguration - so lange zur Verfügung, bis der User seinen Browser schließt oder ein definierter Zeitraum ohne Aktion vergangen ist.

Wie nützt uns das jetzt für einen Login? Bei einem Login werden einmal irgendwo Zugangsdaten abgefragt, und dann stehen sie in der Folge als Kriterium für die Zugangserlaubnis zur Verfügung.

Unterstellen wir einfach mal, wir würden einfach immer in das Array-Element $_SESSION['ist_eingeloggt'] ein True rein schreiben, wenn ein Login erfolgreich war. Dieser Unterstellung folgend, müssten wir jetzt nur noch auf allen zu schützenden Seiten einen ganz kleinen Code einfügen, der den Zugriff auf eingeloggte User beschränkt.

Wer da? Kenn ich Dich?



PHP Quellcode:
<?php
  /* Zuerst einmal müssen wir die aktuelle Skriptinstanz an die zum User gehörige
   * Session binden. Dies geschieht durch den Aufruf der Funktion session_start().
   * ACHTUNG: Vor session_start() darf keine weitere Ausgabe erfolgen!
   */

  session_start();
 
  /* Nach Anbindung der Skriptinstanz an die Session stehen in $_SESSION alle vorher
   * in der Session abgelegten Werte zur Verfügung.
   * Wenn kein "ist_eingeloggt"-Wert in der Session existiert, dann hat noch kein
   * Login stattgefunden.
   */

  if(empty($_SESSION['ist_eingeloggt']))
  {
    /* in $_SESSION['ist_eingeloggt'] steht kein True.
     * wir zeigen also ein Login-Formular an:
     */

    require 'loginForm.php';
    /* und beenden das aktuelle Skript */
    exit;
  }

  /* Hier steht dann der schützenswerte Inhalt
   * An diese Stelle gelangen wir durch das exit oben allerdings nur, wenn
   * vorher ein erfolgreicher Login erfolgt ist. Alles was hier unten steht
   * ist also vor unbefugten Augen geschützt.
   */


Login Formular



Ich hab im vorherigen Abschnitt von einem Login-Formular gesprochen. Hierzu legen wir uns eine Datei loginForm.php irgendwo in die Landschaft:

Code (Php):
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
    <title>Welcome to my little page - please provide your credentials</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <style type="text/css">
      /* wir wolln's mal nicht übertreiben :D */
    </style>
  </head>
  <body>
    <div id="contentArea">
      <form action="login.php" method="post">
        <fieldset>
          <legend>Login:</legend>
          <label for="name">Name</label><input type="text" name="name" id="name" /><br />
          <label for="password">Password</label><input type="password" name="password" id="password" /><br />
          <input type="hidden" name="src" value="<?php htmlspecialchars($_SERVER['SCRIPT_NAME']); ?>" />
          <input class="frmBtn" type="reset" name="reset" value="reset" />
          <input class="frmBtn" type="submit" name="submit" value="login" />
        </fieldset>
      </form>
    </div>
  </body>
</html>


Formularauswertung



Und wie kommt jetzt das True in die Session?

In den vergangenen Kapiteln haben wir uns einen Speicher für Benutzerdaten geschaffen. Außerdem haben wir gelernt, dass wir in einer Session Daten über mehrere Aufrufe hinweg mitschleifen können. Wir können auch schon entscheiden, dass ein User keinen Zugriff auf eine schützenswerte Seite hat, und wir können ihm ein Formular zur Eingabe seiner Login-Daten anzeigen.

Jetzt fehlt eigentlich nur noch die Auswertung dieses Formulars. Wie man sieht verweist das Formular selbst auf eine login.php, die legen wir jetzt also als nächstes an. An dieser Stelle ist wieder der Speicherort der Zugangsdaten von Interesse. Je nach gewähltem Speicherort, müssten hier andere der oben erstellten Funktionen Verwendung finden.

Datenbank



PHP Quellcode:
/* Zuerst einmal müssen wir die aktuelle Skriptinstanz an die zum User gehörige
 * Session binden. Dies geschieht durch den Aufruf der Funktion session_start().
 * ACHTUNG: Vor session_start() darf keine weitere Ausgabe erfolgen!
 */

session_start();

/* Zuerst einmal ist zu prüfen, ob überhaupt Formulardaten eingegangen sind:
 * Da unser Formular oben auf POST ausgelegt ist, werten wir hierzu das superglobale
 * Array $_POST aus:
 */

if(empty($_POST['name']) || empty($_POST['password']))
{
  /* Wenn einer der beiden Werte fehlt, zeigen wir einfach das Formular wieder an
   * Eine mögliche Erweiterung wäre, hier eine Fehlermeldung zu definieren, die
   * dann im Formular ausgegeben wird.
   */

  require 'loginForm.php';
  exit;
}

/* Datenbankverbindung herstellen
 * Anmerkung: Ich unterstelle hier, dass die DB-Zugangsdaten und der Datenbankname in
 * der php.ini deklariert sind
 */

$dbLink=new mysqli();

/* Wenn Zugangsdaten falsch
 *   Zur Überprüfung der Daten verwenden wir hier die eingangs definierte Funktion
 *   MySQLiConfirmNameAndPassword(). Diese liefert true zurück, wenn Nutzername
 *   und Passwort gültig sind und verwendet dabei eine MySQL-Datenbank als Datenspeicher.
 */

if(!MySQLiConfirmNameAndPassword($dbLink,$_POST['name'],$_POST['password']))
{
  // Reset des "ist_eingeloggt"-Flags
  $_SESSION['ist_eingeloggt']=false;
  // Abbruch - Hier wäre natürlich das einbinden einer richtigen HTML-Seite angebracht
  die('Is nicht - Zugangsdaten falsch');
}

// Ansonsten: Nutzer freischalten
$_SESSION['ist_eingeloggt']=true;

// Ziel der Weiterleitung ermitteln:
$srcURL=http://www.domain.tld/';
$srcURL.=str_replace(array("\r","\n"),'',$_POST['src']); // Zeilenumbrüche könnten XSS ermöglichen - weg damit

// Umleitung zur aufrufenden Seite:
header('Location: '.$srcURL);


Dateibasiert



PHP Quellcode:
/* Zuerst einmal müssen wir die aktuelle Skriptinstanz an die zum User gehörige
 * Session binden. Dies geschieht durch den Aufruf der Funktion session_start().
 * ACHTUNG: Vor session_start() darf keine weitere Ausgabe erfolgen!
 */

session_start();

/* Zuerst einmal ist zu prüfen, ob überhaupt Formulardaten eingegangen sind:
 * Da unser Formular oben auf POST ausgelegt ist, werten wir hierzu das superglobale
 * Array $_POST aus:
 */

if(empty($_POST['name']) || empty($_POST['password']))
{
  /* Wenn einer der beiden Werte fehlt, zeigen wir einfach das Formular wieder an
   * Eine mögliche Erweiterung wäre, hier eine Fehlermeldung zu definieren, die
   * dann im Formular ausgegeben wird.
   */

  require 'loginForm.php';
  exit;
}

// Hier wird Name und Pfad der Passwort-Datei hinterlegt
$credentialsFile='/path/2/credentials.dat';

/* Wenn Zugangsdaten falsch
 *   Zur Überprüfung der Daten verwenden wir hier die eingangs definierte Funktion
 *   FileConfirmNameAndPassword(). Diese liefert true zurück, wenn Nutzername
 *   und Passwort gültig sind und verwendet dabei eine Datendatei als Datenspeicher.
 */

if(!FileConfirmNameAndPassword($credentialsFile,$_POST['name'],$_POST['password']))
{
  // Reset des "ist_eingeloggt"-Flags
  $_SESSION['ist_eingeloggt']=false;

  // Abbruch - Hier wäre natürlich das einbinden einer richtigen HTML-Seite angebracht
  die('Is nicht - Zugangsdaten falsch');
}
// Ansonsten: Nutzer freischalten
$_SESSION['ist_eingeloggt']=true;

// Ziel der Weiterleitung ermitteln:
$srcURL=http://www.domain.tld/';
$srcURL.=str_replace(array("\r","\n"),'',$_POST['src']); // Zeilenumbrüche könnten XSS ermöglichen - weg damit

// Umleitung zur aufrufenden Seite:
header('Location: '.$srcURL);


Nachwort



Das ist jetzt natürlich nur die absolut simpelste Form einer Loginprüfung. Vertiefend seien die Wiki-Artikel EVA Prinzip, Affenformular und Dynamischer Content genannt, um das ganze ein wenig eleganter zu gestalten.

Die Pflicht ist getan - jetzt kommt die Kür



So, der Login geht. Wir haben prinzipiell gelernt, wie wir Seiten rudimentär gegen ein Betreten durch Unbefugte absichern.

Tipp:
Rudimentär? Warum Rudimentär? Ein System ist immer nur so stark, wie sein schwächstes Glied. Momentan gibt es noch jede Menge Risiken. Session Sicherheit bietet einige gute Ansätze, andere wären der Einsatz von HTTPS als Übertragungsprotokoll und diverse Plausibilitätsprüfungen für Benutzernamen und Passwörter.


Ihr Kinderlein kommet - oder auch User, registriert Euch


Jetzt brauchen wir aber noch ein paar Benutzer. Auch nicht ganz unnormal wäre, dass Benutzern erlaubt wird, sich selbst am System zu registrieren.

Mit den oben gezeigten Funktionen zum Schreiben von Usern ist auch das relativ einfach zu bewerkstelligen. Fangen wir an mit einem Formular dafür.

Registrierungsformular



Ganz ohne große Worte - register.html

Code (Html):
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
    <title>Welcome to my little page - Join my world</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <style type="text/css">
      /* wir wolln's mal nicht übertreiben :D */
    </style>
  </head>
  <body>
    <div id="contentArea">
      <form action="register.php" method="post">
        <fieldset>
          <legend>User Registration:</legend>
          <label for="name">Name</label><input type="text" name="name" id="name" /><br />
          <label for="password">Password</label><input type="password" name="password" id="password" /><br />
          <label for="password2">retype Password</label><input type="password" name="password2" id="password2" /><br />
          <label for="email">E-Mail</label><input type="text" name="email" id="email" /><br />
          <input class="frmBtn" type="reset" name="reset" value="reset" />
          <input class="frmBtn" type="submit" name="submit" value="Join Now" />
        </fieldset>
      </form>
    </div>
  </body>
</html>


Formularauswertung



Bei der Auswertung muss wieder der oben gewählte Speicherort berücksichtigt werden. Es gibt also wieder zwei Versionen, jenachdem ob eine Datenbank oder eine Datei zum Einsatz kommt.

Datenbank


PHP Quellcode:
/* Zuerst einmal müssen wir die aktuelle Skriptinstanz an die zum User gehörige
 * Session binden. Dies geschieht durch den Aufruf der Funktion session_start().
 * ACHTUNG: Vor session_start() darf keine weitere Ausgabe erfolgen!
 */

session_start();

/* Zuerst einmal ist zu prüfen, ob überhaupt Formulardaten eingegangen sind:
 * Da unser Formular oben auf POST ausgelegt ist, werten wir hierzu das superglobale
 * Array $_POST aus:
 */

if(empty($_POST['name']) || empty($_POST['password'])
  || empty($_POST['password2']) || empty($_POST['email']))
{
  /* Wenn einer der beiden Werte fehlt, zeigen wir einfach das Formular wieder an
   * Eine mögliche Erweiterung wäre, hier eine Fehlermeldung zu definieren, die
   * dann im Formular ausgegeben wird.
   */

  readfile('registerForm.html');
  exit;
}

/* Hier erfolgt die Plausibilitätsprüfung der eingegangenen Daten:
 *   - Der Name soll keine "HTML-Sonderzeichen" enthalten und nicht mit Leerzeichen
 *     starten oder beginnen.
 *   - Der Name soll min. 4 und max. 50 Zeichen lang sein (Typ: VARCHAR(50))
 *   - Beide Passwörter sollen übereinstimmen
 *   - Das Passwort soll mindestens 8 Zeichen lang sein
 *   - Das Passwort soll aus den vier Zeichengruppen Kleinbuchstaben, Großbuchstaben,
 *     Zahlen und Sonderzeichen mindestens drei verwenden.
 *   - Die E-Mail-Adresse soll formal gültig sein. Ein Funktionstest findet nicht statt.
 *
 *   Wir überprüfen jetzt jedes der Kriterien einzeln und legen ggf. eine Fehlermeldung
 *   in einem Array ab. Wenn nach diesen Tests das Array nicht leer ist, dann verweigern
 *   wir die Verarbeitung der eingangsdaten mit einem Fehler.
 */

$errors=array();

/*   - Der Name soll keine "HTML-Sonderzeichen" enthalten und nicht mit Leerzeichen
 *     starten oder beginnen.
 */

if(trim(htmlspecialchars($_POST['name'],ENT_QUOTES,'UTF-8'))!=$_POST['name'])
  $errors[]='Der Name enthält ungültige Zeichen.';

/*   - Der Name soll min. 4
 */

if(strlen($_POST['name'])<4)
  $errors[]='Der Name muss mindestens vier Zeichen lang sein.';

/* und max. 50 Zeichen lang sein (Typ: VARCHAR(50))
 */

if(strlen($_POST['name'])>50)
  $errors[]='Der Name darf nicht länger als fünfzig Zeichen sein.';

/*   - Beide Passwörter sollen übereinstimmen
 */

if($_POST['password']!=$_POST['password2'])
  $errors[]='Passwörter stimmen nicht überein.';

/*   - Das Passwort soll mindestens 8 Zeichen lang sein
 */

if(strlen($_POST['password'])<8)
  $errors[]='Das Passwort muss mindestens acht Zeichen lang sein.';  

/*   - Das Passwort soll aus den vier Zeichengruppen Kleinbuchstaben, Großbuchstaben,
 *     Zahlen und Sonderzeichen mindestens drei verwenden.
 */

$checksum=0;
$checksum+=preg_match('#[A-Z]#',$_POST['password']);
$checksum+=preg_match('#[a-z]#',$_POST['password']);
$checksum+=preg_match('#[0-9]#',$_POST['password']);
$checksum+=preg_match('#[^A-Z0-9]#i',$_POST['password']);

if($checksum<3)
  $errors[]='Das Passwort ist nicht komplex genug';

/*   - Die E-Mail-Adresse soll formal gültig sein. Ein Funktionstest findet nicht statt.
 */

if(!filter_var($_POST['email'],VALIDATE_EMAIL))
  $errors[]='E-Mail-Adresse nicht gültig.';
 
// Keine Fehler bisher?
if(!count($errors))
{
  /* Datenbankverbindung herstellen
   * Anmerkung: Ich unterstelle hier, dass die DB-Zugangsdaten und der Datenbankname in
   * der php.ini deklariert sind
   */

  $dbLink=new mysqli();
 
  /* Und nun legen wir unseren Account an.
   *   Wir verwenden hierzu die am Anfang deklarierte Funktion MySQLiCreateAccount()
   */

  $id=MySQLiCreateAccount($dbLink,$_POST['name'],$_POST['password'],$_POST['email']);
 
  /* Und dann werten wir noch aus, ob das geklappt hat */
  if($id===false)
    $errors[]='Den Benutzer gibt es schon!';
}
if(count($errors))
  die("Eingabefehler: <ul>\n<li>\n".implode("</li>\n<li>",$errors)."</li>\n</ul>\n");

header('http://www.domain.tld/path/2/startseite.html');


Datei


Tja, so langsam wirds vorhersehbar. Im Datei-Fall sieht das ganze ganz genauso aus - nur halt mit der anderen Funktion für die Benutzeranlage:

PHP Quellcode:
/* Zuerst einmal müssen wir die aktuelle Skriptinstanz an die zum User gehörige
 * Session binden. Dies geschieht durch den Aufruf der Funktion session_start().
 * ACHTUNG: Vor session_start() darf keine weitere Ausgabe erfolgen!
 */

session_start();

/* Zuerst einmal ist zu prüfen, ob überhaupt Formulardaten eingegangen sind:
 * Da unser Formular oben auf POST ausgelegt ist, werten wir hierzu das superglobale
 * Array $_POST aus:
 */

if(empty($_POST['name']) || empty($_POST['password'])
  || empty($_POST['password2']) || empty($_POST['email']))
{
  /* Wenn einer der beiden Werte fehlt, zeigen wir einfach das Formular wieder an
   * Eine mögliche Erweiterung wäre, hier eine Fehlermeldung zu definieren, die
   * dann im Formular ausgegeben wird.
   */

  readfile('registerForm.html');
  exit;
}


/* Hier erfolgt die Plausibilitätsprüfung der eingegangenen Daten:
 *   - Der Name soll keine "HTML-Sonderzeichen" enthalten und nicht mit Leerzeichen
 *     starten oder beginnen.
 *   - Der Name soll min. 4 und max. 50 Zeichen lang sein (Typ: VARCHAR(50))
 *   - Beide Passwörter sollen übereinstimmen
 *   - Das Passwort soll mindestens 8 Zeichen lang sein
 *   - Das Passwort soll aus den vier Zeichengruppen Kleinbuchstaben, Großbuchstaben,
 *     Zahlen und Sonderzeichen mindestens drei verwenden.
 *   - Die E-Mail-Adresse soll formal gültig sein. Ein Funktionstest findet nicht statt.
 *
 *   Wir überprüfen jetzt jedes der Kriterien einzeln und legen ggf. eine Fehlermeldung
 *   in einem Array ab. Wenn nach diesen Tests das Array nicht leer ist, dann verweigern
 *   wir die Verarbeitung der eingangsdaten mit einem Fehler.
 */

$errors=array();

/*   - Der Name soll keine "HTML-Sonderzeichen" enthalten und nicht mit Leerzeichen
 *     starten oder beginnen.
 */

if(trim(htmlspecialchars($_POST['name'],ENT_QUOTES,'UTF-8'))!=$_POST['name'])
  $errors[]='Der Name enthält ungültige Zeichen.';

/*   - Der Name soll min. 4 Zeichen
 */

if(strlen($_POST['name'])<4)
  $errors[]='Der Name muss mindestens vier Zeichen lang sein.';

/* und max. 50 Zeichen lang sein (Typ: VARCHAR(50))
 */

if(strlen($_POST['name'])>50)
  $errors[]='Der Name darf nicht länger als fünfzig Zeichen sein.';

/*   - Beide Passwörter sollen übereinstimmen
 */

if($_POST['password']!=$_POST['password2'])
  $errors[]='Passwörter stimmen nicht überein.';

/*   - Das Passwort soll mindestens 8 Zeichen lang sein
 */

if(strlen($_POST['password'])<8)
  $errors[]='Das Passwort muss mindestens acht Zeichen lang sein.';  

/*   - Das Passwort soll aus den vier Zeichengruppen Kleinbuchstaben, Großbuchstaben,
 *     Zahlen und Sonderzeichen mindestens drei verwenden.
 */

$checksum=0;
$checksum+=preg_match('#[A-Z]#',$_POST['password']);
$checksum+=preg_match('#[a-z]#',$_POST['password']);
$checksum+=preg_match('#[0-9]#',$_POST['password']);
$checksum+=preg_match('#[^A-Z0-9]#i',$_POST['password']);

if($checksum<3)
  $errors[]='Das Passwort ist nicht komplex genug';

/*   - Die E-Mail-Adresse soll formal gültig sein. Ein Funktionstest findet nicht statt.
 */

if(!filter_var($_POST['email'],VALIDATE_EMAIL))
  $errors[]='E-Mail-Adresse nicht gültig.';
 
// Keine Fehler bisher?
if(!count($errors))
{
  // Hier wird Name und Pfad der Passwort-Datei hinterlegt
  $credentialsFile='/path/2/credentials.dat';
 
  /* Und nun legen wir unseren Account an.
   *   Wir verwenden hierzu die am Anfang deklarierte Funktion FileCreateAccount()
   */

  $id=FileCreateAccount($credentialsFile,
                        $_POST['name'],
                        $_POST['password'],
                        $_POST['email']);
 
  if($id===false)
    $errors[]='Den Benutzer gibt es schon!';
}
if(count($errors))
  die("Eingabefehler: <ul>\n<li>\n".implode("</li>\n<li>",$errors)."</li>\n</ul>\n");

header('http://www.domain.tld/path/2/startseite.html');


Benutzerrechte/Autorisierung



Mit den bisher gezeigten Techniken ist es möglich, Benutzer zu Authentifizieren. Unter Verwendung der gezeigten Skripte weiß PHP jetzt, wer vor dem Browser sitzt. Das ist aber noch nicht alles. Die andere Hälfte der ganzen Angelegenheit ist, aus diesem Wissen abzuleiten, was das besagte "Wer" denn jetzt eigentlich mit der Anwendung alles so anstellen darf. Das zugehörige Theoretische Konzept trägt den Namen Autorisierung und wird voraussichtlich noch Bestandteil eines anderen Artikels hier im Wiki werden. Da besagter Artikel aber bisher noch halbfertig auf einer gewissen Festplatte liegt, empfehlen sich die Stichworte ACL und RBAC für die Suche nach weiteren Informationen.

Links


Authentication (EN, Wikipedia)
Login-Tutorial von Quakenet

« Vorheriges Kapitel   Tutorials
  Nächstes Kapitel »

Erstellt von Bernd456, 20.09.2008 am 00:45
Zuletzt bearbeitet von Jens Clasen, 02.03.2011 am 14:31
24 Kommentare , 35116 Betrachtungen

Dieser Text steht unter der GNU-Lizenz für freie Dokumentation


 

Lesezeichen

Stichworte
login, mysql, passwort, session, tutorial, user

Artikel-Optionen
Ansicht

Forumregeln
Es ist Ihnen nicht erlaubt, neue Themen zu verfassen.
Es ist Ihnen nicht erlaubt, auf Beiträge zu antworten.
Es ist Ihnen nicht erlaubt, Anhänge hochzuladen.
Es ist Ihnen nicht erlaubt, Ihre Beiträge zu bearbeiten.

BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus.

Gehe zu
Ähnliche Themen
Thema Autor Forum Antworten Letzter Beitrag
geschützte Dateien im Login-Bereich Tinus PHP 10 13.01.2008 01:46
Zwei getrennte Login-Bereiche mit Sessions? Need Help hokkaro PHP 4 12.12.2007 13:04
Geschützte Bereiche geeforce PHP 6 10.08.2007 13:15
Login Bereiche dr.ache PHP 7 27.10.2005 02:04
Geschützte Ordner? sephir0th PHP 2 28.01.2004 11:34


Alle Zeitangaben in WEZ +2. Es ist jetzt 13:12 Uhr.


Powered by vBulletin® Version 3.8.8 (Deutsch)
Copyright ©2000 - 2018, Jelsoft Enterprises Ltd.
Powered by NuWiki v1.3 RC1 Copyright ©2006-2007, NuHit, LLC