Verschiedene Nginx-Weiterleitungen basieren auf der Upstream-Proxy-Antwort

7

Ich habe einen Upstream-Server, der das Login unserer Website verwaltet. Bei einer erfolgreichen Anmeldung möchte ich den Benutzer zum sicheren Teil der Site umleiten. Bei einem fehlgeschlagenen Login möchte ich den Benutzer zum Login-Formular umleiten.

Der Upstream-Server gibt 200 OKbei einer erfolgreichen Anmeldung eine und 401 Unauthorizedbei einer fehlgeschlagenen Anmeldung eine zurück.

Dies ist der relevante Teil meiner Konfiguration:

{
    error_page 401 = @error401
    location @error401 {
        return 302 /login.html # this page holds the login form
    }

    location = /login { # this is the POST target of the login form
        proxy_pass http://localhost:8080;
        proxy_intercept_errors on;
        return 302 /secure/; # without this line, failures work. With it failed logins (401 upstream response) still get 302 redirected
    }
}

Dieses Setup funktioniert, wenn die Anmeldung erfolgreich war. Der Client wird mit einem 302 umgeleitet. Dies funktioniert nicht , wenn die Anmeldung fehlschlägt. Der Upstream-Server gibt 401 zurück und ich habe erwartet, dass der error_pagedann einschaltet. Aber ich bekomme immer noch den 302. Wenn ich die return 302 /secure/Zeile entferne, funktioniert die Umleitung zur Anmeldeseite. Es scheint also, dass ich eins haben kann, aber nicht beide.

Bonus-Frage; Ich bezweifle, dass die Art und Weise, wie ich error_pagemit dem genannten Ort umgehe, The Way ist. Habe ich recht damit?

edit : Es stellt sich heraus, dass ein returnim locationBlock enthaltenes Nginx das überhaupt nicht verwendet proxy_pass. Es ist also sinnvoll, dass die Fehlerseite nicht getroffen wird. Das Problem, wie dies zu tun ist, bleibt jedoch bestehen.

Schaden
quelle
3
Können Sie den Upstream-Server reparieren? Diese Art der Umleitung gehört im Allgemeinen in die Anwendung, nicht in den Webserver.
Michael Hampton
Nicht wirklich. Dieser Server wird auch als API-Authentifizierungsserver verwendet. Außerdem glaube ich nicht, dass es wirklich kaputt ist. 200 OKscheint beim Erfolg der Anmeldung völlig vernünftig zu sein (dies ergibt einen Cookie) und 401 Unauthorizedscheint auch in Ordnung zu sein.
Schaden
Sie sind gute Antworten von einem Backend-API-Server. Aber nicht so sehr vom Frontend.
Michael Hampton
Daher die Frage. :)
Schaden
2
Auch dies gehört in Ihre Anwendung, nicht in die Webserverkonfiguration.
Michael Hampton

Antworten:

4

Die genaue Lösung der Frage besteht darin, die Lua-Funktionen von Nginx zu nutzen.

Unter Ubuntu 16.04 können Sie eine Version von Nginx installieren, die Lua unterstützt mit:

$ apt install nginx-extra

Auf anderen Systemen kann dies anders sein. Sie können sich auch für die Installation von OpenResty entscheiden.

Mit Lua haben Sie vollen Zugriff auf die Upstream-Antwort. Beachten Sie , dass Sie scheinbar über die $upstream_statusVariable Zugriff auf den Upstream-Status haben . Und auf eine Art und Weise, die Sie tun, aber aufgrund der Art und Weise, wie 'if'-Anweisungen in Nginx ausgewertet werden, können Sie sie nicht $upstream_statusin der' if'-Anweisung verwenden.

Mit Lua sieht Ihre Konfiguration dann folgendermaßen aus:

    location = /login { # the POST target of your login form
           rewrite_by_lua_block {
                    ngx.req.read_body()
                    local res = ngx.location.capture("/login_proxy", {method = ngx.HTTP_POST})
                    if res.status == 200 then
                            ngx.header.Set_Cookie = res.header["Set-Cookie"] # pass along the cookie set by the backend
                            return ngx.redirect("/shows/")
                    else
                            return ngx.redirect("/login.html")
                    end
            }
    }

    location = /login_proxy {
            internal;
            proxy_pass http://localhost:8080/login;
    }

Ziemlich einfach. Die einzigen zwei Macken sind das Lesen des Anforderungshauptteils, um die POST-Parameter weiterzugeben, und das Setzen des Cookies in der endgültigen Antwort an den Client.


Was ich tatsächlich getan habe, nachdem ich viel von der Community gestoßen wurde, ist, dass ich die Upstream-Stream-Antworten auf der Client-Seite bearbeitet habe. Dadurch blieb der Upstream-Server unverändert und meine Nginx-Konfiguration einfach:

location = /login {
       proxy_pass http://localhost:8080;
}

Der Client, der die Anforderung initialisiert, verarbeitet die Upstream-Antwort:

  <body>
    <form id='login-form' action="/login" method="post">
      <input type="text" name="username">
      <input type="text" name="password">
      <input type="submit">
    </form>
    <script type='text/javascript'>
      const form = document.getElementById('login-form');
      form.addEventListener('submit', (event) => {
        const data = new FormData(form);
        const postRepresentation = new URLSearchParams(); // My upstream auth server can't handle the "multipart/form-data" FormData generates.
        postRepresentation.set('username', data.get('username'));
        postRepresentation.set('password', data.get('password'));

        event.preventDefault();

        fetch('/login', {
          method: 'POST',
          body: postRepresentation,
        })
          .then((response) => {
            if (response.status === 200) {
              console.log('200');
            } else if (response.status === 401) {
              console.log('401');
            } else {
              console.log('we got an unexpected return');
              console.log(response);
            }
          });
      });
    </script>
  </body>

Die obige Lösung erreicht mein Ziel einer klaren Trennung der Bedenken. Der Authentifizierungsserver kennt die Anwendungsfälle nicht, die die Anrufer unterstützen möchten.

Schaden
quelle
Ohne deine Hilfe hätte ich es nicht geschafft!
Schaden
1

Obwohl ich @ michael-hampton vollkommen zustimme, dh, dass dieses Problem nicht von nginx behandelt werden sollte, haben Sie versucht, error_pagein den Standortblock zu wechseln:

{
    location @error401 {
        return 302 /login.html # this page holds the login form
    }

    location = /login { # this is the POST target of the login form
        proxy_pass http://localhost:8080;
        proxy_intercept_errors on;
        error_page 401 = @error401;
        return 302 /secure/; # without this line, failures work. With it failed logins (401 upstream response) still get 302 redirected
    }
}
2ps
quelle
Ich habe das jetzt versucht und es machte keinen Unterschied. :(
Schaden
0

Ich bin mir nicht ganz sicher, ob das Folgende funktioniert, und ich teile die Ansicht von Michael, aber Sie könnten versuchen, das HTTP-Authentifizierungsanforderungsmodul zu verwenden , vielleicht etwas in dieser Richtung:

location /private/ {
    auth_request /auth;
    ... 
}

location = /auth {
    proxy_pass ...
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
    # This is only needed if your auth server uses this information,
    # for example if you're hosting different content which is 
    # only accessible to specific groups, not all of them, so your
    # auth server checks "who" and "what"
    proxy_set_header X-Original-URI $request_uri;
    error_page 401 = @error401;
}

location @error401 {
    return 302 /login.html
}

Sie würden dieses Konfigurations-Snippet nicht auf dem Server verwenden, der die eigentliche Authentifizierung durchführt, sondern auf dem Server, auf dem sich der geschützte Inhalt befindet. Ich konnte das momentan nicht testen, aber vielleicht hilft dir das immer noch.

Zu Ihrer Bonusfrage: Ja, AFAIK, so sollte das gehandhabt werden.

gf_
quelle
Das ist mehr oder weniger das, worauf ich gekommen bin. Das Problem ist jedoch, dass ich eine 200-OK-Antwort vom Upstream-Server umleiten möchte. error_pageIch werde das nicht tun lassen. Das Hinzufügen returndes locationBlocks verhindert, dass die andere error_pageDirektive funktioniert.
Schaden
@harm Warum willst du umleiten, wenn du eine bekommst 200 OK? Warum ist es nicht möglich, den Benutzer auf den Inhalt zugreifen zu lassen, wenn er einen erhält 200 OK, und nur umzuleiten, wenn er einen erhält 401?
gf_
Ich mache das, wenn ein Benutzer darauf zugreift /secure, auth_requestund das funktioniert absolut großartig. Aber ich suche nach einer Lösung, wenn sich ein Benutzer anmeldet . Der Benutzer sendet ein Formular an /login. Entweder hat er eine korrekte Kombination aus Benutzername und Passwort angegeben oder nicht. Wenn er zu umgeleitet hat /secure(was eine ergibt auth_request), wenn er nicht zum Anmeldeformular zurückgeleitet hat.
Schaden
Ich fürchte , ich verstehe es nicht, aber vielleicht dies ist keine Hilfe. Mein Vorschlag wird dort besser beschrieben.
gf_
Hahaha! Das ist genau die Vorlage, die ich beim Einrichten verwendet habe. Dieser Artikel war / ist enorm hilfreich! Danke vielmals. Sie haben login.example.comdem Benutzer das Auth-Cookie zur Verfügung gestellt und haben wahrscheinlich alle möglichen Verhaltensweisen in dieser Domain (Umgang mit Registrierung und Anmeldefluss). Ich möchte das in Nginx kodieren, damit ich dafür keine separate App schreiben muss. Aber es scheint, ich muss.
Schaden
0

Das Verhalten, das Sie sehen, wird erwartet, da returnes alles ersetzt, was Ihre Umschreibungen generiert haben.

Ich stimme Michael Hampton zwar voll und ganz zu, aber wenn Sie wirklich keine anderen Optionen haben, können Sie Folgendes ausprobieren. Bitte denken Sie daran, dass dies ein schmutziger Hack ist. Sie müssen Ihre Architektur in erster Linie berücksichtigen:

upstream backend {
    server http://localhost:8080;
}

server {
     location / {
         proxy_pass http://backend;
         if ($upstream_status = 401) {
             return 302 /login.html;
         }
         if ($upstream_status = 200) {
             return 302 /secure/;
         }
     }
}
Peter Zhabin
quelle
$upstream_statusscheint völlig ignoriert zu werden. Bei dieser Konfiguration ist die Antwort nur die Upstream-Antwort.
Schaden
Die Tatsache, dass die Variable ignoriert wird, habe ich vor BTW bemerkt. Es ist ziemlich seltsam. Wenn ich es als Header hinzufügen (Ich habe keine Ahnung , wie dies zu debuggen in irgendeiner anderen Art und Weise sane) etwa so: add_header X-Debug $upstream_status always;Ich kann den richtigen Status ausgegeben sehen. Vielleicht sollte der Test in der if-Anweisung anders sein?
Schaden
Und genug Leute sagten, das sei eine schlechte Idee. Ich kann den Upstream-Server nicht ändern. Soll ich dann etwas zwischen den aktuellen Upstream und Nginx setzen? Etwas, das 200zu 302 /secure/und 401zu übersetzt 302 /login.html?
Schaden
Ich glaube, dass dies passiert, weil, wenn es tatsächlich ausgewertet wird, bevor die Anfrage gestellt wird, Sie im Moment kein Nginx außerhalb der Produktion haben, um dies zu testen. Aber Sie können dann Lua-Modul für Nginx versuchen, mit dem Sie tun können, was Sie mit Antworten wollen ..
Peter Zhabin
1
Ja, dies von Grund auf neu zu
erstellen