phaziz.com

Startseite > 03.10.2017 Login-System mit Slim3

03.10.2017 Login-System mit Slim3

Login-System mit Slim3 (https://www.slimframework.com)

Slim Framework

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';

  //Logger
  use Monolog\Logger;
  use Monolog\Handler\StreamHandler;

  //Slim3
  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'
      ]
  ];

  // Slim3 initialisieren
  $app = new \Slim\App($config);

  // DI-Container
  $container = $app -> getContainer();

  // den Monolog Logger initialisieren und bereitstellen
  $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();
  };

  // Twig einrichten
  $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!');

      // Weiterleitung ins Backend
      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
    ]);
  }
);

// Das Logout
$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:

// Register provider
$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)
{
    // Set flash message for next request
    $this -> flash -> addMessage('Test', 'This is a message');

    // Redirect
    return $res -> withStatus(302) -> withHeader('Location', '/bar');
});

$app -> get('/bar', function ($req, $res, $args)
{
    // Get flash messages from previous request
    $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:

// Register with container
$container = $app -> getContainer();

$container['csrf'] = function ($c)
{
  return new \Slim\Csrf\Guard;
};

// Register middleware for all routes

// 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) { // CSRF token name and value $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

 

Suche

Suchbegriffe mit mindestens 3 Zeichen! Suchvorgang mit ENTER-Taste starten.