phaziz.com

Login-System mit SLIM3

slim-php-framework-logo.png

Jede Internetseite - oder App, in der Daten eingespeist werden um diese zu Verarbeiten, benötigt einen sicheren Login-Mechanismus. Wo die Daten für die Benutzer herkommen, ist (erst einmal) egal - interessant ist der Weg einen berechtigten Benutzer zu verifizieren.

Die Bestandteile

Benötigt werden mehrere Ansichten - Startseite, Login-Seite mit Formular, eine Route mit eventueller Ansicht um den Benutzer zu erkennen, nötige Session-Variablen zu setzen, ein Backend - der Ort, wo Daten eingepflegt werden können und schließlich auch wieder ein Logout-System.

Slim3

Die Bestandteile von Slim3 geben hierfür alles benötigte her: Ein schnelles System, ein flinker Workflow, die nötigen Erweiterungen (Middleware),Templating mit Twig, Logging mit Monolog, SlimFlash als Messaging-System, Slim-CSRF für sichere Kommunikation via Post/Get innerhalb verschiedener Views, außerdem die Erweiterbarkeit via PSR4 - Autoloading. HTTPS als bevorzugtes Protokoll für ein Login-System muß ich hier ja nicht extra noch erwähnen ;-)

Legen wir Los

Mit Composer - weil es so schön einfach ist:

{
"require": {
"slim/slim": "^3.0",
"slim/csrf": "0.8.1",
"slim/flash": "*",
"monolog/monolog": "1.23.0",
"twig/twig": "*",
"ramsey/uuid": "3.7.1"
}}

Danach ein flinkes php composer.phar update per SSH auf dem Webserver und unsere Grundstruktur ist bereits eingerichtet - zusätzlich ist natürlich wieder ein /public-Verzeichnis sinnvoll. Wir schaffen uns folgende Struktur und routen entsprechend die eingerichtete Domain auf das angelegte /public-Verzeichnis:

...
/cache
/logs
/public
.htaccess
index.php
/components
/vendor
/views
composer.json
composer.lock
composer.phar
...

Das /cache-Verzeichnis benutzen wir für Twig und das Route-Caching von Slim3. Das Verzeichnis /logs dient der Speicherung der Monolog-Logger. Im /public-Verzeichnis legen wir unsere index.php-Datei für die Slim3-Steuerung ab, ebenso eine .htaccess-Datei - Sicherheit geht vor, möchten wir Assets in Form von CSS und Javascript benutzen, lohnt auch immer noch das Anlegen eines /assets-Verzeichnis innerhalb von /public.

Das Verzeichnis /vendor wird von Composer befüllt und in den /views speichern wir unsere Templates für die verschiedenen Ansichten. Fertig.

.htaccess im /public-Verzeichnis

Habe ich schon die .htaccess-Firewall erwähnt? Unbedingt, sollte dieses kleine Schmuckstück zu jedem Web-Projekt gehören. (Alle Informationen dazu gibt es hier: https://perishablepress.com/6g/) - diese Firewall in Kombination mit dem ModRewrite für Slim3 ergibt folgende .htaccess-Datei:

# 6G FIREWALL/BLACKLIST
# @ https://perishablepress.com/6g/
# 6G:[QUERY STRINGS]
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{QUERY_STRING}(eval\() [NC,OR]
RewriteCond %{QUERY_STRING}(127\.0\.0\.1) [NC,OR]
RewriteCond %{QUERY_STRING}([a-z0-9]{2000,}) [NC,OR]
RewriteCond %{QUERY_STRING}(javascript:)(.*)(;) [NC,OR]
RewriteCond %{QUERY_STRING}(base64_encode)(.*)(\() [NC,OR]
RewriteCond %{QUERY_STRING}(GLOBALS|REQUEST)(=|\[|%) [NC,OR]
RewriteCond %{QUERY_STRING}(<|%3C)(.*)script(.*)(>|%3) [NC,OR]
RewriteCond %{QUERY_STRING}(\\|\.\.\.|\.\./|~|`|<|>|\|) [NC,OR]
RewriteCond %{QUERY_STRING}(boot\.ini|etc/passwd|self/environ) [NC,OR]
RewriteCond %{QUERY_STRING}(thumbs?(_editor|open)?|tim(thumb)?)\.php [NC,OR]
RewriteCond %{QUERY_STRING}(\'|\")(.*)(drop|insert|md5|select|union) [NC]
RewriteRule .* - [F]
</IfModule>
# 6G:[REQUEST METHOD]
<IfModule mod_rewrite.c>
RewriteCond %{REQUEST_METHOD}^(connect|debug|move|put|trace|track) [NC]
RewriteRule .* - [F]
</IfModule>
# 6G:[REFERRERS]
<IfModule mod_rewrite.c>
RewriteCond %{HTTP_REFERER}([a-z0-9]{2000,}) [NC,OR]
RewriteCond %{HTTP_REFERER}(semalt.com|todaperfeita) [NC]
RewriteRule .* - [F]
</IfModule>
# 6G:[REQUEST STRINGS]
<IfModule mod_alias.c>
RedirectMatch 403 (?i)([a-z0-9]{2000,})
RedirectMatch 403 (?i)(https?|ftp|php):/
RedirectMatch 403 (?i)(base64_encode)(.*)(\()
RedirectMatch 403 (?i)(=\\\'|=\\%27|/\\\'/?)\.
RedirectMatch 403 (?i)/(\$(\&)?|\*|\"|\.|,|&|&amp;?)/?$
RedirectMatch 403 (?i)(\{0\}|\(/\(|\.\.\.|\+\+\+|\\\"\\\")
RedirectMatch 403 (?i)(~|`|<|>|:|;|,|%|\\|\s|\{|\}|\[|\]|\|)
RedirectMatch 403 (?i)/(=|\$&|_mm|cgi-|etc/passwd|muieblack)
RedirectMatch 403 (?i)(&pws=0|_vti_|\(null\)|\{\$itemURL\}|echo(.*)kae|etc/passwd|eval\(|self/environ)
RedirectMatch 403 (?i)\.(aspx?|bash|bak?|cfg|cgi|dll|exe|git|hg|ini|jsp|log|mdb|out|sql|svn|swp|tar|rar|rdf)$
RedirectMatch 403 (?i)/(^$|(wp-)?config|mobiquo|phpinfo|shell|sqlpatch|thumb|thumb_editor|thumbopen|timthumb|webshell)\.php
</IfModule>
# 6G:[USER AGENTS]
<IfModule mod_setenvif.c>
SetEnvIfNoCase User-Agent ([a-z0-9]{2000,}) bad_bot
SetEnvIfNoCase User-Agent (archive.org|binlar|casper|checkpriv|choppy|clshttp|cmsworld|diavol|dotbot|extract|feedfinder|flicky|g00g1e|harvest|heritrix|httrack|kmccrew|loader|miner|nikto|nutch|planetwork|postrank|purebot|pycurl|python|seekerspider|siclab|skygrid|sqlmap|sucker|turnit|vikspider|winhttp|xxxyy|youda|zmeu|zune) bad_bot
# Apache < 2.3
<IfModule !mod_authz_core.c>
Order Allow,Deny
Allow from all
Deny from env=bad_bot
</IfModule>
# Apache >= 2.3
<IfModule mod_authz_core.c>
<RequireAll>
Require all Granted
Require not env bad_bot
</RequireAll>
</IfModule>
</IfModule>
# 6G:[BAD IPS]
<Limit GET HEAD OPTIONS POST PUT>
Order Allow,Deny
Allow from All
# uncomment/edit/repeat next line to block IPs
# Deny from 123.456.789
</Limit>
#Slim3-ModRewrite für clean-URIs
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME}!-f
RewriteCond %{REQUEST_FILENAME}!-d
RewriteRule ^ index.php [QSA,L]
</IfModule>

Slim3 index.php

Jetzt können wir die /public/index.php befüllen - man beachte die Kommentare:

<?php
// PHP-Session starten, falls noch nicht erledigt
if (session_status() == PHP_SESSION_NONE){session_start();
}// Composer-Autoloading implementieren
require_once __DIR__ . '/../vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;
// uuids für die Session nach dem Login
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\Exception\UnsatisfiedDependencyException;
// Slim3-Konfiguration
$config = [
'settings' => [
'displayErrorDetails' => true,
'determineRouteBeforeAppMiddleware' => true,
'debug' => true,
'addContentLengthHeader' => true,
'routerCacheFile' => __DIR__ . '/../cache/routes.cache'
]
];
$app = new \Slim\App($config);
// DI-Container
$container = $app -> getContainer();
$container['logger'] = function(){$logger = new \Monolog\Logger('phaziz');
$file_handler = new \Monolog\Handler\StreamHandler(__DIR__ . '/../logs/' . date('Y-m-d') . '-log.logfile');
$logger -> pushHandler($file_handler);
return $logger;
};
// Slim3-CSRF bereitstellen
$container['csrf'] = function (){return new \Slim\Csrf\Guard;
};
// uuid-Gernerator bereitstellen
$container['uuid'] = function(){try
{
$uuid5 = Uuid::uuid5(Uuid::NAMESPACE_DNS, 'slimdev.phaziz.com' . date('YmdHis'));
return $uuid5 -> toString();
}catch (UnsatisfiedDependencyException $e){return 'Caught exception: ' . $e -> getMessage();
}};
// Slim3-Flash-Messaging bereitstellen
$container['flash'] = function (){return new \Slim\Flash\Messages();
};
$container['twig'] = function(){$loader = new Twig_Loader_Filesystem(__DIR__ . '/../views/');
$twig = new Twig_Environment($loader, [
'cache' => __DIR__ . '/../cache/',
'debug' => false,
'strict_variables' => true,
'autoescape' => 'html',
'optimizations' => -1,
'charset' => 'utf-8'
]);
return $twig;
};
///CSRF-Middleware einbinden
$app->add($container->get('csrf'));
//...

Routing

Halten wir es einfach - wir möchten eine Startseite, eine Login-Seite, eine versteckte Route für die Verifizierung von Benutzern, einen Backend-Bereich und schließlich einen Logout:

// die Startseite in der Route ./ der Domain
$app -> get('/', function (Request $request, Response $response) use ($app){$this -> logger -> addInfo('Root Path');
return $this -> twig -> render('index.html', [
'PageTitle' => 'Homepage'
]);
});
// die Login-Seite ./login mit Formular und CSRF-Token
$app -> get('/login', function (Request $request, Response $response) use ($app){$this -> logger -> addInfo('Login Path');
$tNameKey = $this -> csrf -> getTokenNameKey();
$tValueKey = $this -> csrf -> getTokenValueKey();
$tName = $request -> getAttribute($tNameKey);
$tValue = $request -> getAttribute($tValueKey);
$messages = $this -> flash -> getMessages('login');
return $this -> twig -> render('login.html', [
'PageTitle' => 'Login',
'tNameKey' => $tNameKey,
'tName' => $tName,
'tValueKey' => $tValueKey,
'tValue' => $tValue,
'uuid' => $this -> uuid,
'messages' => $messages
]);
});
// die Verifizierung von Benutzern - hier nur exemplarisch einen Benutzer mit den Daten:
// Benutzername: user
// Passwort: password
// Das ganze per Post unter ./verify
$app -> post('/verify', function (Request $request, Response $response) use ($app){$this -> logger -> addInfo('Verify Path');
$Username = $_POST['username'];
$Password = $_POST['password'];
$UUID = $_POST['uuid'];
// Hier könnte man zum Beispiel eine DB-Abfrage implementieren, oder Daten aus einer ENV-Datei
// integrieren... wir wollen es aber einfach und geben den berechtigten Benutzer einfach vor
if($Username == 'user' && $Password == 'password'){// Mit dieser Session-Variablen erkennen wir angemeldete Benutzer
// Sollte man in der Praxis selbstverständich auch ein wenig erweitern
$_SESSION['uuid'] = $UUID;
// Slim3-Flash-Messaging
$this -> flash -> addMessage('login', 'Successfully logged in!');
return $response -> withRedirect('./backend');
}// Oder Ablehnung, wegen falscher Benutzerdaten...
else
{
$this -> flash -> addMessage('login', 'Bad user-credentials! User unknown!');
return $response -> withRedirect('./login');
}});
// Den Backend-Bereich nur für angemeldete Benutzer
$app -> get('/backend', function (Request $request, Response $response, $args) use ($app){$this -> logger -> addInfo('Backend Path');
if(!isset($_SESSION['uuid'])){$this -> flash -> addMessage('login', 'Bad user-credentials! User unknown!');
return $response -> withRedirect('./login');
exit;
}$messages = $this -> flash -> getMessages('login');
return $this -> twig -> render('backend.html', [
'PageTitle' => 'Backend',
'messages' => $messages
]);
});
$app -> get('/logout', function (Request $request, Response $response, $args) use ($app){$this -> logger -> addInfo('Logout Path: ' . $_SESSION['uuid']);
session_destroy();
$_SESSION = [];
return $response -> withRedirect('../');
});
// Schließlich lassen wir unser Baby laufen
$app -> run();

Templating

Benötigen wir nur noch die entsprechenden Ansichten - Templates - im Verzeichnis /views:

<!--Unsere ./-Route-->
<!DOCTYPE html>
<html>
<head>
<title>{{ PageTitle|raw }}</title>
<meta charset=utf-8>
<meta name=viewport content="width=device-width, initial-scale=1">
</head>
<body>
<h1>{{ PageTitle|raw }}</h1>
<p><a href="./login" target=_self>Login</a></p>
</body>
</html>
<!--Die Login-Seite unter ./login-->
<!--Mit Slim3-CSRF und Flash-Messaging und dem eigentlichen Login-Formular-->
<!DOCTYPE html>
<html>
<head>
<title>{{ PageTitle|raw }}</title>
<meta charset=utf-8>
<meta name=viewport content="width=device-width, initial-scale=1">
</head>
<body>
<h1>{{ PageTitle|raw }}</h1>
{% if messages %}<p>
{% for message in messages %}{{ message.0|raw }}{% endfor %}</p>
{% endif %}<form method=post action="./verify" enctype=application/x-www-form-urlencoded autocomplete=off>
<input type=hidden name=uuid value="{{ uuid|raw }}" required=required/>
<input type=hidden name="{{ tNameKey|raw }}" value="{{ tName|raw }}" required=required/>
<input type=hidden name="{{ tValueKey|raw }}" value="{{ tValue|raw }}" required=required/>
<p>Name:</p>
<p><input type=text name=username value="" required=required/></p>
<p>Password</p>
<p><input type=password name=password value="" required=required/></p>
<p><button type=submit>Login</button></p>
</form>
</body>
</html>
<!--Die Backend-Ansicht unter ./backend-->
<!DOCTYPE html>
<html>
<head>
<title>{{ PageTitle|raw }}</title>
<meta charset=utf-8>
<meta name=viewport content="width=device-width, initial-scale=1">
</head>
<body>
<h1>{{ PageTitle|raw }}</h1>
{% if messages %}<p>
{% for message in messages %}{{ message.0|raw }}{% endfor %}</p>
{% endif %}<p><a href="./logout" target=_self>Logout</a></p>
</body>
</html>

Fertig!

Interessant ist hierbei schließlich der Einsatz des Slim3 eigenen Messaging-Systems über Flash-Messages (https://github.com/slimphp/Slim-Flash). Installation entweder direkt über die composer.json-Datei, oder via:

php composer.phar require slim/flash

Danach die Registrierung in der ./public/index.php-Datei:


$container['flash'] = function (){return new \Slim\Flash\Messages();
};

Und schließlich der Einsatz als Messaging-System zwischen zwei, oder mehr Routen:

$app -> get('/foo', function ($req, $res, $args){$this -> flash -> addMessage('Test', 'This is a message');
return $res -> withStatus(302) -> withHeader('Location', '/bar');
});
$app -> get('/bar', function ($req, $res, $args){$messages = $this -> flash -> getMessages();
print_r($messages);
});
$app -> run();

Außerdem auch der Einsatz der Slim3-CSRF-Middleware (https://github.com/slimphp/Slim-Csrf):

DI-Registrierung:


$container = $app -> getContainer();
$container['csrf'] = function ($c){return new \Slim\Csrf\Guard;
};
// If you are implementing per-route checks you must not add this
$app -> add($container -> get('csrf'));

Und schließlich die Verwendung laut Dokumentation innerhalb der Routen in Slim3:

$app -> get('/foo', function ($request, $response, $args){$nameKey = $this -> csrf -> getTokenNameKey(); $valueKey = $this -> csrf -> getTokenValueKey(); $name = $request -> getAttribute($nameKey); $value = $request -> getAttribute($valueKey);

 // Render HTML form which POSTs to /bar with two hidden input fields for the
// name and value:
// <input type=hidden name="<?= $nameKey ?>" value="<?= $name ?>">
// <input type=hidden name="<?= $valueKey ?>" value="<?= $value ?>">

});

$app -> post('/bar', function ($request, $response, $args){// CSRF protection successful if you reached this far. });

$app -> run();

Goodies

Der Einsatz eines Output-Compressors - hierdurch wird das Datenvolumen verringert, das bei einem HTTP-Response ausgeliefert werden muß. Wir setzen hier die Twig-Templating-Engine ein (https://twig.symfony.com/) und dafür gibt es ein ganz wunderbares Plugin - wenn wir unsere composer.json ein wenig erweitern:

"nochso/html-compress-twig": "*"

Zu finden ist dieser Compressor hier: https://github.com/nochso/html-compress-twig

Wir müssen für den Einsatz unseren DI-Container für TWIG ein wenig erweitern:

$container['twig'] = function(){$loader = new Twig_Loader_Filesystem(__DIR__ . '/../views/');
$twig = new Twig_Environment($loader, [
'cache' => __DIR__ . '/../cache/',
'strict_variables' => true,
'optimizations' => -1,
'charset' => 'utf-8'
]);
// Hier wird dieser schließlich implementiert
$twig -> addExtension(new \nochso\HtmlCompressTwig\Extension());
return $twig;
};

Wenn wir danach unsere views ein wenig anpassen:

{% htmlcompress %}<!DOCTYPE html>
<html>
<head>
...
</body>
</html>
{% endhtmlcompress %}

Erhalten wir einen stark komprimierten HTML-Output im Browser - als Quelltext für Menschen völlig unlesbar - aber nicht für den Browser - dieser interessiert sich nämlich nicht für Leerstellen und Umbrüche in HTML-Texten.

github

Dieses Projekt auf GitHub unter https://github.com/phaziz/Slim3DevUserAuth

phaziz.com   |   ConstructrCMS   |   ConstructrCMS GitHub   |   Impressum & Datenschutz