根据上游代理响应进行不同的 Nginx 重定向

根据上游代理响应进行不同的 Nginx 重定向

我有一个上游服务器处理我们网站的登录。登录成功后,我想将用户重定向到网站的安全部分。登录失败后,我想将用户重定向到登录表单。

200 OK如果登录成功,上游服务器将返回,如果401 Unauthorized登录失败,则返回 。

这是我的配置的相关部分:

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

此设置仅在成功登录时有效。客户端将使用 302 重定向。这才不是登录失败时可以正常工作。上游服务器返回 401,我预计error_page随后会启动。但我仍然得到 302。如果我删除该return 302 /secure/行,重定向到登录页面即可正常工作。因此,似乎我可以选择其中之一,但不能同时选择两者。

附加问题:我怀疑我处理该命名位置的方式error_page是否正确。我这样做正确吗?

编辑:事实证明,块return中有一个location会使 Nginx 根本不使用proxy_pass。因此,错误页面不会被访问是合理的。然而,如何做到这一点的问题仍然存在。

答案1

精确的解决这个问题的方法是使用 Nginx 的 Lua 功能。

在 Ubuntu 16.04 上,你可以使用以下命令安装支持 Lua 的 Nginx 版本:

$ apt install nginx-extra

在其他系统上可能会有所不同。您也可以选择安装 OpenResty。

使用 Lua,您可以完全访问上游响应。请注意,您出现通过变量访问上游状态$upstream_status。在某种程度上,您可以这样做,但由于 Nginx 中“if”语句的评估方式,您无法$upstream_status在“if”语句中使用条件。

使用 Lua 你的配置将如下所示:

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

非常简单。唯一的两个怪癖是读取请求主体以传递 POST 参数和在最终响应中设置 cookie 给客户端。


实际上在社区的大量敦促下,我最终决定在客户端处理上游流响应。这保持上游服务器不变,我的 Nginx 配置也很简单:

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

发起请求的客户端处理上游响应:

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

上述解决方案实现了我的目标,即明确分离关注点。身份验证服务器不了解调用者想要支持的用例。

答案2

虽然我完全同意@michael-hampton的观点,即这个问题应该不是由 nginx 处理,您是否尝试过移入error_page位置块:

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

答案3

我不完全确定下面的方法是否有效,我同意迈克尔的观点,但你可以尝试使用HTTP 身份验证请求模块,可能是这样的:

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
}

您可以使用这个配置片段不是在执行实际身份验证的服务器上,但在托管受保护内容的服务器上。我现在无法测试这一点,但也许这对你仍然有帮助。

关于您的奖励问题:是的,据我所知,应该这样处理。

答案4

您需要 proxy_intercept_errors on在服务器级别进行设置以处理代理错误。

server {
    server_name mydomain.com;
    proxy_intercept_errors on;

    location @error401 {
        return 302 /login.html # this page holds the login form
    }
    location / {
        proxy_pass http://localhost:8080;
        error_page 401 = @error401;
    }
}

相关内容