Entwicklung & Design

REST-basierte Schnittstellen mit dem Symfony-Framework implementieren: Web Services leicht gemacht

Seite 3 / 4

Komplexer: Datenmanipulation

Mit den beschriebenen Funktionen hat man im Prinzip bereits alles zusammen, was man braucht, um eine sehr einfache API zum Auslesen und Bereitstellen von Daten zu implementieren. Anspruchsvoller wird es jedoch, wenn die API nicht nur Daten auslesen, sondern auch ändern, löschen oder erstellen soll. Ein weitaus komplexeres Anwendungsszenario ist daher die Implementierung einer Schnittstellenfunktion, mit der ein Verleger neue Bücher direkt aus dem eigenen Warenwirtschaftssystem in das Shop-System übertragen kann. Aus einer „lesenden“ wird so eine „schreibende“ API.

Je umfangreicher eine API wird, desto sinnvoller kann es sein, diese in eine eigenständige Applikation auszulagern um eine saubere Trennnung zu den Front- und Backend-Applikationen zu haben, die in fast jedem Symfony-Projekt vorhanden sind.

Eine neue API-Applikation samt Book-Modul ist dank der Kommandozeilentools schnell angelegt:

API-Applikation genieren 
$ symfony generate:app api
$ symfony generate:module api book

Listing 3

Für das Book-Modul das Scaffolding zu verwenden, ist durchaus möglich, aber nicht unbedingt sinnvoll: Es werden dann zwar alle nötigen Methoden im Actioncontroller angelegt, diese gehen aber davon aus, dass die Daten zum Anlegen oder Ändern von Datensätzen von den ebenfalls generierten (HTML-)Formularen in den Templates stammen. Für die API allerdings müssen diese stattdessen aus den HTTP-Bodys extrahiert werden, wo sie im XML-Format an die Schnittstelle übertragen werden.

Bei einer dedizierten Applikation für die API kann auch die View-Konfiguration geändert werden (Listing 4): Ein Layout wird jetzt nicht mehr benötigt, da die Templates aus reinem XML bestehen. Außerdem wird auch der standardmäßige Content-Type angepasst.

Anpassungen an der view.yml für die API 
default:
	http_metas:
		content-type: text/xml
	has_layout:     off

Listing 4

Symfony und das Doctrine-ORM stellen für das Routing einen nützlichen Shortcut bereit:

Shortcut in der routing.yml 
book:
	class:   sfDoctrineRouteCollection
	options: { model: Book }

Listing 5

Diese Regel wird zu einer Reihe REST-konformer Routen aufgelöst, ähnlich wie jener in Listing 1, die man sich in der Zusammenfassung auch durch die Kommandozeile anzeigen lassen kann:

Routen in der API 
$ symfony app:routes
>> app       Current routes for application
Name		Method          Pattern
book				  GET    	/book.:sf_format
book_new		 GET    	/book/new.:sf_format
book_create	 POST   	/book.:sf_format
book_edit		 GET    	/book/:id/edit.:sf_for
book_update	PUT	/book/:id.:sf_format
book_delete	 DELETE 	/book/:id.:sf_format
book_show		GET    	/book/:id.:sf_format

Listing 6

Diese Routen verweisen auf die jeweiligen Methoden (new, create, edit, update usw.) im Actioncontroller des Book-Moduls, die später implementiert werden müssen.

Für den Einsatz innerhalb einer API sollten die „Edit“- und „New“-Methoden nicht implementiert werden beziehungsweise eine Fehlermeldung werfen, da eine GET-Methode niemals in der Lage sein sollte, Daten auf einem Server in irgendeiner Weise zu ändern (oder zu erstellen). Das ist ausschließlich den idempotenten Methoden PUT, POST und DELETE vorbehalten.

Authentifizierung durch API-Keys

Natürlich sollen nur autorisierte Benutzer Datensätze erstellen oder ändern dürfen. Bevor also eine Aktion ausgeführt wird, muss sich der Benutzer zunächst gegenüber der API authentifizieren. Eine Möglichkeit dazu ist die Verwendung von API-Keys. Der API-Key kann im HTTP-Header (zum Beispiel als „X-ApiKey“) mitgeschickt werden und dient als Ersatz für die übliche Benutzernamen/Passwort-Kombination (sinnvollerweise sollten Requests an die API in diesem Fall per HTTP/S gesichert werden). Die Authentifizierung kann in Symfony als Filter (Listing 7) implementiert werden, der vor dem Abarbeiten des eigentlichen Requests im Controller durchlaufen wird und in der filter.yml aktiviert werden muss.

API-Filter Klasse 
class apiFilter extends sfFilter {
	public function execute($filterChain) {
		if ($this->isFirstCall()) {
	       $request = $this->getContext()->getRequest();
     		  $session = $this->getContext()->getUser();
             // Get API-Key from HTTP-Header
             $apikey = $request->getHttpHeader('X-ApiKey');
             // Select user from database by given API-Key
             $userprofile = Doctrine::getTable('User')->findOneByApikey($apikey);
             // Throw an error if user cannot be found...
             jeHTTPError::forwardUnless($userprofile, 401, 'Invalid or missing API-Key');
             //  ...or save the user in the session
             $session->setAttribute('user',$userprofile);
        }
		// Execute next filter in filter symfonys filter chain
		$filterChain->execute();
		// Code to execute AFTER the action execution, before the rendering
		// Delete User-Session after every API-Request
		session_unset();
		session_destroy();
	}
}

Listing 7

Dieser Filter lädt zum einen das Benutzerobjekt anhand des API-Keys im Header aus der Datenbank und speichert es in der Symfony-Benutzersession, sodass es überall im Code zur Verfügung steht. Ist der API-Key nicht korrekt, wird stattdessen eine Fehlermeldung mit entsprechendem HTTP-Statuscode gesendet. Zum anderen muss die Session wieder gelöscht werden, nachdem der Request abgearbeitet wurde. REST ist per Definition zustandslos, das heisst eine „klassische“ Session wird nicht gespeichert.

Für das Error-Handling kann wie in Listing 7 das jeHTTPErrorException-Plugin genutzt werden, welches das Symfony-Error Handling um die Fähigkeit erweitert, beliebige HTTP-Statuscodes und Fehlermeldungen in der Response zurückzugeben. Gerade im Zusammenhang mit REST ist das eine praktische Sache. Das Plugin lässt sich mit Hilfe des CLI schnell installieren:

Plugin installieren 
$ symfony plugin:install jeHTTPErrorExceptionPlugin

Listing 8

Bitte beachte unsere Community-Richtlinien

Wir freuen uns über kontroverse Diskussionen, die gerne auch mal hitzig geführt werden dürfen. Beleidigende, grob anstößige, rassistische und strafrechtlich relevante Äußerungen und Beiträge tolerieren wir nicht. Bitte achte darauf, dass du keine Texte veröffentlichst, für die du keine ausdrückliche Erlaubnis des Urhebers hast. Ebenfalls nicht erlaubt ist der Missbrauch der Webangebote unter t3n.de als Werbeplattform. Die Nennung von Produktnamen, Herstellern, Dienstleistern und Websites ist nur dann zulässig, wenn damit nicht vorrangig der Zweck der Werbung verfolgt wird. Wir behalten uns vor, Beiträge, die diese Regeln verletzen, zu löschen und Accounts zeitweilig oder auf Dauer zu sperren.

Trotz all dieser notwendigen Regeln: Diskutiere kontrovers, sage anderen deine Meinung, trage mit weiterführenden Informationen zum Wissensaustausch bei, aber bleibe dabei fair und respektiere die Meinung anderer. Wir wünschen Dir viel Spaß mit den Webangeboten von t3n und freuen uns auf spannende Beiträge.

Dein t3n-Team

6 Kommentare
fishbone
fishbone

Mit der Api-Key Lösung durch einen Filter entsteht das Problem, dass jeder vollen Zugriff hat, sobald z.B. aufgrund eines Fehlers ein Record erzeugt wurde der keinen Api-Key enthält.

Im Allgemeinen suche ich eine saubere Lösung für das Authentifizierungsproblem für Webservices in Symfony. Das wird im Jobeet-Tutorial geschickt übergangen, weil es in dem verwendeten Beispiel aufgrund des reinen Lesezugriffs nicht erforderlich ist. Um das Sessionbasierte Sicherheitssystem hinreichend anzupassen, bin ich jetzt seit Tagen damit beschäftigt, den kompletten Aufbau von Symfony zu analyisieren.
Dabei kommt mir immer wieder in den Sinn, dass es dafür bereits fertige saubere Lösungen geben müsste.
Vor allem denke ich, dass in puncto Sicherheit generell keine Workarounds angebracht sind.

Antworten
Daniel Haller
Daniel Haller

Hallo fishbone,

glaube, so ganz verstehe ich Dein Problem nicht. Zugriffe mit ungültigen API-Keys werden mit einem Fehler 401 geblockt. Natürlich sollte man die Keys so aufbauen, dass sie nicht erraten werden können und eine SSL-gesicherte Verbindung verwenden.

Wenn ich Dich richtig verstanden habe, hast Du aber mit einem anderen Anwendungsfall Bauchschmerzen: Ein User könnte sich gegenüber der API mit einem korrekten Key authentifizieren und anschließend auf Datensätze zugreifen, die nicht von ihm selbst stammen. Habe ich das richtig verstanden? Das wäre allerdings keine Lücke, die man auf Ebene der API lösen kann – dasselbe Problem hätte man schließlich auch bei einer „herkömmlichen“ Authentifizierung.
Natürlich muss man auf Applikationsebene sicherstellen, das ein bereits authentifizierter User (egal ob er sich mit einem API-Key oder Name/Passwort-Kombination authentfiziert hat) auch nur auf seine eigenen Datensätze zugreifen kann – auch dann wenn die Datensätze fehlerhaft in der DB abgelegt wurden (was ja schon an der Stelle verhindert werden müßte).

Hilft Dir das weiter? Oder habe ich Dich missverstanden?

vg,
Daniel

Antworten
fishbone
fishbone

Wie bereits gesagt, entsteht das Problem nur, falls das ApiKey-Feld in der Tabelle für einen Benutzer leer ist. Wenn kein Api-Key bei der Authentifizierung übergeben wird, stimmen die ApiKeys überein (übergebener und leerer in der Datenbank).

Es ist klar, dass dies ein grundlegender Fehler ist. Die Spalte Api-Key darf natürlich nicht leer oder NULL sein. Allerdings kann dies passieren, wenn man die Schnittstelle erst in einer späteren Version hinzufügt und bereits Benutzerdaten vorhanden sind. Bei der Migration kann durchaus ein Fehler entstehen, der ein leerbleibendes ApiKey-Feld zur Folge hat. Auch wenn das unwahrscheinlich ist, möglich ist es.

Deswegen leite ich in meinem sfFilter sofort zu 401 weiter, falls kein ApiKey übergeben worden ist. Ich weiß aber nicht mehr, wie ich das im Detail gelöst habe. Denn wenn sich jemand über Cookie authentifiziert, wird (korrekterweise) auch kein ApiKey übergeben.

Zum Thema saubere Lösung:
Schnittstellen sind kein Sonderfall. Trotzdem muss man sich mit dem Aufbau des Sicherheitssystem befassen, während man für die normale Authentifizierung auf die offiziellen Symfonyplugins zurückgreifen kann. Vielleicht hätte ich da von Symfony einfach mehr erwartet. Aber schon Methoden wie forward404If() zeigen, dass Symfony 1 nicht unbedingt für REST ausgelegt ist.

Antworten
Daniel Haller
Daniel Haller

Ja, da hast Du recht – die Version 1.x von symfony ist noch nicht so 100%ig RESTfull wie das bspw. bei Rails der Fall ist. Ich kam leider noch nicht dazu, mich mit 2.x auseinander zu setzen, aber soweit ich bisher gelesen habe, wird hier sehr viel stärker auf eine RESTartige Architektur gesetzt. Ich würde mich wundern, wenn im Rahmen dessen nicht auch eine Authentifizierungslösung integriert wärde.

Ein leeres API-Key Feld darf aber natürlich nicht sein wenn man eine Schnittstelle implementiert, das ist klar. Ich bin in meinem Fall davon ausgegangen, dass es sich schon auf DB-Ebene um ein NOT NULL-Feld handelt, dass in jedem Fall befüllt sein muss – vielleicht hätte ich in dem Artikel noch darauf hinweisen sollen.

Antworten
fishbone
fishbone

Auf jeden Fall danke ich für den Artikel, der mich bei diesem Projekt, das inzwischen abgeschlossen ist, sehr unterstützt hat.

Antworten
Daniel Haller
Daniel Haller

Freut mich zu hören, Dankeschön :)

Antworten

Melde dich mit deinem t3n Account an oder fülle die unteren Felder aus.

Bitte schalte deinen Adblocker für t3n.de aus!

Hey du! Schön, dass du hier bist. 😊

Bitte schalte deinen Adblocker für t3n.de aus, um diesen Artikel zu lesen.

Wir sind ein unabhängiger Publisher mit einem Team bestehend aus 65 fantastischen Menschen, aber ohne riesigen Konzern im Rücken. Banner und ähnliche Werbemittel sind für unsere Finanzierung sehr wichtig.

Danke für deine Unterstützung.

Digitales High Five,
Stephan Dörner (Chefredakteur t3n.de) & das gesamte t3n-Team

Anleitung zur Deaktivierung