Requests in mehreren Schritten behandeln
- Requests werden in der Regel von mehreren Handlern hintereinander behandelt
- entspricht meist verschiedenen logischen Schichten der App
- z.B. CSRF Schutz → Sitzung überprüfen → Autorisierung → Applikationslogik
- Die Go Library Alice macht das Verketten von Handlern einfach
- Handler müssen mit einer Konstruktorfunktion mit vorgegebener Signatur erzeugt werden können:
func MyHandler(http.Handler) http.Handler
- Handler bearbeiten Request: alles ok → nächster Handler in der Kette, sonst Abbruch der Request-Bearbeitung
- Alice sorgt dafür, dass die Handler in der richtigen Reihenfolge und mit dem passenden nächsten Handler konstruiert werden
- So können Ketten von Handlern einfach angelegt werden
anyChecking := alice.New(nosurf.NewPure, controller.SessionChecker, controller.AuthAny) mux.Handle("/work", anyChecking.ThenFunc(controller.HandleWork))
Auf der Github Seite von Alice ist beschrieben, wie auch Handler mit mehreren Parametern in der Konstruktorfunktion angepasst werden können.
Eigene Request Filter implementieren
Mit wenig Aufwand können eigene Filter implementiert werden. Zuerst brauchen wir ein bischen Infrastruktur-Code:
// filter is called before chaining handlers. Next handler in // the chain is only called when filter returns true type filter func(http.ResponseWriter, *http.Request, interface{}) bool // a struct to chain several handlers for use with alice type chainableHandler struct { filter filter chain http.Handler } // make chainableHandler an http.Handler func (c chainableHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if c.filter(w, r, nil) { c.chain.ServeHTTP(w, r) } }
- 12
- Der abstrakte Funktionstyp chainfunc definiert die Signatur für den Filter-Algorithmus. Der 3. Parameter vom Typ interface{} macht die Filterfunktion flexibel, sie kann einen beliebigen weiteren Parameter bekommen.
- 15-18
- Die struct chainableHandler speichert eine Filterfunktion und den Link zum nächsten Handler in der Kette.
- 21-25
- Die struct chainableHandler bekommt eine Methode ServeHTTP(…) und implementiert damit das http.Handler Interface. ServeHTTP ruft den nächsten Handler nur dann auf, wenn die Filterfunktion true liefert.
- Damit ist die Basis für eigene Filter in der Alice Filterkette vorhanden.
Einfaches Beispiel: ein Request Logger
Die Implementierung eines einfachen Filters besteht mit der oben beschriebenen Infrastruktur aus zwei Schritten:
- Filterfunktion implementieren (als Beispiel logRequest(…) ).
- Public Konstruktorfunktion RequestLogger anlegen. Diese bettet die Filterfunktion in die struct chainableHandler ein. In Zeile 59 ist ein typecast auf den Funktionstyp chainfunc notwendig.
// logRequest writes relevant information from the request // to the logging output func logRequest(w http.ResponseWriter, r *http.Request, ignore interface{}) bool { log.Printf("Host: %s, URL: %s, URI: %s\n", r.Host, r.URL.Path, r.RequestURI) return true }
// RequestLogger embeds logRequest function into // chainableHandler struct and returns it func RequestLogger(h http.Handler) http.Handler { c := chainableHandler { filter: chainfunc(logRequest), chain: h, } return c }
Session Checker Filter
Dieser Filter überprüft, ob eine Gorilla Session vorhanden ist. Der Aufbau ist weitgehend analog zum Logging Filter, jedoch treten einige Fälle auf, in denen false zurückgegeben wird. Damit wird der Aufruf der weiteren Handler in der Filterkette unterbunden. In Zeilen 41 und 49 erkennt man, wie der Filter in den Fluss der Webseiten durch ein Redirect eingreifen kann, wenn nach einem Server-Neustart eine ungültige Sitzung entdeckt wird.
// SessionChecker filter checks if there is a valid session, // i.e if someone is logged in func SessionChecker(h http.Handler) http.Handler { c := chainableHandler{chain: h, filter: filter(checkSession)} return c } // here the session check is actually implemented func checkSession(w http.ResponseWriter, r *http.Request) bool { session, err := SessionStore().Get(r, S_DKFAI) if err != nil { if err.(scc.Error).IsDecode() { // recover from an old hanging session going to login http.Redirect(w, r, "/login", http.StatusFound) } else { http.Error(w, err.Error(), http.StatusInternalServerError) } return false } if session.IsNew { // no session there, goto login http.Redirect(w, r, "/login", http.StatusFound) return false } return true }
Aufbau von Filtern mit zusätzlichen Parametern
Häufig benötigt man Filterfunktionen, die noch einen zusätzlichen Parameter besitzen. Da Alice nicht vorsieht, dass ein Filter noch weitere Parameter im Konstruktor bekommt, können wir eine eigene Filterfunktion für jeden sinnvollen Wert dieses Parameters anlegen. Das geht natürlich nur, wenn es einen relativ kleinen Wertebereich für diesen Parameter gibt. Die Hilfsfunktion makeFilter(…) macht die Konstruktion derartiger Filter einfacher, indem sie einer Funktion mit drei Parametern einen Filter mit den für Alice notwendigen 2 Parametern erzeugt.
Wenn die Filterfunktion also weitere Parameter besitzt, müssen diese jeweils für einen bestimmten Wert festgehalten werden. Dadurch wird die Anzahl der Parameter reduziert, wir erhalten eine Funktion vom Typ filter. In der funktionalen Programmierung wird das nach dem Mathematiker Haskel Curry als Currying bezeichnet. Es gibt also ein oder zwei weitere Schritte:
- Eine „Currying“-Funktion implementieren, wenn makeFilter nicht passt.
- Für jeden sinnvollen Wert der Zusatzparameter eine Konstruktorfunktion für den Filter-Handler anlegen. Das wird weiter unten am Beispiel des Authorization Filter demonstriert.
// reduce additional function parameter to get a filter func makeFilter(f func(http.ResponseWriter, *http.Request, interface{}) bool, mask interface{}) filter { return func(w http.ResponseWriter, r *http.Request) bool { return f(w, r, mask) } }
- 89-90
- makeFilter ist eine Funktion, die als Parameter eine Funktion
f func(http.ResponseWriter, *http.Request, interface{}) bool mit drei Parametern und Rückgabetyp bool sowie ein Interface mask interface{} besitzt und eine Funktion vom Typ filter zurückgibt. - 92
- Die anonyme Funktion, die zurückgegeben wird, ruft in ihrer Implementierung die als Parameter übergebene Funktion f mit dem 3. Parameter mask auf, der damit festgehalten wird.
Authorization Filter
Dieser Filter verwendet die in der Session abgespeicherten Informationen über den aktuellen User, um seine Zugriffsrechte auf eine URL zu überprüfen.Das Beispiel verwendet ein einfaches Rollenmodell zur Rechteverwaltung. Die eigentliche Filterfunktion ist checkAuth(…) .
// checkAuth is the filter function where the actual authorizing is done func checkAuth(w http.ResponseWriter, r *http.Request, mask interface{}) bool { session, e0 := SessionStore().Get(r, S_DKFAI) m, ifaceok := mask.(int) role, sessionok := session.Values["role"].(int) if e0 != nil || !ifaceok || !sessionok { http.Error(w, "error validating role", http.StatusInternalServerError) return false } if role & m == 0 { http.Error(w, "Not Authorized", http.StatusUnauthorized) return false } return true }
Wie zu erkennen ist, braucht diese Funktion noch einen zusätzlichen Parameter (Z. 73), der als Maskierung für die role Variable dient (Z. 81). Mit makeFilter(…) (s.o.) wird daraus ein Filter mit den für Alice notwendigen 2 Parametern erzeugt.
Damit lassen sich jetzt sehr einfach die Autorisierungsfilter für die fünf verschiedenen Rollen erzeugen.
// authorize for anyone who is logged in func AuthAny(h http.Handler) http.Handler { c := chainableHandler{ filter: makeFilter(checkAuth, model.U_ALL), chain: h, } return c } // authorize for deans office staff for enrolling func AuthEnrol(h http.Handler) http.Handler { c := chainableHandler{ filter: makeFilter(checkAuth, model.U_ENROL), chain: h, } return c } // authorize for project office staff func AuthProjectOffice(h http.Handler) http.Handler { c := chainableHandler{ filter: makeFilter(checkAuth, model.U_POFF), chain: h, } return c } // authorize for user administrator func AuthUserAdmin(h http.Handler) http.Handler { c := chainableHandler{ filter: makeFilter(checkAuth, model.U_UADMIN), chain: h, } return c } // authorize for master administrator func AuthMasterAdmin(h http.Handler) http.Handler { c := chainableHandler{ filter: makeFilter(checkAuth, model.U_FULLADMIN), chain: h, } return c }
- 99, 108, 117, 126, 135
- In diesen Zeilen wird mit makeFilter eine Filterfunktion aus der Funktion checkAuth() erzeugt. Dabei wird jeweils eine Konstante als 3. Parameter an checkAuth übergeben.
- 97-103
- AuthAny() implementiert damit eine Handlerfunktion für die Rolle, die durch die Konstante model.U_ALL vorgegeben ist.
- 106-112, 115-121, 124-130, 133-139
- Entsprechend werden auch die Filterfunktionen für die anderen definierten Rollen implementiert.