Entwicklung & Design

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

Seite 4 / 4

Der Controller des Book-Moduls

In Listing 5 werden Routen auf die Index-, Show-, Create-, Update-, und Delete-Methoden des Book-Controllers erzeugt, die implementiert werden müssen. Die sfDoctrineRouteCollection-Klasse koppelt dabei ein Datenbankobjekt direkt mit dem ID-Parameter in der URL, sodass man im Controller darauf zugreifen und sich eigene Queries weitgehend sparen kann. Wird kein Datensatz mit der gewünschten ID gefunden, wirft Symfony einen 404-Fehler. Die Implementierung der Controller-Methoden geht daher schnell von der Hand. Um die Dateilansicht eines Buchs anzuzeigen, reicht eine Zeile im Controller:

Detailansicht eines Buchs 
// HTTP-Request: GET /book/[id] HTTP/1.1
public function executeShow(sfWebRequest $request) {
	$this->book = $this->getRoute()->getObject();  // $this->book is now available in the template
}

Listing 9

Ein Request für denselben URI mit der DELETE-Methode wird REST-konform zur Delete-Methode geroutet:

Ein Buch löschen 
// HTTP-Request: DELETE /book/[id] HTTP/1.1
public function executeDelete(sfWebRequest $request) {
	$book = $this->getRoute()->getObject();
	// User allowed to delete this Book? If not, stop execution and send HTTP-Statuscode 403
	$this->checkPermission($book);
	// ...User has Permission, let's delete this Book
	$book->delete();
}

Listing 10

Mit den POST- und PUT-Methoden wird ein neues Buch angelegt beziehungsweise ein bestehendes Buch geändert. Die dazu erforderlichen Daten werden als XML im HTTP-Body übertragen und müssen daher ausgelesen und geparst werden. Hierzu kann man im lib-Verzeichnis in einer Klasse eine einfache Methode (Listing 11) implementieren, die bei Gebrauch durch den Autoload-Mechanismus von Symfony inkludiert wird.

Auslesen der XML-Nutzlast 
class Tools {
	public static function getRequestBody ()    {
		$stream = fopen("php://input", "r");
		$data = stream_get_contents($stream);
		fclose($stream);
		return simplexml_load_string($data);
	}
}

Listing 11

Ein neues Buch lässt sich ebenfalls sehr einfach erzeugen:

Ein neues Buch anlegen 
// HTTP-Request: POST /book HTTP/1.1
public function executeCreate(sfWebRequest $request) {
	$data = Tools::getRequestBody();
	$book = new Book();
	$book->saveBook($data);  // Save Book in the Model
	$this->getResponse()->setStatusCode(201);
	$this->getResponse()->setHttpHeader('Location',
		$request->getHost().'/book/'.$book->get('id'));
}

Listing 12

In dieser Methode werden die Daten zunächst aus dem Request-Body extrahiert und an das Model übergeben, wo das neue Buch dann angelegt wird. Wichtig ist, dem Client im Response per HTTP-Statuscode mitzuteilen, dass die Ressource erzeugt wurde („201 – Created“) und deren (neue) URI im Location-Header anzugeben.

Analog dazu funktioniert das Editieren eines Buchs:

Ein bestehendes Buch editieren 
// HTTP-Request: PUT  /book/[id] HTTP/1.1
public function executeUpdate(sfWebRequest $request) {
	$data = Tools::getRequestBody();
	$book = $this->getRoute()->getObject();
	// User allowed to edit Book? If not, stop execution, send HTTP-Statuscode 403
	$this->checkPermission($book);
	$book->saveBook($data);
}

Listing 13

Für alle genannten Methoden müssen natürlich auch die jeweiligen (XML-)Templates angelegt werden, um einem Client das gewünschte Feedback – zum Beispiel in Form einer Statusmeldung – geben zu können.

Für die Validierung der Benutzerdaten bringt Symfony eine Reihe von Standard-Validatoren mit, die allerdings primär zur Verwendung mit dem Formular-Framework gedacht sind. Noch einfacher lassen sich alternativ die Validatoren des Doctrine-ORMs [4] verwenden, die man bereits in der Schema-Datei konfigurieren kann und die die Daten noch auf der Anwendungsebene unmittelbar vor dem Speichern in der Datenbank validieren.

Zusammenfassung

Mit Hilfe des Symfony-Frameworks lassen sich REST-basierte Schnittstellen sehr sauber und vergleichsweise einfach implementieren. Dieser Artikel zeigt nur einen von vielen Ansätzen, der sich aber grundsätzlich auch für umfangreiche APIs eignet – selbst wenn für den produktiven Einsatz noch einige Aspekte tiefer ausgearbeitet werden müssten.

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