Diferentes redirecionamentos Nginx baseados na resposta do proxy upstream

6

Eu tenho um servidor upstream gerenciando o login do nosso site. Em um login bem-sucedido, quero redirecionar o usuário para a parte segura do site. Em caso de falha no login, quero redirecionar o usuário para o formulário de login.

O servidor a montante retorna um 200 OK em um login bem-sucedido e um 401 Unauthorized em um login com falha.

Esta é a parte relevante da minha configuração:

{
    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
    }
}

Esta configuração funciona ao ter sucesso no login. O cliente é redirecionado com um 302. Isso não funciona ao falhar no login. O servidor upstream retorna 401 e eu esperava que o error_page entrasse em vigor. Mas ainda recebo o 302. Se eu remover a linha return 302 /secure/ , o redirecionamento para a página de login funcionará. Então parece que eu posso ter um, mas não os dois.

Pergunta bônus; Eu duvido que a maneira que eu ligo o error_page com esse local nomeado é o Caminho. Estou correto em fazer assim?

editar : Acontece que ter um return no bloco location faz o Nginx não usar o proxy_pass . Portanto, faz sentido que a página de erro não seja atingida. O problema sobre como fazer isso, no entanto, permanece.

    
por harm 15.12.2016 / 20:04

4 respostas

3

A solução exata para a questão é usar os recursos Lua do Nginx.

No Ubuntu 16.04 você pode instalar uma versão do Nginx que suporta Lua com:

$ apt install nginx-extra

Em outros sistemas, pode ser diferente. Você também pode optar por instalar o OpenResty.

Com Lua você tem acesso total à resposta do upstream. Note que você aparece para ter acesso ao status upstream através da variável $upstream_status . E de uma forma que você faz, mas devido à maneira como as instruções 'if' são avaliadas no Nginx, você não pode usar $upstream_status na instrução 'if' condicional.

Com Lua, sua configuração será semelhante a:

    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;
    }

Bastante direto. As únicas duas peculiaridades são a leitura do corpo da solicitação para transmitir os parâmetros do POST e a configuração do cookie na resposta final ao cliente.

O que eu realmente acabei fazendo, depois de muita insistência da comunidade, é que lidei com as respostas do fluxo upstream no lado do cliente. Isso deixou o servidor upstream inalterado e minha configuração Nginx simples:

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

O cliente que inicializa o pedido manipula a resposta do upstream:

  <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>

A solução acima atinge meu objetivo de ter uma clara separação de interesses. O servidor de autenticação ignora os casos de uso que os chamadores desejam suportar.

    
por 28.12.2016 / 10:44
1

Embora eu concorde totalmente com @ michael-hampton, ou seja, que esse problema não seja não manipulado pelo nginx, você tentou mover error_page para o bloco de localização:

{
    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
    }
}
    
por 15.12.2016 / 21:08
0

Não tenho certeza se o seguinte funciona, e compartilho a visão de Michael, mas você pode tentar usar o HTTP auth request module , talvez algo ao longo destas linhas:

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
}

Você usaria este snippet de configuração não no servidor que faz a autenticação real, mas no servidor que hospeda o conteúdo protegido. Eu não pude testar isso agora, mas talvez isso ainda seja de alguma ajuda para você.

Em relação à sua pergunta de bônus: Sim, AFAIK, é assim que isso deve ser tratado.

    
por 22.12.2016 / 21:45
0

Comportamento que você vê é esperado, pois return substituirá qualquer que tenha sido a reescrita.

Embora eu concorde completamente com Michael Hampton, se você está realmente fora de outras opções, você pode tentar algo ao longo do seguinte. Por favor, tenha em mente que este é um hack sujo, você realmente precisa considerar sua arquitetura em primeiro lugar:

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/;
         }
     }
}
    
por 23.12.2016 / 11:36