[Edit]
Klassen-Beschreibung im 2. Post begonnen
Kleinen Fehler in der Methode registerUser behoben, der dazu führte, dass neue User im mit isActive=0 registriert wurden, auch wenn durch Übergabe des Parameters $requiresActivation=false festgelegt wurde, dass keine Aktivierung nötig ist.
[/Edit]
Hier habe ich mal eine kleine User-Klasse mit ein paar Grundfunktionalitäten. Ist jetzt nicht besonders umfangreich, kann aber recht problemlos erweitert werden (weiter User-Felder z.B.).
Benötigt:
PHP5
MySQLi (OOP-Stil)
User können sich registrieren, hierbei kann per Parameter festgelegt werden, ob eine Aktivierung erforderlich ist oder der User direkt freigeschaltet wird.
Ist eine Aktivierung erforderlich wird beim registrieren ein Aktivierungscode erstellt und zurückgegeben. Mit diesem Aktivierungscode und der id des soeben angelegten Users kann der User dann freigeschaltet werden, bzw ein Link generiert werden den man dem User per Email schickt, wie ihr das macht ist eure Sache.
Für das User-Login kann festgelegt werden, ob IP-Adressen nach einer bestimmten Anzahl fehlgeschlagener Login-Versuche in einem bestimmten Zeitraum für einen bestimmten Zeitraum gesperrt werden sollen.
Die entsprechende Anzahl und der entsprechende Zeitraum können in den Klassen-Konstanten angepasst werden.
User.class.php
<?php
class LoginException extends Exception {}
class UserException extends Exception {}
class RegisterException extends Exception {}
class User {
// Settings for registration and login
const activationExpires = 14; // Number of days after which Activation-Code for new User expires
const IP_maxLoginTries = 5; // Number of failed Login-Tries after which IP is blocked, set NULL for disabling IP-Blocking
const IP_blockDuration = 15; // Number of minutes for which IP is blocked after reaching max failed login-tries
// MySQLi-Object
private $db;
// Array for caching User-Data
private $cache = array();
// Allowed fields to be retrieved and altered via magic methods __get() and __set()
private $fields = array('email','name','ipAdress','isActive');
// id of current user
private $id;
// Constructor expects MySQLi-Object as first Parameter and user_id as (optional) second Parameter
public function __construct(mysqli $db,$user_id=0) {
$this->db = $db;
if((int)$user_id) $this->id = (int)$user_id;
}
// Magic Method __get provides user-data from the user-table
public function __get($key) {
// first of all, check if requested field exists and is allowed, if so, it is contained in the fields-array
if(!in_array($key,$this->fields)) {
throw new UserException ("Das gewünschte Feld existiert nicht oder darf nicht abgefragt werden.",10);
}
// Check if the requested field is already cached
if(!isset($this->cache[$key])) {
// if not cached, try to get it from the database user-table...
$sql = "SELECT $key FROM user WHERE id=?";
$res = $this->db->prepare($sql);
$res->bind_param("i",$this->id);
$res->bind_result($value);
$res->execute();
if ($res->fetch()) {
// if successfull, write it into the cache-array
$res->free_result();
$this->cache[$key] = $value;
}
else {
throw new UserException ("Das gewünschte Feld existiert nicht.",10);
}
}
// return the requested field-value
return $this->cache[$key];
}
// Magic Method __set can be used to update user-fields
public function __set($key, $value) {
// first of all, check if requested field exists and is allowed, if so, it is contained in the fields-array
if(!in_array($key, $this->fields)) {
throw new UserException ("Das gewünschte Feld existiert nicht oder darf nicht geändert werden.",30);
}
// Try to update the user-table
$sql = "UPDATE user SET $key=? WHERE id=?";
$res = $this->db->prepare($sql);
$res->bind_param("si",$value,$this->id);
if(!$res->execute()) {
// if update wasn't successfull throw exception and return
throw new UserException ("Das gewünschte Feld konnte nicht aktualsiert werden.",20);
}
else {
// otherwise, update the cache-array
$this->cache[$key] = $value;
}
}
// Check if login-data is valid
public function checkLogin($email,$pass,$ipAdress=NULL) {
if(!empty($email) && !empty($pass)) {
if($ipAdress===NULL || !self::IP_maxLoginTries || $this->getIpPermission($ipAdress)) {
$sql = "SELECT id,password,isActive FROM user WHERE email=?";
$res = $this->db->prepare($sql);
$res->bind_param("s",$email);
$res->bind_result($id,$dbPass,$isActive);
$res->execute();
if($res->fetch()) {
$res->free_result();
if($isActive) {
$salt = substr($dbPass,-23);
if(md5($pass.$salt).$salt==$dbPass) {
$this->setLogin($id,$ipAdress);
$this->id = $id;
return true;
}
}
else {
$this->setFailedLogin($ipAdress);
throw new LoginException ("Ihr Zugang wurde noch nicht aktiviert.",30);
}
}
else {
$res->free_result();
$this->setFailedLogin($ipAdress);
throw new LoginException ("Falsches Login oder Passwort. Bitte probieren Sie es erneut.",20);
}
}
}
else {
$this->setFailedLogin($ipAdress);
throw new LoginException ("Sie müssen ein Login und ein Passwort eingeben. Bitte probieren Sie es erneut.",10);
}
}
// Check if IP was blocked due to too many failed Logins
public function getIpPermission($ipAdress) {
$cpr = mktime()-(60*self::IP_blockDuration);
$sql = "SELECT `count`,UNIX_TIMESTAMP(`dateTime`) FROM login_failed WHERE UNIX_TIMESTAMP(`dateTime`)>? AND ipAdress=?";
$res = $this->db->prepare($sql);
$res->bind_param("is",$cpr,$ipAdress);
$res->bind_result($count,$timestamp);
$res->execute();
if($res->fetch() && $count>=self::IP_maxLoginTries) {
$res->free_result();
$timeLeft = ceil(((self::IP_blockDuration*60) - (mktime()-$timestamp))/60);
throw new LoginException ("Zuviele fehlgeschlagene Logins mit Ihrer IP-Adresse ".$ipAdress.". Sie können es in ".$timeLeft." Minuten erneut probieren.",30);
}
$res->free_result();
return true;
}
// Returns the user_id as it can't be received via magic method __get as it mustn't be changed via magic method __set
public function getId() {
return isset($this->id) ? $this->id : false;
}
// Register new user
public function registerUser($email,$pass,$name,$ipAdress,$requiresActivation=true) {
$salt = substr(md5(uniqid(mt_rand(), true)),0,23);
$pass = md5($pass.$salt).$salt;
$isActive = $requiresActivation ? 1 : 0;
$sql = "
INSERT INTO
user
SET
email=?,password=?,name=?,register_ipAdress=?,register_date=CURDATE(),isActive=".$isActive;
$res = $this->db->prepare($sql);
$res->bind_param("ssss",$email,$pass,$name,$ipAdress);
if($res->execute() && $this->db->affected_rows) {
$this->id = $this->db->insert_id;
if(!$requiresActivation) {
return $this->id;
}
$activationCode = md5(uniqid(mt_rand(), true));
$expireDate = date("Y-m-d",mktime()+86400*self::activationExpires);
$sql = "INSERT INTO user_activation SET user_id=?,activationCode=?,expireDate=?";
$res = $this->db->prepare($sql);
$res->bind_param("iss",$this->id,$activationCode,$expireDate);
if($res->execute() && $this->db->affected_rows) {
return $activationCode;
}
}
return false;
}
public function activateUserByCode($id,$activationCode) {
$sql = "SELECT activationCode,expireDate FROM user_activation WHERE user_id=?";
$res = $this->db->prepare($sql);
$res->bind_param("i",$id);
$res->bind_result($dbActivationCode,$expireDate);
$res->execute();
if($res->fetch()) {
$res->free_result();
if($dbActivationCode==$activationCode) {
if(date("Y-m-d")<=$expireDate) {
$sql = "UPDATE user SET isActive=1 WHERE id=?";
$res = $this->db->prepare($sql);
$res->bind_param("i",$id);
$res->execute();
$sql = "DELETE FROM user_activation WHERE user_id=?";
$res = $this->db->prepare($sql);
$res->bind_param("i",$id);
$res->execute();
return true;
}
else {
throw new UserException ("Dein Aktivierungscode ist abgelaufen.",40);
}
}
else {
throw new UserException ("Der Aktivierungscode ist nicht korrekt.",50);
}
}
else {
throw new UserException ("Der Benutzer wurde nicht gefunden oder ist bereits aktiviert.",60);
}
}
public function changeMyPass($id,$passOld,$passNew) {
return $this->checkUserPass($id,$passOld) && $this->changeUserPass($id,$passNew) ? true : false;
}
public function checkUserPass($id,$pass) {
$sql = "SELECT password FROM user WHERE id=?";
$res = $this->db->prepare($sql);
$res->bind_param("i",$id);
$res->bind_result($passDB);
$res->execute();
if($res->fetch()) {
$salt = substr($passDB,-23);
if(md5($pass.$salt).$salt==$passDB) {
return true;
}
}
return false;
}
public function changeUserPass($id,$pass) {
$salt = substr(md5(uniqid(mt_rand(), true)),0,23);
$pass = md5($pass.$salt).$salt;
$sql = "UPDATE user SET password=? WHERE id=?";
$res = $this->db->prepare($sql);
$res->bind_param("si",$pass,$id);
return $res->execute() && $this->db->affected_rows ? true : false;
}
public function deleteUser($id) {
$sql = "DELETE FROM user WHERE id=?";
$res = $this->db->prepare($sql);
$res->bind_param("i",$id);
return $res->execute() && $this->db->affected_rows ? true : false;
}
private function setLogin($id,$ipAdress) {
$sql = "UPDATE user SET lastLogin_date=NOW(),lastLogin_ipAdress=? WHERE id=?";
$res = $this->db->prepare($sql);
$res->bind_param("si",$ipAdress,$id);
$res->execute();
}
private function setFailedLogin($ipAdress) {
$cpr = mktime()-(60*self::IP_blockDuration);
$sql = "SELECT COUNT(*) FROM login_failed WHERE UNIX_TIMESTAMP(`dateTime`)>? AND ipAdress=?";
$res = $this->db->prepare($sql);
$res->bind_param("is",$cpr,$ipAdress);
$res->bind_result($count);
$res->execute();
if($res->fetch() && $count) {
$res->free_result();
$sql = "UPDATE login_failed SET `count`=`count`+1,`dateTime`=NOW() WHERE ipAdress=?";
$res = $this->db->prepare($sql);
$res->bind_param("s",$ipAdress);
}
else {
$res->free_result();
$sql = "REPLACE INTO login_failed SET ipAdress=?,`count`=1,`dateTime`=NOW()";
$res = $this->db->prepare($sql);
$res->bind_param("s",$ipAdress);
}
$res->execute();
$res->close();
}
private function generateRandomPass($length=8,$chars="abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZäöüÄÖÜß0123456789_-.:+*#!$%/") {
$password = "";
for($i=0;$i<$length;$i++) {
$password.= substr($chars,(rand()%(strlen($chars))),1);
}
return $password;
}
}
?>
Alles anzeigen
tables.sql
CREATE TABLE IF NOT EXISTS `login_failed` (
`ipAdress` char(15) NOT NULL,
`dateTime` datetime NOT NULL,
`count` tinyint(4) NOT NULL,
PRIMARY KEY (`ipAdress`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`email` varchar(70) NOT NULL,
`password` char(55) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`name` varchar(70) NOT NULL,
`register_date` date NOT NULL,
`register_ipAdress` varchar(15) NOT NULL,
`lastLogin_date` datetime NOT NULL,
`lastLogin_ipAdress` varchar(15) NOT NULL,
`isActive` tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
CREATE TABLE IF NOT EXISTS `user_activation` (
`user_id` int(10) unsigned NOT NULL,
`activationCode` char(32) NOT NULL,
`expireDate` date NOT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Alles anzeigen
Desweiteren habe ich hier noch einen kleinen Beispielcode zur Verwendung. Man kann sich damit registrieren, der Aktivierungslink wird nicht verschickt sondern direkt ausgegeben. Man kann sich einloggen und ausloggen. Ist aber alles eher schlampig auf die schnelle runtergehackt. Zum testen reicht es aber
Die Dateien müssen so benannt werden wie hier und im gleichen Verzeichnis wie die User.class.php abgelegt werden. Natürlich müssen vorher die MySQL-Tabellen angelegt werden.
In der test.php müssen außerdem die Daten für die DB-Verbindung angepasst werden.
test.php
<?php
session_start();
$db = new mysqli('localhost','root','','bela');
require_once "User.class.php";
// Wurde das Login-Formular abgeschickt? Dann...
if(isset($_POST['login']) && isset($_POST['email']) && isset($_POST['password'])) {
// ... neues User-Objekt erstellen und...
$user = new User($db);
// ... Login-Daten prüfen.
try {
$user->checkLogin($_POST['email'],$_POST['password'],$_SERVER['REMOTE_ADDR']);
// Bei korrekten Login-Daten wird die user_id in der Session gespeichert.
$_SESSION['user_id'] = $user->getId();
}
catch(LoginException $Exception) {
// Bei fehlgeschlagenem Login wird die Fehlermeldung für die spätere Ausgabe abgefangen und gespeichert.
$loginMsg = $Exception->getMessage();
}
}
// Wurde das Registrierungs-Formular abgeschickt? Dann...
elseif(isset($_POST['register']) && isset($_POST['name']) && isset($_POST['email']) && isset($_POST['password'])) {
// ... neues User-Objekt erstellen und...
$user = new User($db);
// ... Login-Daten prüfen.
try {
$activationCode = $user->registerUser($_POST['email'],$_POST['password'],$_POST['name'],$_SERVER['REMOTE_ADDR']);
// Bei korrekten Login-Daten wird der Aktivierungslink ausgegeben
$registerMsg = "<a href='?activate=1&uid=".$user->getId()."&code={$activationCode}'>Zugang aktivieren</a>";
}
catch(RegisterException $Exception) {
// Bei fehlgeschlagenem Login wird die Fehlermeldung für die spätere Ausgabe abgefangen und gespeichert.
$registerMsg = $Exception->getMessage();
}
}
// Wurde die Aktivierungsseite aufgerufen?
elseif(isset($_GET['activate']) && isset($_GET['uid']) && isset($_GET['code'])) {
$user = new user($db);
try {
$user->activateUserByCode($_GET['uid'],$_GET['code']);
$_SESSION['user_id'] = (int)$_GET['uid'];
$activateMsg = "Ihr Account wurde aktiviert. Sie können sich nun einloggen.<br /><a href='test.php'>zum login</a>";
}
catch(UserException $Exception) {
$activateMsg = $Exception->getMessage();
}
}
// Logout aufgerufen?
elseif(isset($_REQUEST['logout']) && isset($_SESSION['user_id'])) {
// Session zerstören und...
session_destroy();
// ... Seite neu laden
header("Location: ./test.php");
}
// Ist der User eingeloggt?
if(!isset($_SESSION['user_id'])) {
if(isset($_GET['register'])) {
$registerMsg = isset($registerMsg) ? $registerMsg : "Bitte geben Sie Ihre Daten ein.";
include "registerForm.tpl.php";
}
elseif(isset($activateMsg)) {
echo $activateMsg;
}
else {
// Prüfen ob eine Fehlermeldung aus einem fehlgeschlagegen Login-Versuch gesetzt ist,
// ansonsten Standard-Meldung zur Ausgabe über dem Login-Formular
$loginMsg = isset($loginMsg) ? $loginMsg : "Bitte mit Passwort und Email einloggen.";
// Das Login-Formular laden
include "loginForm.tpl.php";
}
}
else {
// User-Bereich nach erfolgreichem Login
echo "Herzlichen Glühstrumpf. Du bist jetzt eingeloggt...<br /><a href='?logout=1'>ausloggen</a>";
}
?>
Alles anzeigen
loginForm.tpl.php
<html>
<head>
<title>Login</title>
</head>
<body>
<?php echo $loginMsg; ?>
<br />
<form method='post' action=''>
Email:
<input type='text' name='email' />
Passwort:
<input type='password' name='password' />
<input type='submit' name='login' value='Einloggen' />
</form>
<a href='?register=1'>registrieren</a>
</body>
</html>
Alles anzeigen
registerForm.tpl.php
<html>
<head>
<title>Registrieren</title>
</head>
<body>
<?php echo $registerMsg; ?>
<br />
<form method='post' action=''>
Name:
<input type='text' name='name' />
Email:
<input type='text' name='email' />
Passwort:
<input type='password' name='password' />
<input type='submit' name='register' value='Registrieren' />
</form>
<a href='?'>zum login</a>
</body>
</html>
Alles anzeigen