Authentifizierung mit dem Zend AMF Server
von Thomas Müller
Um einen Webservice für eine Flex Anwendung abzusichern, gibt es viele Möglichkeiten. Handelt es sich um eine öffentlich zugängliche Anwendung, ist es natürlich leicht, denn jeder Benutzer darf auf den Webservice zugreifen.
Häufig jedoch ist dies nicht gewünscht und der Webservice muss abgesichert werden. Dazu gibt es viele Möglichkeiten, die alle ihre Vor- und Nachteile mit sich bringen. Für eine Anwendung an der ich zurzeit arbeite, greife ich auf eine bestehende Benutzerdatenbank zu. Wie es üblich ist, erfolgt der Login über eine E-Mail Adresse und ein als MD5 Hash in der Datenbank gespeichertes Passwort.
In diesem Artikel möchte ich auf die Authentifizierung mittels des Zend Frameworks eingehen.
Die Anwendung setzt auf Remote Objects und nutzt den Zend AMF Server als Gateway. Nachdem ein erster Entwurf der Anwendung angefertigt und der Webservice konzipiert und umgesetzt wurde, fehlte noch der Login auf meiner Liste. In vielen Tutorials wird mit einem Value Object für den Login gearbeitet. Das Login Fenster sendet Username und Passwort an den Webservice und wenn beide Angaben korrekt waren, wird die UserID (oder das User Objekt usw.) in der Session gespeichert.
Dieser gängige Ansatz hat jedoch den Nachteil, das ein neuer Login erforderlich ist, sobald die Session abgelaufen ist. Ein weiterer Nachteil ist es, dass für diesen Ansatz Cookies benötigt werden. Doch wieso nicht das Authtifizierungsmodell des AMF Protokolls benutzen? Der Zend AMF Server bietet seit geraumer Zeit Unterstützung dafür. In diesem Beitrag möchte ich auf die Implementierung eines solchen Authtifizierungsmodells eingehen.
Dabei kommen der aktuelle Flash Builder Beta 2 sowie das Zend Framework 1.9.5 zu Einsatz.
Der PHP Teil
In Kapitel 3.2.7. “Authentication” der Zend Framework Dokumentation wird in groben Zügen beschrieben, wie die Authentifizierung funktioniert:
Um Authentifizierung zu definieren, muß der Benutzer einen Authentifizierungs-Adapter anbieten der die abstrakte Klasse
Zend_Amf_Auth_Abstracterweitert. Der Adapter sollte dieauthenticate()Methode implementieren so wie jeder normale Authentifizierungs-Adapter.
An dieser Stelle wird leider nicht weiter auf die Implementierung des Authentifizierungs-Adapters eingegangen. Es folgt noch ein kurzer Hinweis, wie die eigenen Authentifizierungs Klasse eingebunden wird:
$server->setAuth(new My_Amf_Auth());
Ein guter Hinweis ist die Klasse Zend_Amf_Auth_Abstract, die sich als reichlich simpel entpuppt:
abstract class Zend_Amf_Auth_Abstract implements Zend_Auth_Adapter_Interface { protected $_username; protected $_password; public function setCredentials($username, $password) { $this->_username = $username; $this->_password = $password; } }
Des Weiteren heißt es “Der Adapter sollte die authenticate() Methode implementieren” – richtig ist jedoch: Der Adapter muss die authenticate() Methode implementieren. Es wäre schön, wenn die Klasse eine abstrakte Funktion authenticate() beinhalten würde.
Denn wenn der Zend AMF Gateway aus der Flex Anwendung die Credentials (Username und Passwort) gesendet bekommt und eine Auth Klasse gesetzt ist, wird automatisch die setCredentials() und anschließend die (von der eigenen Klasse implementierte) authenticate() Methode aufgerufen.
In Kapitel 5.1.1. der Zend Framework Dokumentation finden wir ein Beispiel, für eine authenticate() Methode:
/** * Führt einen Authentifizierungs-Versuch durch * * @throws Zend_Auth_Adapter_Exception Wenn die Authentifizierung nicht * durchgeführt wurde * @return Zend_Auth_Result */ public function authenticate() { // ... }
Wieder ein Stück weiter ist jetzt bekannt, dass diese Methode ein Zend_Auth_Result Objekt zurück liefern muss. Ein Zend_Auth_Result Objekt erfordert (mindestens) 2 Parameter: Einen Code und eine Rolle. Auf die Rollen des Zend Frameworks möchte ich in diesem Beitrag nicht näher eingehen, deswegen kann an dieser Stelle ein beliebiger String oder ein beliebiges Objekt übergeben werden. Ein Beispiel für die zu verwendenden Codes befindet sich in Kapitel 5.1.2.
Die Zuvor erwähnt Klasse My_Amf_Auth könnte demnach wie folgt aussehen:
class My_Amf_Auth extends Zend_Amf_Auth_Abstract { public function authenticate() { $user = mysql_fetch_object(mysql_query("SELECT * FROM users WHERE username = ".$this->_username)); // Username ungültig if (!is_object($user)) { return new Zend_Auth_Result(Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND, "guest"); } // Passwort ungültig if ($user->password != $this->_password) { return new Zend_Auth_Result(Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID, "guest"); } // User ist kein Admin if (!$user->isAdmin) { return new Zend_Auth_Result(Zend_Auth_Result::FAILURE_IDENTITY_AMBIGUOUS, "guest"); } // User ist ein Admin return new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, "admin"); } }
Der Flex Teil
Nachdem die Daten nun auf dem Server verarbeitet werden, stellt sich natürlich noch die Frage, wie Username und Passwort per Flex an die Remote Klassen gesendet werden können. Wenn man seine Webservice Abfragen mit dem eingebauten Daten/Dienste Manager erstellt hat, findet man in dem Paket des Services zwei Dateien: _Super_MyService.as und MyService.as. Die Super Klasse darf, wie man dem Quellcode auch entnehmen kann, nicht bearbeitet werden. Diese Datei wird automatich beim Aktualisieren der Services überschrieben. Stattdesses erweitert man die bisher noch leere MyService.as:
package services.myservice { import mx.core.FlexGlobals; public class MyService extends _Super_MyService { } }
In dieser Klasse mann man einen Konstruktor einfügen, der die Credentials setzt:
package services.categoryservice { import mx.core.FlexGlobals; public class MyService extends _Super_MyService { public function MyService () { this._serviceControl.setCredentials("Username", "Password"); } } }
this._serviceControl ist das in der Super Klasse instanzierte RemoteObject:
_serviceControl = new RemoteObject();
Erzeugt man nun in einer Komponente eine neue MyService Instanz, werden Benutzername und Passwort automatisch gesetzt:
<fx:Declarations> <s:CallResponder id="myResult" result="myResult_resultHandler(event)"/> <myservice:MyService id="myData" showBusyCursor="true" fault="myService_faultHandler(event)"/> </fx:Declarations>
Schöner ist es natürlich, Benutzername und Passwort nicht im Klartext in die Klassen einzutragen, sondern sie als Variablen über ein Loginfenster an die Klassen zu binden. Etwaige Fehler bei der Authentifizierung können mittels eines myService_faultHandler(event:FaultEvent) Listeners des MyService Objektes abgefangen werden.
Jetzt werden die Credentials im Header jeder AMF Anfrage mitgesendet. Ich empfehle, dass Passwort auf jeden Fall verschlüsselt zu senden, in meinem Fall als MD5 mittels des as3corelibs und den Webservice über ein HTTPS Protokoll anzusprechen. Mehr dazu gibt es in einem späteren Beitrag.
Sollte Interesse bestehen, kann ich auf weitere Aspekte dieses Authentifizierungsmethode eingeben und beantworte Fragen in den Kommentaren gerne so gut ich kann.