Laravel 4: Dateien und Downloads vor unberechtigtem Zugriff schützen
22. Mai 2013 Web-Entwicklung von Eric Kubenka
Oft kommt es vor, dass die zum Download angebotenen Dateien nur für angemeldete Benutzer verfügbar sein sollen. Dazu kann das Dokumenten-Root Verzeichnis public von Laravel nicht verwendet werden, da generell jedem Anwender Zugriff auf alle Dateien in diesem Verzeichnis gewährt wird. Auch eine Abfrage des aktuell angemeldeten Benutzers kann dies nicht verhindern.
Wie muss das ganze nun gelöst werden, damit sichergestellt werden kann, dass der Benutzer auch wirklich angemeldet ist? Dies möchte ich folgend an einem Beispiel erläutern. Für die Ungeduldigen befindet sich ein Link zum kompletten Source-Code am Ende des Artikels.
php_fileinfo-Extension aktivieren
Damit Laravel ohne großen Aufwand ermitteln kann um welchen Dateityp es sich bei Download-Files handelt, muss zuvor in der php.ini die Extension php_fileinfo aktiviert werden. Dazu einfach sicherstellen, dass folgende Zeile in besagter Datei nicht auskommentiert ist:
extension=php_fileinfo.dll
Das Storage Verzeichnis
Im app-Verzeichnis des Laravel-Ordners befindet sich ein Ordner namens storage. Dies ist der von Laravel für dieses Szenario vorgesehene Ablage-Ordner der zum Download angebotenen Dateien. In diesem Verzeichnis habe ich für das Beispiel einen Ordner mit dem Namen files angelegt. Darin habe ich die Datei protect.txt abgelegt. Der Verzeichnisbaum sieht nun wie folgend abgebildet aus.
Die Authentifizierung
Um die Datei nur angemeldeten Benutzern zur Verfügung zu stellen, müssen diese sich zuerst authentifizieren. Wie eine einfache Nutzer-Authentifizierung in Laravel abgebildet wird, kann in meinem Artikel bezüglich dieser Thematik nachgeschlagen werden.
Das View
Ich verwende für dieses Beispiel ein simples View mit einem Login-Formular und einem Download-Link zur Datei im Storage-Verzeichnis. Außerdem erhält das View noch eine kleine Info-Box am oberen Rand in welcher diverse Meldungen ausgegeben werden.
Wichtig: An dieser Stelle darf nicht direkt auf das Storage-Verzeichnis verlinkt werden, sondern auf eine dafür vorgesehene Route, welche die Umleitung übernimmt.
<!DOCTYPE html> <html> <head> <title>Safe File Download Example</title> <link href="assets/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen"> <script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script> <script src="assets/bootstrap/js/bootstrap.min.js"></script> <link href="assets/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet" media="screen"> </head> <body> <p></p> <div class="content container-fluid"> @if(Session::has('message')) <div class="alert alert-info "> {'{ Session::get('message') }'} </div> @endif <div class="row-fluid"> <div class="well span3"> <h3>User Area</h3> @if(!Auth::check()) {'{ Form::open(array('url' => 'login', 'method' => 'post', 'class' => 'form-horizontal')) }'} {'{ Form::label('username', 'username') }'} {'{ Form::text('username', '', array('class' => 'input')) }'} {'{ Form::label('password', 'password') }'} {'{ Form::password('password', array('class' => 'input')) }'} {'{ Form::submit('Sign in', array('name' => 'submitLogin', 'class' => 'btn btn-primary ')) }'} {'{ Form::close() }'} @else <p>Welcome {'{ Auth::user()->username }'}</p> <a href="{'{ URL::to('logout') }'}" class="btn btn-warning">Logout</a> @endif </div> <div class="well span3"> <h3>File List</h3> <a href="{'{ URL::to('download/protect.txt') }'}">Download protect.txt</a> </div> </div> </div> </body> </html>
Hinweis: Entfernt die einfachen Hochkommas zwischen den doppelten “{‘{“ falls ihr den Code direkt von diesem Snippet kopieren möchtet. Diese stellen in dem verwendten CMS des Blogs ebenfalls Platzhalter dar und nur so lassen sich Anzeigefehler vermeiden. Für einen Link zu den direkten Dateien scrollt zum Ende des Artikels.
Anfragen auf das Storage-Verzeichnis umleiten
In der app/routes.php-Datei habe ich folgende Route definiert um die Anfragen von public/download/dateiname auf /app/storage/files/dateiname umzuleiten.
<?php /** * protect the following routes */ Route::group(array('before' => 'auth'), function() { /** * Logout the current user and redirect */ Route::get('/logout', function() { Auth::logout(); return Redirect::route('home')->with('message', 'See you later'); }); /** * notice: enable php_fileinfo extension in php.ini * response with download or redirect and error */ Route::get('download/{name}', function($name) { $filePath = storage_path() . '/files/' . $name; if (File::exists($filePath)) { return Response::download($filePath); } else { return Redirect::route('home')->with('message', "The file you're looking fore doesn't exists"); } }); }); /** * route: home */ Route::get('/', array('as' => 'home', function() { return View::make('hello'); })); /** * route: validate user data and reddirect */ Route::post('/login', function() { $userData = array( 'username' => Input::get('username'), 'password' => Input::get('password') ); if (Auth::attempt($userData)) { return Redirect::route('home')->with('message', 'Login successfull'); } else { return Redirect::route('home')->with('message', 'Login failed'); } });
Dem abgebildeten Quellcode der routes.php lässt sich entnehmen, dass Laravel bereits eine Methode zum Ermitteln des Storage-Pfades besitzt – namentlich die Methode storage_path().
Durch den gesetzten Auth-Filter wird sichergestellt, dass nur angemeldete Nutzer Zugriff auf die Download-Route erhalten und anderenfalls mit der Aufforderung zum Login auf die Startseite umgeleitet werden.
Fazit und Show-Case
Damit ist das Szenario vom Quellcode her auch schon komplett umgesetzt. Nun möchte ich nur noch Schrittweise demonstrieren wie die verschiedenen Ausgaben in der Info-Box demonstrieren.
SourceCode
Der komplette Quellcode inklusive der kleinen Datenbank steht auf GitHub bereit.