Benutzer:Suit/Loginsystem und Benutzerregistrierung mit PHP und MySQL

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Achtung!

Artikel in Benutzernamensräumen sind möglicherweise nicht mehr aktuell oder gar fehlerhaft. Sie sind auf jeden Fall nicht Teil der offiziellen Dokumentation. Ein Großteil dieses Artikels ist inzwischen unter PHP/Tutorials/Loginsystem zu finden.

Wenn man Teile eines Internetauftritts vor neugierigen Augen schützen will, benötigt man ein wirksames System zur Zugangskontrolle. In vielen Fällen bietet sich hier die bekannte HTTP-Authentifizierung an, die man beispielsweise per .htaccess-Datei umsetzen kann. Diese Methode hat aber den Nachteil, dass es auch in modernen Browsern immer noch keine praktikable Möglichkeit gibt, sich wieder abzumelden. Ebenso können die zur Anmeldung gehörenden Informationen nicht nahtlos in die Gestaltung der Seite eingefügt werden. Dieser Artikel soll zeigen, wie man Dokumente auf relativ einfache Weise sinnvoll schützen kann. Voraussetzung dafür ist ein Webserver mit PHP- und MySQL-Unterstützung.

Grundlagen[Bearbeiten]

Ob ein Benutzer angemeldet ist, erkennt das System mittels einer Sitzung (engl. Session). In PHP ist eine Session im Prinzip eine Datei, die sich im Dateisystem des Webservers befindet. In ihr sind Daten gespeichert, auf die man über das superglobale Array $_SESSION zugreifen kann. Jeder Benutzer, der mit seinem Browser eine Seite aufruft, die Sessions benutzt, bekommt nun vom Server eine einmalige Session-ID zugewiesen, über die er identifiziert werden kann. Dadurch ist es möglich, Daten benutzerbezogen zu speichern, die während einer Sitzung wiederverwendet werden können. Anwendungsmöglichkeiten sind zum Beispiel ein Warenkorb oder eine Administrationsoberfläche für ein Content-Management-System.

Das hier beschriebene Loginsystem umfasst die 5 Dateien register.php, login.php, logout.php, auth.php und index.php, welche sich alle im selben Verzeichnis befinden. Die Datei register.php ist dazu in der Lage neue Benutzer in die Datenbank einzutragen. Die Datei login.php beinhaltet sowohl das Formular zum Anmelden als auch die Routinen, um die Benutzerdaten zu verarbeiten und bei erfolgreicher Anmeldung entsprechende Daten in die Session zu speichern. Die Datei logout.php entfernt sämtliche Sitzungsdaten und meldet so den Benutzer ab. auth.php enthält den essentiellen Teil, nämlich die Überprüfung, ob der Benutzer aktuell angemeldet ist und somit berechtigt, das angeforderte Dokument anzusehen. Diese Datei wird in jedes zu schützende Dokument eingebunden, als Beispiel hierfür dient die Datei index.php.

Verwalten von Benutzerdaten[Bearbeiten]

Als Speicherort für die Benutzerdaten werden wir eine Tabelle in einer MySQL-Datenbank verwenden. Um überhaupt Daten in dieser anzulegen, muss zuerst eine entsprechende Tabelle erstellt werden, dazu legen wir zuerst einen neuen Katalog (Datenbank) an und erstellen danach die Tabelle mit folgendem SQL-Statement.

CREATE TABLE IF NOT EXISTS 'users' (
  'id'       int(11) NOT NULL AUTO_INCREMENT,
  'username' varchar(255) NOT NULL,
  'password' varchar(255) NOT NULL,
  PRIMARY KEY ('id'),
  UNIQUE KEY 'username' ('username')
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

In dieser Benutzertabelle können selbstverständlich auch weitere Benutzerinformationen, wie z.B. Name, die E-Mail-Adresse oder sonstiges gespeichert werden - in der Praxis ist es oft sinnvoll, Merkmale wie den Zeitpunkt der Registrierung und der letzten Anmeldung zu protokollieren, in diesem Artikel beschränken wir uns allerdings auf das wesentliche.

Die maximale Länge der Feldwerte wurde willkürlich gewählt, in der Praxis kann es sinnvoll sein die Beschränkungen nach den Anforderungen zu überdenken. So muss z.B. ein Benutzername nicht notwendigerweise eindeutig sein, ebenso eignet sich auch die E-Mail-Adresse oder eine Kundennummer als Eindeutigkeitsmerkmal.

Beachten Sie: im Falle des Kennworts (engl. password) korreliert die Längenbeschränkung des Datenfeldes nicht mit der maximal möglichen Länge des Passworts.

Registrieren eines neuen Benutzers[Bearbeiten]

Wir geben dem Benutzer selbst die Möglichkeit ein Benutzerkonto anzulegen. Dies geschieht mit einem einfachen Affenformular welches die Eingaben überprüft und bei Erfolg einen neuen Benutzer in die Benutzertabelle einträgt.

Damit das Kennwort nicht im Klartext in der Datenbank gespeichert werden muss, überführen wir es in einen abgeleiteten Schlüssel (engl. derived key). Im Beispiel verwenden wir als Verfahren bcrypt: hierbei wird der Klartext mit einer zufälligen Zeichenfolge (Salt) abhängig eines Kostenfaktors (engl. cost factor) mehrfach wiederholt verknüpft. Das Endergebnis ist eine Zeichenkette die sich aus der Information über den Algorithmus, den Kostenfaktor, den Salt-Wert und den Rückgabewert der letzten Iteration zusammensetzt. Gegenüber einem einfachen Streuwert besitzt ein abgeleiteter Schlüssen den Vorteil, dass auch für zwei identische Ursprungszeichenketten völlig unterschiedliche Schlüssel entstehen.

$2a$10$qNWE9w7eMFknNw5Wm5rGKehVw7.SCExw3a4KPKHaP7YvZqBpbyOm6
 |  |  |                     | 
 |  |  |                     Schlüsseltext: 31 Zeichen (24 Byte, in Base64) 
 |  |  Salt: 22 Zeichen (16 Byte, in Base64) 
 |  Kostenfaktor: 10 entspricht 2^10 = 1024 Wiederholungen
 Algorithmus: 2a steht für die aktuelle Version von bcrypt

Anmerkung: im Gegensatz zu einer Standard-Base64-Darstellung welche in RFC 4648 beschrieben wird, ist hier das + durch ein . ersetzt.

Da PHP erst ab Version 5.5 eine vollständige bcrypt-Implementierung bereitstellt nutzen wir hier im Beispiel die crypt-Funktion und generieren den Salt-Wert mit mt_rand() selbst. Diese Funktion ist kryptologisch zwar nicht ausreichend sicher, da der Salt-Wert aber ohnehin einen öffentlichen Teil des abgeleiteten Schlüssels darstellt, können wir diesen Umstand vernachlässigen. Zu seinem späteren Zeitpunkt kann die Funktion recht einfach durch password_hash() ersetzt werden, welche die auch die Erzeugung des Salt-Wertes selbst durchführen kann.

$salt = ; 
for ($i = 0; $i < 22; $i++) { 
	$salt .= substr('./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', mt_rand(0, 63), 1); 
}
$_POST['f']['password'] = crypt(
	$_POST['f']['password'],
	'$2a$10$' . $salt
);

Vollständiger Quelltext[Bearbeiten]

Benutzerdatenbank in MySQL[Bearbeiten]

SQL-Tabelle für die Benutzerdaten
CREATE TABLE IF NOT EXISTS 'users' (
  'id'       int(11) NOT NULL AUTO_INCREMENT,
  'username' varchar(255) NOT NULL,
  'password' varchar(255) NOT NULL,
  PRIMARY KEY ('id'),
  UNIQUE KEY 'username' ('username')
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Registrierung von neuen Benutzern[Bearbeiten]

register.php
<?php
	$message = array();
	if (!empty($_POST)) {
		if (
			empty($_POST['f']['username']) ||
			empty($_POST['f']['password']) ||
			empty($_POST['f']['password_again'])
		) {
			$message['error'] = 'Es wurden nicht alle Felder ausgefüllt.';
		} else if ($_POST['f']['password'] != $_POST['f']['password_again']) {
			$message['error'] = 'Die eingegebenen Passwörter stimmen nicht überein.';
		} else {
			unset($_POST['f']['password_again']);
			$salt = ''; 
			for ($i = 0; $i < 22; $i++) { 
				$salt .= substr('./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', mt_rand(0, 63), 1); 
			}
			$_POST['f']['password'] = crypt(
				$_POST['f']['password'],
				'$2a$10$' . $salt
			);
			
			$mysqli = @new mysqli('localhost', 'root', '', 'loginsystem');
			if ($mysqli->connect_error) {
				$message['error'] = 'Datenbankverbindung fehlgeschlagen: ' . $mysqli->connect_error;
			}
			$query = sprintf(
				"INSERT INTO users (username, password)
				SELECT * FROM (SELECT '%s', '%s') as new_user
				WHERE NOT EXISTS (
					SELECT username FROM users WHERE username = '%s'
				) LIMIT 1;",
				$mysqli->real_escape_string($_POST['f']['username']),
				$mysqli->real_escape_string($_POST['f']['password']),
				$mysqli->real_escape_string($_POST['f']['username'])
			);
			$mysqli->query($query);
			if ($mysqli->affected_rows == 1) {
				$message['success'] = 'Neuer Benutzer (' . htmlspecialchars($_POST['f']['username']) . ') wurde angelegt, <a href="login.php">weiter zur Anmeldung</a>.';
				header('Location: http://' . $_SERVER['HTTP_HOST'] . '/login.php');
			} else {
				$message['error'] = 'Der Benutzername ist bereits vergeben.';
			}
			$mysqli->close();
		}
	} else {
		$message['notice'] = 'Übermitteln Sie das ausgefüllte Formular um ein neues Benutzerkonto zu erstellen.';
	}
?><!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<title>loginsystem - register.php</title>
	</head>
	<body>
		<form action="./register.php" method="post">
<?php if (isset($message['error'])): ?>
			<fieldset class="error"><legend>Fehler</legend><?php echo $message['error'] ?></fieldset>
<?php endif;
	if (isset($message['success'])): ?>
			<fieldset class="success"><legend>Erfolg</legend><?php echo $message['success'] ?></fieldset>
<?php endif;
	if (isset($message['notice'])): ?>
			<fieldset class="notice"><legend>Hinweis</legend><?php echo $message['notice'] ?></fieldset>
<?php endif; ?>
			<fieldset>
				<legend>Benutzerdaten</legend>
				<div><label for="username">Benutzername</label> <input type="text" name="f[username]" id="username"<?php echo isset($_POST['f']['username']) ? ' value="' . htmlspecialchars($_POST['f']['username']) . '"' : '' ?> /></div>
				<div><label for="password">Kennwort</label> <input type="password" name="f[password]" id="password" /></div>
				<div><label for="password_again">Kennwort wiederholen</label> <input type="password" name="f[password_again]" id="password_again" /></div>
			</fieldset>
			<fieldset>
				<div><input type="submit" name="submit" value="Registrieren" /></div>
			</fieldset>
		</form>
	</body>
</html>

Anmelden von Benutzern[Bearbeiten]

login.php
<?php
if (isset($_SESSION['login'])) {
	header('Location: http://' . $_SERVER['HTTP_HOST'] . '/index.php');
} else {
	if (!empty($_POST)) {
		if (
			empty($_POST['f']['username']) ||
			empty($_POST['f']['password'])
		) {
			$message['error'] = 'Es wurden nicht alle Felder ausgefüllt.';
		} else {
			$mysqli = @new mysqli('localhost', 'root', '', 'loginsystem');
			if ($mysqli->connect_error) {
				$message['error'] = 'Datenbankverbindung fehlgeschlagen: ' . $mysqli->connect_error;
			} else {
				$query = sprintf(
					"SELECT username, password FROM users WHERE username = '%s'",
					$mysqli->real_escape_string($_POST['f']['username'])
				);
				$result = $mysqli->query($query);
				if ($row = $result->fetch_array(MYSQLI_ASSOC)) {
					if (crypt($_POST['f']['password'], $row['password']) == $row['password']) {
						session_start();
						
						$_SESSION = array(
							'login' => true,
							'user'  => array(
								'username'  => $row['username']
							)
						);
						$message['success'] = 'Anmeldung erfolgreich, <a href="index.php">weiter zum Inhalt.';
						header('Location: http://' . $_SERVER['HTTP_HOST'] . '/index.php');
					} else {
						$message['error'] = 'Das Kennwort ist nicht korrekt.';
					}
				} else {
					$message['error'] = 'Der Benutzer wurde nicht gefunden.';
				}
				$mysqli->close();
			}
		}
	} else {
		$message['notice'] = 'Geben Sie Ihre Zugangsdaten ein um sich anzumelden.<br />' .
			'Wenn Sie noch kein Konto haben, gehen Sie <a href="./register.php">zur Registrierung</a>.';
	}
}
?>
<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<title>loginsystem - login.php</title>
	</head>
	<body>
		<form action="./login.php" method="post">
<?php if (isset($message['error'])): ?>
			<fieldset class="error"><legend>Fehler</legend><?php echo $message['error'] ?></fieldset>
<?php endif;
	if (isset($message['success'])): ?>
			<fieldset class="success"><legend>Erfolg</legend><?php echo $message['success'] ?></fieldset>
<?php endif;
	if (isset($message['notice'])): ?>
			<fieldset class="notice"><legend>Hinweis</legend><?php echo $message['notice'] ?></fieldset>
<?php endif; ?>
			<fieldset>
				<legend>Benutzerdaten</legend>
				<div><label for="username">Benutzername</label>
					<input type="text" name="f[username]" id="username"<?php 
					echo isset($_POST['f']['username']) ? ' value="' . htmlspecialchars($_POST['f']['username']) . '"' : '' ?> /></div>
				<div><label for="password">Kennnwort</label> <input type="password" name="f[password]" id="password" /></div>
			</fieldset>
			<fieldset>
				<div><input type="submit" name="submit" value="Anmelden" /></div>
			</fieldset>
		</form>
	</body>
</html>

Überprüfung der Sitzungsdaten[Bearbeiten]

auth.php
<?php
	session_start();
	session_regenerate_id();

	if (empty($_SESSION['login'])) {
		header('Location: http://' . $_SERVER['HTTP_HOST'] . '/login.php');
		exit;
	} else {
		$login_status = '
			<div style="border: 1px solid black">
				Sie sind als <strong>' . htmlspecialchars($_SESSION['user']['username']) . '</strong> angemeldet.<br />
				<a href="./logout.php">Sitzung beenden</a>
			</div>
		';
	}
?>
index.php
<?php require_once './auth.php'; ?>
<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<title>loginsystem - index.php</title>
	</head>
	<body>
		<?php echo $login_status; ?>
		<h1>Inhalt</h1>
		<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr.</p>
	</body>
</html>

Abmelden und beenden einer Sitzung[Bearbeiten]

logout.php
<?php
	session_start();
	$_SESSION = array();
	if (ini_get('session.use_cookies')) {
		$params = session_get_cookie_params();
		setcookie(
			session_name(),
			'',
			time() - 42000,
			$params['path'],
			$params['domain'],
			$params['secure"'],
			$params['httponly']
		);
	}
	session_destroy();
	header('Location: ./login.php');
?>